Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
522
src/Framework/Database/Schema/Comparison/MigrationGenerator.php
Normal file
522
src/Framework/Database/Schema/Comparison/MigrationGenerator.php
Normal file
@@ -0,0 +1,522 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Schema\Comparison;
|
||||
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
|
||||
/**
|
||||
* Generates migration code from schema differences
|
||||
*/
|
||||
final readonly class MigrationGenerator
|
||||
{
|
||||
/**
|
||||
* Generate migration code from schema differences
|
||||
*
|
||||
* @param SchemaDifference $difference The schema differences
|
||||
* @param string $className The name of the migration class
|
||||
* @param string|null $description The description of the migration
|
||||
* @param MigrationVersion|null $version The version of the migration (generated if null)
|
||||
* @return string The generated migration code
|
||||
*/
|
||||
public function generateMigration(
|
||||
SchemaDifference $difference,
|
||||
string $className,
|
||||
?string $description = null,
|
||||
?MigrationVersion $version = null
|
||||
): string {
|
||||
if (! $difference->hasDifferences()) {
|
||||
throw new \InvalidArgumentException('Cannot generate migration from schema with no differences');
|
||||
}
|
||||
|
||||
$version = $version ?? MigrationVersion::fromDateTime(new \DateTimeImmutable());
|
||||
$description = $description ?? "Schema update from {$difference->sourceSchema} to {$difference->targetSchema}";
|
||||
|
||||
$upCode = $this->generateUpCode($difference);
|
||||
$downCode = $this->generateDownCode($difference);
|
||||
|
||||
return $this->generateMigrationClass($className, $description, $version, $upCode, $downCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the up method code
|
||||
*/
|
||||
private function generateUpCode(SchemaDifference $difference): string
|
||||
{
|
||||
$code = [];
|
||||
|
||||
// Handle missing tables (create them)
|
||||
foreach ($difference->missingTables as $tableName => $tableDef) {
|
||||
$code[] = $this->generateCreateTableCode($tableName, $tableDef);
|
||||
}
|
||||
|
||||
// Handle table differences
|
||||
foreach ($difference->tableDifferences as $tableDifference) {
|
||||
$code[] = $this->generateTableModificationCode($tableDifference);
|
||||
}
|
||||
|
||||
// Handle extra tables (drop them in down method)
|
||||
|
||||
return implode("\n\n", $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the down method code
|
||||
*/
|
||||
private function generateDownCode(SchemaDifference $difference): string
|
||||
{
|
||||
$code = [];
|
||||
|
||||
// Handle extra tables (drop them)
|
||||
foreach ($difference->extraTables as $tableName => $tableDef) {
|
||||
$code[] = " \$schema->dropIfExists('{$tableName}');";
|
||||
}
|
||||
|
||||
// Handle table differences (reverse the changes)
|
||||
foreach ($difference->tableDifferences as $tableDifference) {
|
||||
$code[] = $this->generateReverseTableModificationCode($tableDifference);
|
||||
}
|
||||
|
||||
// Handle missing tables (create them in down method)
|
||||
foreach ($difference->missingTables as $tableName => $tableDef) {
|
||||
$code[] = $this->generateDropTableCode($tableName);
|
||||
}
|
||||
|
||||
return implode("\n\n", $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code to create a table
|
||||
*/
|
||||
private function generateCreateTableCode(string $tableName, array $tableDef): string
|
||||
{
|
||||
$code = [];
|
||||
$code[] = " \$schema->create('{$tableName}', function (\$table) {";
|
||||
|
||||
// Add columns
|
||||
foreach ($tableDef['columns'] ?? [] as $columnName => $columnDef) {
|
||||
$type = $columnDef['type'] ?? 'string';
|
||||
$columnCode = " \$table->{$this->mapColumnType($type)}('{$columnName}')";
|
||||
|
||||
// Add modifiers
|
||||
if (isset($columnDef['length']) && $columnDef['length'] > 0) {
|
||||
$columnCode .= "->length({$columnDef['length']})";
|
||||
}
|
||||
|
||||
if (isset($columnDef['nullable']) && $columnDef['nullable']) {
|
||||
$columnCode .= '->nullable()';
|
||||
}
|
||||
|
||||
if (isset($columnDef['default'])) {
|
||||
$default = $this->formatDefaultValue($columnDef['default'], $type);
|
||||
$columnCode .= "->default({$default})";
|
||||
}
|
||||
|
||||
if (isset($columnDef['unsigned']) && $columnDef['unsigned']) {
|
||||
$columnCode .= '->unsigned()';
|
||||
}
|
||||
|
||||
if (isset($columnDef['autoincrement']) && $columnDef['autoincrement']) {
|
||||
$columnCode .= '->autoIncrement()';
|
||||
}
|
||||
|
||||
$columnCode .= ';';
|
||||
$code[] = $columnCode;
|
||||
}
|
||||
|
||||
// Add indexes
|
||||
foreach ($tableDef['indexes'] ?? [] as $indexName => $indexDef) {
|
||||
$columns = implode("', '", $indexDef['columns'] ?? []);
|
||||
|
||||
if (isset($indexDef['type']) && $indexDef['type'] === 'PRIMARY') {
|
||||
$code[] = " \$table->primary(['{$columns}']);";
|
||||
} elseif (isset($indexDef['unique']) && $indexDef['unique']) {
|
||||
$code[] = " \$table->unique(['{$columns}'], '{$indexName}');";
|
||||
} else {
|
||||
$code[] = " \$table->index(['{$columns}'], '{$indexName}');";
|
||||
}
|
||||
}
|
||||
|
||||
// Add foreign keys
|
||||
foreach ($tableDef['foreign_keys'] ?? [] as $fkName => $fkDef) {
|
||||
$columns = implode("', '", $fkDef['columns'] ?? []);
|
||||
$refTable = $fkDef['referenced_table'] ?? '';
|
||||
$refColumns = implode("', '", $fkDef['referenced_columns'] ?? []);
|
||||
|
||||
$fkCode = " \$table->foreignKey(['{$columns}'], '{$refTable}', ['{$refColumns}'], '{$fkName}')";
|
||||
|
||||
if (isset($fkDef['update_rule'])) {
|
||||
$fkCode .= "->onUpdate('{$fkDef['update_rule']}')";
|
||||
}
|
||||
|
||||
if (isset($fkDef['delete_rule'])) {
|
||||
$fkCode .= "->onDelete('{$fkDef['delete_rule']}')";
|
||||
}
|
||||
|
||||
$fkCode .= ';';
|
||||
$code[] = $fkCode;
|
||||
}
|
||||
|
||||
$code[] = ' });';
|
||||
|
||||
return implode("\n", $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code to drop a table
|
||||
*/
|
||||
private function generateDropTableCode(string $tableName): string
|
||||
{
|
||||
return " \$schema->dropIfExists('{$tableName}');";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code to modify a table
|
||||
*/
|
||||
private function generateTableModificationCode(TableDifference $difference): string
|
||||
{
|
||||
if (! $difference->hasDifferences()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$code = [];
|
||||
$code[] = " \$schema->table('{$difference->tableName}', function (\$table) {";
|
||||
|
||||
// Add missing columns
|
||||
foreach ($difference->missingColumns as $columnName => $columnDef) {
|
||||
$type = $columnDef['type'] ?? 'string';
|
||||
$columnCode = " \$table->add{$this->mapColumnType($type)}('{$columnName}')";
|
||||
|
||||
// Add modifiers
|
||||
if (isset($columnDef['length']) && $columnDef['length'] > 0) {
|
||||
$columnCode .= "->length({$columnDef['length']})";
|
||||
}
|
||||
|
||||
if (isset($columnDef['nullable']) && $columnDef['nullable']) {
|
||||
$columnCode .= '->nullable()';
|
||||
}
|
||||
|
||||
if (isset($columnDef['default'])) {
|
||||
$default = $this->formatDefaultValue($columnDef['default'], $type);
|
||||
$columnCode .= "->default({$default})";
|
||||
}
|
||||
|
||||
if (isset($columnDef['unsigned']) && $columnDef['unsigned']) {
|
||||
$columnCode .= '->unsigned()';
|
||||
}
|
||||
|
||||
$columnCode .= ';';
|
||||
$code[] = $columnCode;
|
||||
}
|
||||
|
||||
// Modify columns
|
||||
foreach ($difference->modifiedColumns as $columnName => $columnDiff) {
|
||||
$targetDef = $columnDiff['target'];
|
||||
$type = $targetDef['type'] ?? 'string';
|
||||
$columnCode = " \$table->modify{$this->mapColumnType($type)}('{$columnName}')";
|
||||
|
||||
// Add modifiers
|
||||
if (isset($targetDef['length']) && $targetDef['length'] > 0) {
|
||||
$columnCode .= "->length({$targetDef['length']})";
|
||||
}
|
||||
|
||||
if (isset($targetDef['nullable']) && $targetDef['nullable']) {
|
||||
$columnCode .= '->nullable()';
|
||||
} elseif (isset($targetDef['nullable']) && ! $targetDef['nullable']) {
|
||||
$columnCode .= '->notNullable()';
|
||||
}
|
||||
|
||||
if (isset($targetDef['default'])) {
|
||||
$default = $this->formatDefaultValue($targetDef['default'], $type);
|
||||
$columnCode .= "->default({$default})";
|
||||
}
|
||||
|
||||
if (isset($targetDef['unsigned']) && $targetDef['unsigned']) {
|
||||
$columnCode .= '->unsigned()';
|
||||
}
|
||||
|
||||
$columnCode .= ';';
|
||||
$code[] = $columnCode;
|
||||
}
|
||||
|
||||
// Drop extra columns
|
||||
foreach ($difference->extraColumns as $columnName => $columnDef) {
|
||||
$code[] = " \$table->dropColumn('{$columnName}');";
|
||||
}
|
||||
|
||||
// Add missing indexes
|
||||
foreach ($difference->missingIndexes as $indexName => $indexDef) {
|
||||
$columns = implode("', '", $indexDef['columns'] ?? []);
|
||||
|
||||
if (isset($indexDef['type']) && $indexDef['type'] === 'PRIMARY') {
|
||||
$code[] = " \$table->primary(['{$columns}']);";
|
||||
} elseif (isset($indexDef['unique']) && $indexDef['unique']) {
|
||||
$code[] = " \$table->unique(['{$columns}'], '{$indexName}');";
|
||||
} else {
|
||||
$code[] = " \$table->index(['{$columns}'], '{$indexName}');";
|
||||
}
|
||||
}
|
||||
|
||||
// Drop extra indexes
|
||||
foreach ($difference->extraIndexes as $indexName => $indexDef) {
|
||||
$code[] = " \$table->dropIndex('{$indexName}');";
|
||||
}
|
||||
|
||||
// Add missing foreign keys
|
||||
foreach ($difference->missingForeignKeys as $fkName => $fkDef) {
|
||||
$columns = implode("', '", $fkDef['columns'] ?? []);
|
||||
$refTable = $fkDef['referenced_table'] ?? '';
|
||||
$refColumns = implode("', '", $fkDef['referenced_columns'] ?? []);
|
||||
|
||||
$fkCode = " \$table->foreignKey(['{$columns}'], '{$refTable}', ['{$refColumns}'], '{$fkName}')";
|
||||
|
||||
if (isset($fkDef['update_rule'])) {
|
||||
$fkCode .= "->onUpdate('{$fkDef['update_rule']}')";
|
||||
}
|
||||
|
||||
if (isset($fkDef['delete_rule'])) {
|
||||
$fkCode .= "->onDelete('{$fkDef['delete_rule']}')";
|
||||
}
|
||||
|
||||
$fkCode .= ';';
|
||||
$code[] = $fkCode;
|
||||
}
|
||||
|
||||
// Drop extra foreign keys
|
||||
foreach ($difference->extraForeignKeys as $fkName => $fkDef) {
|
||||
$code[] = " \$table->dropForeignKey('{$fkName}');";
|
||||
}
|
||||
|
||||
$code[] = ' });';
|
||||
|
||||
return implode("\n", $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code to reverse table modifications
|
||||
*/
|
||||
private function generateReverseTableModificationCode(TableDifference $difference): string
|
||||
{
|
||||
if (! $difference->hasDifferences()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$code = [];
|
||||
$code[] = " \$schema->table('{$difference->tableName}', function (\$table) {";
|
||||
|
||||
// Drop missing columns (they were added in up)
|
||||
foreach ($difference->missingColumns as $columnName => $columnDef) {
|
||||
$code[] = " \$table->dropColumn('{$columnName}');";
|
||||
}
|
||||
|
||||
// Restore modified columns to original state
|
||||
foreach ($difference->modifiedColumns as $columnName => $columnDiff) {
|
||||
$sourceDef = $columnDiff['source'];
|
||||
$type = $sourceDef['type'] ?? 'string';
|
||||
$columnCode = " \$table->modify{$this->mapColumnType($type)}('{$columnName}')";
|
||||
|
||||
// Add modifiers
|
||||
if (isset($sourceDef['length']) && $sourceDef['length'] > 0) {
|
||||
$columnCode .= "->length({$sourceDef['length']})";
|
||||
}
|
||||
|
||||
if (isset($sourceDef['nullable']) && $sourceDef['nullable']) {
|
||||
$columnCode .= '->nullable()';
|
||||
} elseif (isset($sourceDef['nullable']) && ! $sourceDef['nullable']) {
|
||||
$columnCode .= '->notNullable()';
|
||||
}
|
||||
|
||||
if (isset($sourceDef['default'])) {
|
||||
$default = $this->formatDefaultValue($sourceDef['default'], $type);
|
||||
$columnCode .= "->default({$default})";
|
||||
}
|
||||
|
||||
if (isset($sourceDef['unsigned']) && $sourceDef['unsigned']) {
|
||||
$columnCode .= '->unsigned()';
|
||||
}
|
||||
|
||||
$columnCode .= ';';
|
||||
$code[] = $columnCode;
|
||||
}
|
||||
|
||||
// Add extra columns (they were dropped in up)
|
||||
foreach ($difference->extraColumns as $columnName => $columnDef) {
|
||||
$type = $columnDef['type'] ?? 'string';
|
||||
$columnCode = " \$table->add{$this->mapColumnType($type)}('{$columnName}')";
|
||||
|
||||
// Add modifiers
|
||||
if (isset($columnDef['length']) && $columnDef['length'] > 0) {
|
||||
$columnCode .= "->length({$columnDef['length']})";
|
||||
}
|
||||
|
||||
if (isset($columnDef['nullable']) && $columnDef['nullable']) {
|
||||
$columnCode .= '->nullable()';
|
||||
}
|
||||
|
||||
if (isset($columnDef['default'])) {
|
||||
$default = $this->formatDefaultValue($columnDef['default'], $type);
|
||||
$columnCode .= "->default({$default})";
|
||||
}
|
||||
|
||||
if (isset($columnDef['unsigned']) && $columnDef['unsigned']) {
|
||||
$columnCode .= '->unsigned()';
|
||||
}
|
||||
|
||||
$columnCode .= ';';
|
||||
$code[] = $columnCode;
|
||||
}
|
||||
|
||||
// Drop missing indexes (they were added in up)
|
||||
foreach ($difference->missingIndexes as $indexName => $indexDef) {
|
||||
$code[] = " \$table->dropIndex('{$indexName}');";
|
||||
}
|
||||
|
||||
// Add extra indexes (they were dropped in up)
|
||||
foreach ($difference->extraIndexes as $indexName => $indexDef) {
|
||||
$columns = implode("', '", $indexDef['columns'] ?? []);
|
||||
|
||||
if (isset($indexDef['type']) && $indexDef['type'] === 'PRIMARY') {
|
||||
$code[] = " \$table->primary(['{$columns}']);";
|
||||
} elseif (isset($indexDef['unique']) && $indexDef['unique']) {
|
||||
$code[] = " \$table->unique(['{$columns}'], '{$indexName}');";
|
||||
} else {
|
||||
$code[] = " \$table->index(['{$columns}'], '{$indexName}');";
|
||||
}
|
||||
}
|
||||
|
||||
// Drop missing foreign keys (they were added in up)
|
||||
foreach ($difference->missingForeignKeys as $fkName => $fkDef) {
|
||||
$code[] = " \$table->dropForeignKey('{$fkName}');";
|
||||
}
|
||||
|
||||
// Add extra foreign keys (they were dropped in up)
|
||||
foreach ($difference->extraForeignKeys as $fkName => $fkDef) {
|
||||
$columns = implode("', '", $fkDef['columns'] ?? []);
|
||||
$refTable = $fkDef['referenced_table'] ?? '';
|
||||
$refColumns = implode("', '", $fkDef['referenced_columns'] ?? []);
|
||||
|
||||
$fkCode = " \$table->foreignKey(['{$columns}'], '{$refTable}', ['{$refColumns}'], '{$fkName}')";
|
||||
|
||||
if (isset($fkDef['update_rule'])) {
|
||||
$fkCode .= "->onUpdate('{$fkDef['update_rule']}')";
|
||||
}
|
||||
|
||||
if (isset($fkDef['delete_rule'])) {
|
||||
$fkCode .= "->onDelete('{$fkDef['delete_rule']}')";
|
||||
}
|
||||
|
||||
$fkCode .= ';';
|
||||
$code[] = $fkCode;
|
||||
}
|
||||
|
||||
$code[] = ' });';
|
||||
|
||||
return implode("\n", $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the migration class
|
||||
*/
|
||||
private function generateMigrationClass(
|
||||
string $className,
|
||||
string $description,
|
||||
MigrationVersion $version,
|
||||
string $upCode,
|
||||
string $downCode
|
||||
): string {
|
||||
$template = <<<PHP
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Database\Migration;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\AbstractMigration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
use App\Framework\Database\Schema\Schema;
|
||||
|
||||
final class {$className} extends AbstractMigration
|
||||
{
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return MigrationVersion::fromString('{$version->toString()}');
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '{$description}';
|
||||
}
|
||||
|
||||
public function up(ConnectionInterface \$connection): void
|
||||
{
|
||||
\$schema = new Schema(\$connection);
|
||||
|
||||
{$upCode}
|
||||
|
||||
\$schema->execute();
|
||||
}
|
||||
|
||||
public function down(ConnectionInterface \$connection): void
|
||||
{
|
||||
\$schema = new Schema(\$connection);
|
||||
|
||||
{$downCode}
|
||||
|
||||
\$schema->execute();
|
||||
}
|
||||
}
|
||||
PHP;
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map database column type to schema builder method
|
||||
*/
|
||||
private function mapColumnType(string $type): string
|
||||
{
|
||||
return match (strtolower($type)) {
|
||||
'int', 'integer' => 'integer',
|
||||
'bigint' => 'bigInteger',
|
||||
'smallint' => 'smallInteger',
|
||||
'tinyint' => 'tinyInteger',
|
||||
'float', 'double', 'real' => 'float',
|
||||
'decimal', 'numeric' => 'decimal',
|
||||
'boolean', 'bool' => 'boolean',
|
||||
'date' => 'date',
|
||||
'datetime' => 'dateTime',
|
||||
'timestamp' => 'timestamp',
|
||||
'time' => 'time',
|
||||
'char' => 'char',
|
||||
'varchar', 'string' => 'string',
|
||||
'text' => 'text',
|
||||
'mediumtext' => 'mediumText',
|
||||
'longtext' => 'longText',
|
||||
'binary' => 'binary',
|
||||
'blob' => 'binary',
|
||||
'json' => 'json',
|
||||
'jsonb' => 'jsonb',
|
||||
'uuid' => 'uuid',
|
||||
'enum' => 'enum',
|
||||
default => 'string',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format default value for use in migration code
|
||||
*/
|
||||
private function formatDefaultValue(mixed $value, string $type): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
return match (strtolower($type)) {
|
||||
'int', 'integer', 'bigint', 'smallint', 'tinyint', 'float', 'double', 'real', 'decimal', 'numeric' => (string) $value,
|
||||
'boolean', 'bool' => $value ? 'true' : 'false',
|
||||
default => "'{$value}'",
|
||||
};
|
||||
}
|
||||
}
|
||||
788
src/Framework/Database/Schema/Comparison/SchemaComparator.php
Normal file
788
src/Framework/Database/Schema/Comparison/SchemaComparator.php
Normal file
@@ -0,0 +1,788 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Schema\Comparison;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Exception\DatabaseException;
|
||||
|
||||
/**
|
||||
* Compares database schemas and identifies differences
|
||||
*/
|
||||
final readonly class SchemaComparator
|
||||
{
|
||||
/**
|
||||
* Create a new schema comparator
|
||||
*/
|
||||
public function __construct(
|
||||
private ConnectionInterface $sourceConnection,
|
||||
private ConnectionInterface $targetConnection
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the schemas of the source and target databases
|
||||
*
|
||||
* @param string|null $sourceSchema The schema name in the source database (null for default)
|
||||
* @param string|null $targetSchema The schema name in the target database (null for default)
|
||||
* @return SchemaDifference The differences between the schemas
|
||||
*/
|
||||
public function compare(?string $sourceSchema = null, ?string $targetSchema = null): SchemaDifference
|
||||
{
|
||||
$sourceDriver = $this->getDriverName($this->sourceConnection);
|
||||
$targetDriver = $this->getDriverName($this->targetConnection);
|
||||
|
||||
if ($sourceDriver !== $targetDriver) {
|
||||
throw new DatabaseException(
|
||||
"Cannot compare schemas between different database drivers: {$sourceDriver} and {$targetDriver}"
|
||||
);
|
||||
}
|
||||
|
||||
return match($sourceDriver) {
|
||||
'mysql' => $this->compareMySql($sourceSchema, $targetSchema),
|
||||
'pgsql' => $this->comparePostgreSql($sourceSchema, $targetSchema),
|
||||
'sqlite' => $this->compareSqlite(),
|
||||
default => throw new DatabaseException("Schema comparison not supported for {$sourceDriver}")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare MySQL schemas
|
||||
*/
|
||||
private function compareMySql(?string $sourceSchema = null, ?string $targetSchema = null): SchemaDifference
|
||||
{
|
||||
// Use the connection's database if schema is not specified
|
||||
$sourceSchema = $sourceSchema ?? $this->getDefaultSchema($this->sourceConnection);
|
||||
$targetSchema = $targetSchema ?? $this->getDefaultSchema($this->targetConnection);
|
||||
|
||||
// Compare tables
|
||||
$sourceTables = $this->getMySqlTables($this->sourceConnection, $sourceSchema);
|
||||
$targetTables = $this->getMySqlTables($this->targetConnection, $targetSchema);
|
||||
|
||||
$missingTables = array_diff($sourceTables, $targetTables);
|
||||
$extraTables = array_diff($targetTables, $sourceTables);
|
||||
$commonTables = array_intersect($sourceTables, $targetTables);
|
||||
|
||||
// Compare table structures for common tables
|
||||
$tableDifferences = [];
|
||||
foreach ($commonTables as $table) {
|
||||
$sourceColumns = $this->getMySqlColumns($this->sourceConnection, $sourceSchema, $table);
|
||||
$targetColumns = $this->getMySqlColumns($this->targetConnection, $targetSchema, $table);
|
||||
|
||||
$missingColumns = array_diff_key($sourceColumns, $targetColumns);
|
||||
$extraColumns = array_diff_key($targetColumns, $sourceColumns);
|
||||
|
||||
// Check for column differences
|
||||
$modifiedColumns = [];
|
||||
foreach (array_intersect_key($sourceColumns, $targetColumns) as $columnName => $sourceColumn) {
|
||||
$targetColumn = $targetColumns[$columnName];
|
||||
|
||||
if ($sourceColumn !== $targetColumn) {
|
||||
$modifiedColumns[$columnName] = [
|
||||
'source' => $sourceColumn,
|
||||
'target' => $targetColumn,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Compare indexes
|
||||
$sourceIndexes = $this->getMySqlIndexes($this->sourceConnection, $sourceSchema, $table);
|
||||
$targetIndexes = $this->getMySqlIndexes($this->targetConnection, $targetSchema, $table);
|
||||
|
||||
$missingIndexes = array_diff_key($sourceIndexes, $targetIndexes);
|
||||
$extraIndexes = array_diff_key($targetIndexes, $sourceIndexes);
|
||||
|
||||
// Check for index differences
|
||||
$modifiedIndexes = [];
|
||||
foreach (array_intersect_key($sourceIndexes, $targetIndexes) as $indexName => $sourceIndex) {
|
||||
$targetIndex = $targetIndexes[$indexName];
|
||||
|
||||
if ($sourceIndex !== $targetIndex) {
|
||||
$modifiedIndexes[$indexName] = [
|
||||
'source' => $sourceIndex,
|
||||
'target' => $targetIndex,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Compare foreign keys
|
||||
$sourceForeignKeys = $this->getMySqlForeignKeys($this->sourceConnection, $sourceSchema, $table);
|
||||
$targetForeignKeys = $this->getMySqlForeignKeys($this->targetConnection, $targetSchema, $table);
|
||||
|
||||
$missingForeignKeys = array_diff_key($sourceForeignKeys, $targetForeignKeys);
|
||||
$extraForeignKeys = array_diff_key($targetForeignKeys, $sourceForeignKeys);
|
||||
|
||||
// Check for foreign key differences
|
||||
$modifiedForeignKeys = [];
|
||||
foreach (array_intersect_key($sourceForeignKeys, $targetForeignKeys) as $fkName => $sourceFk) {
|
||||
$targetFk = $targetForeignKeys[$fkName];
|
||||
|
||||
if ($sourceFk !== $targetFk) {
|
||||
$modifiedForeignKeys[$fkName] = [
|
||||
'source' => $sourceFk,
|
||||
'target' => $targetFk,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Add table differences if any
|
||||
if (! empty($missingColumns) || ! empty($extraColumns) || ! empty($modifiedColumns) ||
|
||||
! empty($missingIndexes) || ! empty($extraIndexes) || ! empty($modifiedIndexes) ||
|
||||
! empty($missingForeignKeys) || ! empty($extraForeignKeys) || ! empty($modifiedForeignKeys)) {
|
||||
|
||||
$tableDifferences[$table] = new TableDifference(
|
||||
$table,
|
||||
$missingColumns,
|
||||
$extraColumns,
|
||||
$modifiedColumns,
|
||||
$missingIndexes,
|
||||
$extraIndexes,
|
||||
$modifiedIndexes,
|
||||
$missingForeignKeys,
|
||||
$extraForeignKeys,
|
||||
$modifiedForeignKeys
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new SchemaDifference(
|
||||
$sourceSchema,
|
||||
$targetSchema,
|
||||
$missingTables,
|
||||
$extraTables,
|
||||
$tableDifferences
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare PostgreSQL schemas
|
||||
*/
|
||||
private function comparePostgreSql(?string $sourceSchema = null, ?string $targetSchema = null): SchemaDifference
|
||||
{
|
||||
// Use 'public' as the default schema if not specified
|
||||
$sourceSchema = $sourceSchema ?? 'public';
|
||||
$targetSchema = $targetSchema ?? 'public';
|
||||
|
||||
// Compare tables
|
||||
$sourceTables = $this->getPostgreSqlTables($this->sourceConnection, $sourceSchema);
|
||||
$targetTables = $this->getPostgreSqlTables($this->targetConnection, $targetSchema);
|
||||
|
||||
$missingTables = array_diff($sourceTables, $targetTables);
|
||||
$extraTables = array_diff($targetTables, $sourceTables);
|
||||
$commonTables = array_intersect($sourceTables, $targetTables);
|
||||
|
||||
// Compare table structures for common tables
|
||||
$tableDifferences = [];
|
||||
foreach ($commonTables as $table) {
|
||||
$sourceColumns = $this->getPostgreSqlColumns($this->sourceConnection, $sourceSchema, $table);
|
||||
$targetColumns = $this->getPostgreSqlColumns($this->targetConnection, $targetSchema, $table);
|
||||
|
||||
$missingColumns = array_diff_key($sourceColumns, $targetColumns);
|
||||
$extraColumns = array_diff_key($targetColumns, $sourceColumns);
|
||||
|
||||
// Check for column differences
|
||||
$modifiedColumns = [];
|
||||
foreach (array_intersect_key($sourceColumns, $targetColumns) as $columnName => $sourceColumn) {
|
||||
$targetColumn = $targetColumns[$columnName];
|
||||
|
||||
if ($sourceColumn !== $targetColumn) {
|
||||
$modifiedColumns[$columnName] = [
|
||||
'source' => $sourceColumn,
|
||||
'target' => $targetColumn,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Compare indexes
|
||||
$sourceIndexes = $this->getPostgreSqlIndexes($this->sourceConnection, $sourceSchema, $table);
|
||||
$targetIndexes = $this->getPostgreSqlIndexes($this->targetConnection, $targetSchema, $table);
|
||||
|
||||
$missingIndexes = array_diff_key($sourceIndexes, $targetIndexes);
|
||||
$extraIndexes = array_diff_key($targetIndexes, $sourceIndexes);
|
||||
|
||||
// Check for index differences
|
||||
$modifiedIndexes = [];
|
||||
foreach (array_intersect_key($sourceIndexes, $targetIndexes) as $indexName => $sourceIndex) {
|
||||
$targetIndex = $targetIndexes[$indexName];
|
||||
|
||||
if ($sourceIndex !== $targetIndex) {
|
||||
$modifiedIndexes[$indexName] = [
|
||||
'source' => $sourceIndex,
|
||||
'target' => $targetIndex,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Compare foreign keys
|
||||
$sourceForeignKeys = $this->getPostgreSqlForeignKeys($this->sourceConnection, $sourceSchema, $table);
|
||||
$targetForeignKeys = $this->getPostgreSqlForeignKeys($this->targetConnection, $targetSchema, $table);
|
||||
|
||||
$missingForeignKeys = array_diff_key($sourceForeignKeys, $targetForeignKeys);
|
||||
$extraForeignKeys = array_diff_key($targetForeignKeys, $sourceForeignKeys);
|
||||
|
||||
// Check for foreign key differences
|
||||
$modifiedForeignKeys = [];
|
||||
foreach (array_intersect_key($sourceForeignKeys, $targetForeignKeys) as $fkName => $sourceFk) {
|
||||
$targetFk = $targetForeignKeys[$fkName];
|
||||
|
||||
if ($sourceFk !== $targetFk) {
|
||||
$modifiedForeignKeys[$fkName] = [
|
||||
'source' => $sourceFk,
|
||||
'target' => $targetFk,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Add table differences if any
|
||||
if (! empty($missingColumns) || ! empty($extraColumns) || ! empty($modifiedColumns) ||
|
||||
! empty($missingIndexes) || ! empty($extraIndexes) || ! empty($modifiedIndexes) ||
|
||||
! empty($missingForeignKeys) || ! empty($extraForeignKeys) || ! empty($modifiedForeignKeys)) {
|
||||
|
||||
$tableDifferences[$table] = new TableDifference(
|
||||
$table,
|
||||
$missingColumns,
|
||||
$extraColumns,
|
||||
$modifiedColumns,
|
||||
$missingIndexes,
|
||||
$extraIndexes,
|
||||
$modifiedIndexes,
|
||||
$missingForeignKeys,
|
||||
$extraForeignKeys,
|
||||
$modifiedForeignKeys
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new SchemaDifference(
|
||||
$sourceSchema,
|
||||
$targetSchema,
|
||||
$missingTables,
|
||||
$extraTables,
|
||||
$tableDifferences
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare SQLite schemas
|
||||
*/
|
||||
private function compareSqlite(): SchemaDifference
|
||||
{
|
||||
// SQLite doesn't have schemas, so we compare the entire database
|
||||
|
||||
// Compare tables
|
||||
$sourceTables = $this->getSqliteTables($this->sourceConnection);
|
||||
$targetTables = $this->getSqliteTables($this->targetConnection);
|
||||
|
||||
$missingTables = array_diff($sourceTables, $targetTables);
|
||||
$extraTables = array_diff($targetTables, $sourceTables);
|
||||
$commonTables = array_intersect($sourceTables, $targetTables);
|
||||
|
||||
// Compare table structures for common tables
|
||||
$tableDifferences = [];
|
||||
foreach ($commonTables as $table) {
|
||||
$sourceColumns = $this->getSqliteColumns($this->sourceConnection, $table);
|
||||
$targetColumns = $this->getSqliteColumns($this->targetConnection, $table);
|
||||
|
||||
$missingColumns = array_diff_key($sourceColumns, $targetColumns);
|
||||
$extraColumns = array_diff_key($targetColumns, $sourceColumns);
|
||||
|
||||
// Check for column differences
|
||||
$modifiedColumns = [];
|
||||
foreach (array_intersect_key($sourceColumns, $targetColumns) as $columnName => $sourceColumn) {
|
||||
$targetColumn = $targetColumns[$columnName];
|
||||
|
||||
if ($sourceColumn !== $targetColumn) {
|
||||
$modifiedColumns[$columnName] = [
|
||||
'source' => $sourceColumn,
|
||||
'target' => $targetColumn,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Compare indexes
|
||||
$sourceIndexes = $this->getSqliteIndexes($this->sourceConnection, $table);
|
||||
$targetIndexes = $this->getSqliteIndexes($this->targetConnection, $table);
|
||||
|
||||
$missingIndexes = array_diff_key($sourceIndexes, $targetIndexes);
|
||||
$extraIndexes = array_diff_key($targetIndexes, $sourceIndexes);
|
||||
|
||||
// Check for index differences
|
||||
$modifiedIndexes = [];
|
||||
foreach (array_intersect_key($sourceIndexes, $targetIndexes) as $indexName => $sourceIndex) {
|
||||
$targetIndex = $targetIndexes[$indexName];
|
||||
|
||||
if ($sourceIndex !== $targetIndex) {
|
||||
$modifiedIndexes[$indexName] = [
|
||||
'source' => $sourceIndex,
|
||||
'target' => $targetIndex,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Compare foreign keys
|
||||
$sourceForeignKeys = $this->getSqliteForeignKeys($this->sourceConnection, $table);
|
||||
$targetForeignKeys = $this->getSqliteForeignKeys($this->targetConnection, $table);
|
||||
|
||||
$missingForeignKeys = array_diff_key($sourceForeignKeys, $targetForeignKeys);
|
||||
$extraForeignKeys = array_diff_key($targetForeignKeys, $sourceForeignKeys);
|
||||
|
||||
// Check for foreign key differences
|
||||
$modifiedForeignKeys = [];
|
||||
foreach (array_intersect_key($sourceForeignKeys, $targetForeignKeys) as $fkName => $sourceFk) {
|
||||
$targetFk = $targetForeignKeys[$fkName];
|
||||
|
||||
if ($sourceFk !== $targetFk) {
|
||||
$modifiedForeignKeys[$fkName] = [
|
||||
'source' => $sourceFk,
|
||||
'target' => $targetFk,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Add table differences if any
|
||||
if (! empty($missingColumns) || ! empty($extraColumns) || ! empty($modifiedColumns) ||
|
||||
! empty($missingIndexes) || ! empty($extraIndexes) || ! empty($modifiedIndexes) ||
|
||||
! empty($missingForeignKeys) || ! empty($extraForeignKeys) || ! empty($modifiedForeignKeys)) {
|
||||
|
||||
$tableDifferences[$table] = new TableDifference(
|
||||
$table,
|
||||
$missingColumns,
|
||||
$extraColumns,
|
||||
$modifiedColumns,
|
||||
$missingIndexes,
|
||||
$extraIndexes,
|
||||
$modifiedIndexes,
|
||||
$missingForeignKeys,
|
||||
$extraForeignKeys,
|
||||
$modifiedForeignKeys
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new SchemaDifference(
|
||||
'main',
|
||||
'main',
|
||||
$missingTables,
|
||||
$extraTables,
|
||||
$tableDifferences
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default schema for a connection
|
||||
*/
|
||||
private function getDefaultSchema(ConnectionInterface $connection): string
|
||||
{
|
||||
$driver = $this->getDriverName($connection);
|
||||
|
||||
return match($driver) {
|
||||
'mysql' => $connection->queryScalar('SELECT DATABASE()'),
|
||||
'pgsql' => 'public',
|
||||
'sqlite' => 'main',
|
||||
default => throw new DatabaseException("Unsupported driver: {$driver}")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the driver name for a connection
|
||||
*/
|
||||
private function getDriverName(ConnectionInterface $connection): string
|
||||
{
|
||||
return $connection->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tables in a MySQL schema
|
||||
*
|
||||
* @return array<string> List of table names
|
||||
*/
|
||||
private function getMySqlTables(ConnectionInterface $connection, string $schema): array
|
||||
{
|
||||
$sql = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'";
|
||||
|
||||
return $connection->queryColumn($sql, [$schema]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns in a MySQL table
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Map of column names to column definitions
|
||||
*/
|
||||
private function getMySqlColumns(ConnectionInterface $connection, string $schema, string $table): array
|
||||
{
|
||||
$sql = "SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, CHARACTER_SET_NAME, COLLATION_NAME
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
||||
ORDER BY ORDINAL_POSITION";
|
||||
|
||||
$columns = [];
|
||||
$result = $connection->query($sql, [$schema, $table]);
|
||||
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$columns[$row['COLUMN_NAME']] = [
|
||||
'type' => $row['COLUMN_TYPE'],
|
||||
'nullable' => $row['IS_NULLABLE'] === 'YES',
|
||||
'default' => $row['COLUMN_DEFAULT'],
|
||||
'extra' => $row['EXTRA'],
|
||||
'character_set' => $row['CHARACTER_SET_NAME'],
|
||||
'collation' => $row['COLLATION_NAME'],
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get indexes in a MySQL table
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Map of index names to index definitions
|
||||
*/
|
||||
private function getMySqlIndexes(ConnectionInterface $connection, string $schema, string $table): array
|
||||
{
|
||||
$sql = "SELECT INDEX_NAME, NON_UNIQUE, COLUMN_NAME, SEQ_IN_INDEX, INDEX_TYPE
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
||||
ORDER BY INDEX_NAME, SEQ_IN_INDEX";
|
||||
|
||||
$result = $connection->query($sql, [$schema, $table]);
|
||||
$indexes = [];
|
||||
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$indexName = $row['INDEX_NAME'];
|
||||
|
||||
if (! isset($indexes[$indexName])) {
|
||||
$indexes[$indexName] = [
|
||||
'unique' => ! $row['NON_UNIQUE'],
|
||||
'type' => $row['INDEX_TYPE'],
|
||||
'columns' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$indexes[$indexName]['columns'][$row['SEQ_IN_INDEX']] = $row['COLUMN_NAME'];
|
||||
}
|
||||
|
||||
// Sort columns by sequence
|
||||
foreach ($indexes as &$index) {
|
||||
ksort($index['columns']);
|
||||
$index['columns'] = array_values($index['columns']);
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get foreign keys in a MySQL table
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Map of foreign key names to foreign key definitions
|
||||
*/
|
||||
private function getMySqlForeignKeys(ConnectionInterface $connection, string $schema, string $table): array
|
||||
{
|
||||
$sql = "SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME,
|
||||
UPDATE_RULE, DELETE_RULE
|
||||
FROM information_schema.KEY_COLUMN_USAGE k
|
||||
JOIN information_schema.REFERENTIAL_CONSTRAINTS r
|
||||
ON k.CONSTRAINT_NAME = r.CONSTRAINT_NAME
|
||||
AND k.CONSTRAINT_SCHEMA = r.CONSTRAINT_SCHEMA
|
||||
WHERE k.TABLE_SCHEMA = ? AND k.TABLE_NAME = ?
|
||||
AND k.REFERENCED_TABLE_NAME IS NOT NULL
|
||||
ORDER BY CONSTRAINT_NAME, ORDINAL_POSITION";
|
||||
|
||||
$result = $connection->query($sql, [$schema, $table]);
|
||||
$foreignKeys = [];
|
||||
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$constraintName = $row['CONSTRAINT_NAME'];
|
||||
|
||||
if (! isset($foreignKeys[$constraintName])) {
|
||||
$foreignKeys[$constraintName] = [
|
||||
'referenced_table' => $row['REFERENCED_TABLE_NAME'],
|
||||
'update_rule' => $row['UPDATE_RULE'],
|
||||
'delete_rule' => $row['DELETE_RULE'],
|
||||
'columns' => [],
|
||||
'referenced_columns' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$foreignKeys[$constraintName]['columns'][] = $row['COLUMN_NAME'];
|
||||
$foreignKeys[$constraintName]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME'];
|
||||
}
|
||||
|
||||
return $foreignKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tables in a PostgreSQL schema
|
||||
*
|
||||
* @return array<string> List of table names
|
||||
*/
|
||||
private function getPostgreSqlTables(ConnectionInterface $connection, string $schema): array
|
||||
{
|
||||
$sql = "SELECT tablename FROM pg_tables WHERE schemaname = ?";
|
||||
|
||||
return $connection->queryColumn($sql, [$schema]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns in a PostgreSQL table
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Map of column names to column definitions
|
||||
*/
|
||||
private function getPostgreSqlColumns(ConnectionInterface $connection, string $schema, string $table): array
|
||||
{
|
||||
$sql = "SELECT a.attname as column_name,
|
||||
pg_catalog.format_type(a.atttypid, a.atttypmod) as data_type,
|
||||
a.attnotnull as not_null,
|
||||
pg_get_expr(d.adbin, d.adrelid) as default_value,
|
||||
col_description(a.attrelid, a.attnum) as comment
|
||||
FROM pg_catalog.pg_attribute a
|
||||
LEFT JOIN pg_catalog.pg_attrdef d ON (a.attrelid, a.attnum) = (d.adrelid, d.adnum)
|
||||
JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
|
||||
JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
|
||||
WHERE c.relname = ?
|
||||
AND n.nspname = ?
|
||||
AND a.attnum > 0
|
||||
AND NOT a.attisdropped
|
||||
ORDER BY a.attnum";
|
||||
|
||||
$columns = [];
|
||||
$result = $connection->query($sql, [$table, $schema]);
|
||||
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$columns[$row['column_name']] = [
|
||||
'type' => $row['data_type'],
|
||||
'nullable' => ! $row['not_null'],
|
||||
'default' => $row['default_value'],
|
||||
'comment' => $row['comment'],
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get indexes in a PostgreSQL table
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Map of index names to index definitions
|
||||
*/
|
||||
private function getPostgreSqlIndexes(ConnectionInterface $connection, string $schema, string $table): array
|
||||
{
|
||||
$sql = "SELECT i.relname as index_name,
|
||||
a.attname as column_name,
|
||||
ix.indisunique as is_unique,
|
||||
ix.indisprimary as is_primary,
|
||||
am.amname as index_type,
|
||||
a.attnum as column_position
|
||||
FROM pg_index ix
|
||||
JOIN pg_class i ON i.oid = ix.indexrelid
|
||||
JOIN pg_class t ON t.oid = ix.indrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
||||
JOIN pg_am am ON i.relam = am.oid
|
||||
WHERE t.relname = ?
|
||||
AND n.nspname = ?
|
||||
ORDER BY i.relname, a.attnum";
|
||||
|
||||
$result = $connection->query($sql, [$table, $schema]);
|
||||
$indexes = [];
|
||||
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$indexName = $row['index_name'];
|
||||
|
||||
if (! isset($indexes[$indexName])) {
|
||||
$indexes[$indexName] = [
|
||||
'unique' => (bool)$row['is_unique'],
|
||||
'primary' => (bool)$row['is_primary'],
|
||||
'type' => $row['index_type'],
|
||||
'columns' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$indexes[$indexName]['columns'][$row['column_position']] = $row['column_name'];
|
||||
}
|
||||
|
||||
// Sort columns by position
|
||||
foreach ($indexes as &$index) {
|
||||
ksort($index['columns']);
|
||||
$index['columns'] = array_values($index['columns']);
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get foreign keys in a PostgreSQL table
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Map of foreign key names to foreign key definitions
|
||||
*/
|
||||
private function getPostgreSqlForeignKeys(ConnectionInterface $connection, string $schema, string $table): array
|
||||
{
|
||||
$sql = "SELECT con.conname as constraint_name,
|
||||
con.confupdtype as update_rule,
|
||||
con.confdeltype as delete_rule,
|
||||
f.relname as referenced_table,
|
||||
a.attname as column_name,
|
||||
af.attname as referenced_column,
|
||||
a.attnum as column_position
|
||||
FROM pg_constraint con
|
||||
JOIN pg_class t ON t.oid = con.conrelid
|
||||
JOIN pg_class f ON f.oid = con.confrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(con.conkey)
|
||||
JOIN pg_attribute af ON af.attrelid = f.oid AND af.attnum = ANY(con.confkey)
|
||||
WHERE con.contype = 'f'
|
||||
AND t.relname = ?
|
||||
AND n.nspname = ?
|
||||
ORDER BY con.conname, a.attnum";
|
||||
|
||||
$result = $connection->query($sql, [$table, $schema]);
|
||||
$foreignKeys = [];
|
||||
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$constraintName = $row['constraint_name'];
|
||||
|
||||
if (! isset($foreignKeys[$constraintName])) {
|
||||
$foreignKeys[$constraintName] = [
|
||||
'referenced_table' => $row['referenced_table'],
|
||||
'update_rule' => $this->convertPostgreSqlRule($row['update_rule']),
|
||||
'delete_rule' => $this->convertPostgreSqlRule($row['delete_rule']),
|
||||
'columns' => [],
|
||||
'referenced_columns' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$foreignKeys[$constraintName]['columns'][$row['column_position']] = $row['column_name'];
|
||||
$foreignKeys[$constraintName]['referenced_columns'][$row['column_position']] = $row['referenced_column'];
|
||||
}
|
||||
|
||||
// Sort columns by position
|
||||
foreach ($foreignKeys as &$fk) {
|
||||
ksort($fk['columns']);
|
||||
ksort($fk['referenced_columns']);
|
||||
$fk['columns'] = array_values($fk['columns']);
|
||||
$fk['referenced_columns'] = array_values($fk['referenced_columns']);
|
||||
}
|
||||
|
||||
return $foreignKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert PostgreSQL rule character to rule name
|
||||
*/
|
||||
private function convertPostgreSqlRule(string $rule): string
|
||||
{
|
||||
return match($rule) {
|
||||
'a' => 'NO ACTION',
|
||||
'r' => 'RESTRICT',
|
||||
'c' => 'CASCADE',
|
||||
'n' => 'SET NULL',
|
||||
'd' => 'SET DEFAULT',
|
||||
default => $rule
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tables in a SQLite database
|
||||
*
|
||||
* @return array<string> List of table names
|
||||
*/
|
||||
private function getSqliteTables(ConnectionInterface $connection): array
|
||||
{
|
||||
$sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'";
|
||||
|
||||
return $connection->queryColumn($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns in a SQLite table
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Map of column names to column definitions
|
||||
*/
|
||||
private function getSqliteColumns(ConnectionInterface $connection, string $table): array
|
||||
{
|
||||
$sql = "PRAGMA table_info({$table})";
|
||||
$result = $connection->query($sql);
|
||||
$columns = [];
|
||||
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$columns[$row['name']] = [
|
||||
'type' => $row['type'],
|
||||
'nullable' => ! $row['notnull'],
|
||||
'default' => $row['dflt_value'],
|
||||
'primary' => (bool)$row['pk'],
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get indexes in a SQLite table
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Map of index names to index definitions
|
||||
*/
|
||||
private function getSqliteIndexes(ConnectionInterface $connection, string $table): array
|
||||
{
|
||||
$sql = "PRAGMA index_list({$table})";
|
||||
$result = $connection->query($sql);
|
||||
$indexes = [];
|
||||
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$indexName = $row['name'];
|
||||
$indexInfo = $connection->query("PRAGMA index_info({$indexName})")->fetchAll();
|
||||
|
||||
$columns = [];
|
||||
foreach ($indexInfo as $column) {
|
||||
$columns[$column['seqno']] = $column['name'];
|
||||
}
|
||||
|
||||
ksort($columns);
|
||||
|
||||
$indexes[$indexName] = [
|
||||
'unique' => (bool)$row['unique'],
|
||||
'columns' => array_values($columns),
|
||||
];
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get foreign keys in a SQLite table
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Map of foreign key names to foreign key definitions
|
||||
*/
|
||||
private function getSqliteForeignKeys(ConnectionInterface $connection, string $table): array
|
||||
{
|
||||
$sql = "PRAGMA foreign_key_list({$table})";
|
||||
$result = $connection->query($sql);
|
||||
$foreignKeys = [];
|
||||
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$id = $row['id'];
|
||||
|
||||
if (! isset($foreignKeys[$id])) {
|
||||
$foreignKeys[$id] = [
|
||||
'referenced_table' => $row['table'],
|
||||
'update_rule' => $row['on_update'],
|
||||
'delete_rule' => $row['on_delete'],
|
||||
'columns' => [],
|
||||
'referenced_columns' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$foreignKeys[$id]['columns'][$row['seq']] = $row['from'];
|
||||
$foreignKeys[$id]['referenced_columns'][$row['seq']] = $row['to'];
|
||||
}
|
||||
|
||||
// Sort columns by sequence
|
||||
foreach ($foreignKeys as &$fk) {
|
||||
ksort($fk['columns']);
|
||||
ksort($fk['referenced_columns']);
|
||||
$fk['columns'] = array_values($fk['columns']);
|
||||
$fk['referenced_columns'] = array_values($fk['referenced_columns']);
|
||||
}
|
||||
|
||||
return $foreignKeys;
|
||||
}
|
||||
}
|
||||
194
src/Framework/Database/Schema/Comparison/SchemaDifference.php
Normal file
194
src/Framework/Database/Schema/Comparison/SchemaDifference.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Schema\Comparison;
|
||||
|
||||
/**
|
||||
* Represents differences between two database schemas
|
||||
*/
|
||||
final readonly class SchemaDifference
|
||||
{
|
||||
/**
|
||||
* Create a new schema difference
|
||||
*
|
||||
* @param string $sourceSchema The source schema name
|
||||
* @param string $targetSchema The target schema name
|
||||
* @param array<string> $missingTables Tables that exist in the source but not in the target
|
||||
* @param array<string> $extraTables Tables that exist in the target but not in the source
|
||||
* @param array<string, TableDifference> $tableDifferences Differences in tables that exist in both schemas
|
||||
*/
|
||||
public function __construct(
|
||||
public string $sourceSchema,
|
||||
public string $targetSchema,
|
||||
public array $missingTables,
|
||||
public array $extraTables,
|
||||
public array $tableDifferences
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any differences between the schemas
|
||||
*/
|
||||
public function hasDifferences(): bool
|
||||
{
|
||||
return ! empty($this->missingTables) || ! empty($this->extraTables) || ! empty($this->tableDifferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary of the differences
|
||||
*
|
||||
* @return array<string, mixed> Summary of the differences
|
||||
*/
|
||||
public function getSummary(): array
|
||||
{
|
||||
$summary = [
|
||||
'source_schema' => $this->sourceSchema,
|
||||
'target_schema' => $this->targetSchema,
|
||||
'missing_tables' => count($this->missingTables),
|
||||
'extra_tables' => count($this->extraTables),
|
||||
'modified_tables' => count($this->tableDifferences),
|
||||
];
|
||||
|
||||
$columnCounts = [
|
||||
'missing_columns' => 0,
|
||||
'extra_columns' => 0,
|
||||
'modified_columns' => 0,
|
||||
];
|
||||
|
||||
$indexCounts = [
|
||||
'missing_indexes' => 0,
|
||||
'extra_indexes' => 0,
|
||||
'modified_indexes' => 0,
|
||||
];
|
||||
|
||||
$foreignKeyCounts = [
|
||||
'missing_foreign_keys' => 0,
|
||||
'extra_foreign_keys' => 0,
|
||||
'modified_foreign_keys' => 0,
|
||||
];
|
||||
|
||||
foreach ($this->tableDifferences as $tableDifference) {
|
||||
$columnCounts['missing_columns'] += count($tableDifference->missingColumns);
|
||||
$columnCounts['extra_columns'] += count($tableDifference->extraColumns);
|
||||
$columnCounts['modified_columns'] += count($tableDifference->modifiedColumns);
|
||||
|
||||
$indexCounts['missing_indexes'] += count($tableDifference->missingIndexes);
|
||||
$indexCounts['extra_indexes'] += count($tableDifference->extraIndexes);
|
||||
$indexCounts['modified_indexes'] += count($tableDifference->modifiedIndexes);
|
||||
|
||||
$foreignKeyCounts['missing_foreign_keys'] += count($tableDifference->missingForeignKeys);
|
||||
$foreignKeyCounts['extra_foreign_keys'] += count($tableDifference->extraForeignKeys);
|
||||
$foreignKeyCounts['modified_foreign_keys'] += count($tableDifference->modifiedForeignKeys);
|
||||
}
|
||||
|
||||
return array_merge($summary, $columnCounts, $indexCounts, $foreignKeyCounts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a detailed description of the differences
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
$description = [];
|
||||
|
||||
$description[] = "Schema Comparison: {$this->sourceSchema} -> {$this->targetSchema}";
|
||||
$description[] = "";
|
||||
|
||||
if (! $this->hasDifferences()) {
|
||||
$description[] = "No differences found.";
|
||||
|
||||
return implode("\n", $description);
|
||||
}
|
||||
|
||||
if (! empty($this->missingTables)) {
|
||||
$description[] = "Missing Tables:";
|
||||
foreach ($this->missingTables as $table) {
|
||||
$description[] = " - {$table}";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->extraTables)) {
|
||||
$description[] = "Extra Tables:";
|
||||
foreach ($this->extraTables as $table) {
|
||||
$description[] = " - {$table}";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->tableDifferences)) {
|
||||
$description[] = "Table Differences:";
|
||||
foreach ($this->tableDifferences as $tableName => $tableDifference) {
|
||||
$description[] = " {$tableName}:";
|
||||
|
||||
if (! empty($tableDifference->missingColumns)) {
|
||||
$description[] = " Missing Columns:";
|
||||
foreach ($tableDifference->missingColumns as $columnName => $columnDef) {
|
||||
$description[] = " - {$columnName}";
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($tableDifference->extraColumns)) {
|
||||
$description[] = " Extra Columns:";
|
||||
foreach ($tableDifference->extraColumns as $columnName => $columnDef) {
|
||||
$description[] = " - {$columnName}";
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($tableDifference->modifiedColumns)) {
|
||||
$description[] = " Modified Columns:";
|
||||
foreach ($tableDifference->modifiedColumns as $columnName => $columnDiff) {
|
||||
$description[] = " - {$columnName}";
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($tableDifference->missingIndexes)) {
|
||||
$description[] = " Missing Indexes:";
|
||||
foreach ($tableDifference->missingIndexes as $indexName => $indexDef) {
|
||||
$description[] = " - {$indexName}";
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($tableDifference->extraIndexes)) {
|
||||
$description[] = " Extra Indexes:";
|
||||
foreach ($tableDifference->extraIndexes as $indexName => $indexDef) {
|
||||
$description[] = " - {$indexName}";
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($tableDifference->modifiedIndexes)) {
|
||||
$description[] = " Modified Indexes:";
|
||||
foreach ($tableDifference->modifiedIndexes as $indexName => $indexDiff) {
|
||||
$description[] = " - {$indexName}";
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($tableDifference->missingForeignKeys)) {
|
||||
$description[] = " Missing Foreign Keys:";
|
||||
foreach ($tableDifference->missingForeignKeys as $fkName => $fkDef) {
|
||||
$description[] = " - {$fkName}";
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($tableDifference->extraForeignKeys)) {
|
||||
$description[] = " Extra Foreign Keys:";
|
||||
foreach ($tableDifference->extraForeignKeys as $fkName => $fkDef) {
|
||||
$description[] = " - {$fkName}";
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($tableDifference->modifiedForeignKeys)) {
|
||||
$description[] = " Modified Foreign Keys:";
|
||||
foreach ($tableDifference->modifiedForeignKeys as $fkName => $fkDiff) {
|
||||
$description[] = " - {$fkName}";
|
||||
}
|
||||
}
|
||||
|
||||
$description[] = "";
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $description);
|
||||
}
|
||||
}
|
||||
220
src/Framework/Database/Schema/Comparison/TableDifference.php
Normal file
220
src/Framework/Database/Schema/Comparison/TableDifference.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Database\Schema\Comparison;
|
||||
|
||||
/**
|
||||
* Represents differences between two database tables
|
||||
*/
|
||||
final readonly class TableDifference
|
||||
{
|
||||
/**
|
||||
* Create a new table difference
|
||||
*
|
||||
* @param string $tableName The name of the table
|
||||
* @param array<string, array<string, mixed>> $missingColumns Columns that exist in the source but not in the target
|
||||
* @param array<string, array<string, mixed>> $extraColumns Columns that exist in the target but not in the source
|
||||
* @param array<string, array<string, array<string, mixed>>> $modifiedColumns Columns that exist in both but have differences
|
||||
* @param array<string, array<string, mixed>> $missingIndexes Indexes that exist in the source but not in the target
|
||||
* @param array<string, array<string, mixed>> $extraIndexes Indexes that exist in the target but not in the source
|
||||
* @param array<string, array<string, array<string, mixed>>> $modifiedIndexes Indexes that exist in both but have differences
|
||||
* @param array<string, array<string, mixed>> $missingForeignKeys Foreign keys that exist in the source but not in the target
|
||||
* @param array<string, array<string, mixed>> $extraForeignKeys Foreign keys that exist in the target but not in the source
|
||||
* @param array<string, array<string, array<string, mixed>>> $modifiedForeignKeys Foreign keys that exist in both but have differences
|
||||
*/
|
||||
public function __construct(
|
||||
public string $tableName,
|
||||
public array $missingColumns,
|
||||
public array $extraColumns,
|
||||
public array $modifiedColumns,
|
||||
public array $missingIndexes,
|
||||
public array $extraIndexes,
|
||||
public array $modifiedIndexes,
|
||||
public array $missingForeignKeys,
|
||||
public array $extraForeignKeys,
|
||||
public array $modifiedForeignKeys
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any differences in the table
|
||||
*/
|
||||
public function hasDifferences(): bool
|
||||
{
|
||||
return ! empty($this->missingColumns) || ! empty($this->extraColumns) || ! empty($this->modifiedColumns) ||
|
||||
! empty($this->missingIndexes) || ! empty($this->extraIndexes) || ! empty($this->modifiedIndexes) ||
|
||||
! empty($this->missingForeignKeys) || ! empty($this->extraForeignKeys) || ! empty($this->modifiedForeignKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary of the differences
|
||||
*
|
||||
* @return array<string, int> Summary of the differences
|
||||
*/
|
||||
public function getSummary(): array
|
||||
{
|
||||
return [
|
||||
'missing_columns' => count($this->missingColumns),
|
||||
'extra_columns' => count($this->extraColumns),
|
||||
'modified_columns' => count($this->modifiedColumns),
|
||||
'missing_indexes' => count($this->missingIndexes),
|
||||
'extra_indexes' => count($this->extraIndexes),
|
||||
'modified_indexes' => count($this->modifiedIndexes),
|
||||
'missing_foreign_keys' => count($this->missingForeignKeys),
|
||||
'extra_foreign_keys' => count($this->extraForeignKeys),
|
||||
'modified_foreign_keys' => count($this->modifiedForeignKeys),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a detailed description of the differences
|
||||
*/
|
||||
public function getDescription(): string
|
||||
{
|
||||
$description = [];
|
||||
|
||||
$description[] = "Table: {$this->tableName}";
|
||||
$description[] = "";
|
||||
|
||||
if (! $this->hasDifferences()) {
|
||||
$description[] = "No differences found.";
|
||||
|
||||
return implode("\n", $description);
|
||||
}
|
||||
|
||||
if (! empty($this->missingColumns)) {
|
||||
$description[] = "Missing Columns:";
|
||||
foreach ($this->missingColumns as $columnName => $columnDef) {
|
||||
$type = $columnDef['type'] ?? 'unknown';
|
||||
$nullable = $columnDef['nullable'] ? 'NULL' : 'NOT NULL';
|
||||
$default = isset($columnDef['default']) ? "DEFAULT {$columnDef['default']}" : '';
|
||||
$description[] = " - {$columnName} {$type} {$nullable} {$default}";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->extraColumns)) {
|
||||
$description[] = "Extra Columns:";
|
||||
foreach ($this->extraColumns as $columnName => $columnDef) {
|
||||
$type = $columnDef['type'] ?? 'unknown';
|
||||
$nullable = $columnDef['nullable'] ? 'NULL' : 'NOT NULL';
|
||||
$default = isset($columnDef['default']) ? "DEFAULT {$columnDef['default']}" : '';
|
||||
$description[] = " - {$columnName} {$type} {$nullable} {$default}";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->modifiedColumns)) {
|
||||
$description[] = "Modified Columns:";
|
||||
foreach ($this->modifiedColumns as $columnName => $columnDiff) {
|
||||
$sourceType = $columnDiff['source']['type'] ?? 'unknown';
|
||||
$targetType = $columnDiff['target']['type'] ?? 'unknown';
|
||||
$sourceNullable = $columnDiff['source']['nullable'] ? 'NULL' : 'NOT NULL';
|
||||
$targetNullable = $columnDiff['target']['nullable'] ? 'NULL' : 'NOT NULL';
|
||||
$sourceDefault = isset($columnDiff['source']['default']) ? "DEFAULT {$columnDiff['source']['default']}" : '';
|
||||
$targetDefault = isset($columnDiff['target']['default']) ? "DEFAULT {$columnDiff['target']['default']}" : '';
|
||||
|
||||
$description[] = " - {$columnName}:";
|
||||
$description[] = " Source: {$sourceType} {$sourceNullable} {$sourceDefault}";
|
||||
$description[] = " Target: {$targetType} {$targetNullable} {$targetDefault}";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->missingIndexes)) {
|
||||
$description[] = "Missing Indexes:";
|
||||
foreach ($this->missingIndexes as $indexName => $indexDef) {
|
||||
$type = $indexDef['type'] ?? 'INDEX';
|
||||
$unique = $indexDef['unique'] ? 'UNIQUE' : '';
|
||||
$columns = implode(', ', $indexDef['columns'] ?? []);
|
||||
$description[] = " - {$indexName} {$unique} {$type} ({$columns})";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->extraIndexes)) {
|
||||
$description[] = "Extra Indexes:";
|
||||
foreach ($this->extraIndexes as $indexName => $indexDef) {
|
||||
$type = $indexDef['type'] ?? 'INDEX';
|
||||
$unique = $indexDef['unique'] ? 'UNIQUE' : '';
|
||||
$columns = implode(', ', $indexDef['columns'] ?? []);
|
||||
$description[] = " - {$indexName} {$unique} {$type} ({$columns})";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->modifiedIndexes)) {
|
||||
$description[] = "Modified Indexes:";
|
||||
foreach ($this->modifiedIndexes as $indexName => $indexDiff) {
|
||||
$sourceType = $indexDiff['source']['type'] ?? 'INDEX';
|
||||
$targetType = $indexDiff['target']['type'] ?? 'INDEX';
|
||||
$sourceUnique = $indexDiff['source']['unique'] ? 'UNIQUE' : '';
|
||||
$targetUnique = $indexDiff['target']['unique'] ? 'UNIQUE' : '';
|
||||
$sourceColumns = implode(', ', $indexDiff['source']['columns'] ?? []);
|
||||
$targetColumns = implode(', ', $indexDiff['target']['columns'] ?? []);
|
||||
|
||||
$description[] = " - {$indexName}:";
|
||||
$description[] = " Source: {$sourceUnique} {$sourceType} ({$sourceColumns})";
|
||||
$description[] = " Target: {$targetUnique} {$targetType} ({$targetColumns})";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->missingForeignKeys)) {
|
||||
$description[] = "Missing Foreign Keys:";
|
||||
foreach ($this->missingForeignKeys as $fkName => $fkDef) {
|
||||
$columns = implode(', ', $fkDef['columns'] ?? []);
|
||||
$refTable = $fkDef['referenced_table'] ?? '';
|
||||
$refColumns = implode(', ', $fkDef['referenced_columns'] ?? []);
|
||||
$updateRule = $fkDef['update_rule'] ?? 'NO ACTION';
|
||||
$deleteRule = $fkDef['delete_rule'] ?? 'NO ACTION';
|
||||
|
||||
$description[] = " - {$fkName}: ({$columns}) REFERENCES {$refTable} ({$refColumns})";
|
||||
$description[] = " ON UPDATE {$updateRule} ON DELETE {$deleteRule}";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->extraForeignKeys)) {
|
||||
$description[] = "Extra Foreign Keys:";
|
||||
foreach ($this->extraForeignKeys as $fkName => $fkDef) {
|
||||
$columns = implode(', ', $fkDef['columns'] ?? []);
|
||||
$refTable = $fkDef['referenced_table'] ?? '';
|
||||
$refColumns = implode(', ', $fkDef['referenced_columns'] ?? []);
|
||||
$updateRule = $fkDef['update_rule'] ?? 'NO ACTION';
|
||||
$deleteRule = $fkDef['delete_rule'] ?? 'NO ACTION';
|
||||
|
||||
$description[] = " - {$fkName}: ({$columns}) REFERENCES {$refTable} ({$refColumns})";
|
||||
$description[] = " ON UPDATE {$updateRule} ON DELETE {$deleteRule}";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
if (! empty($this->modifiedForeignKeys)) {
|
||||
$description[] = "Modified Foreign Keys:";
|
||||
foreach ($this->modifiedForeignKeys as $fkName => $fkDiff) {
|
||||
$sourceColumns = implode(', ', $fkDiff['source']['columns'] ?? []);
|
||||
$sourceRefTable = $fkDiff['source']['referenced_table'] ?? '';
|
||||
$sourceRefColumns = implode(', ', $fkDiff['source']['referenced_columns'] ?? []);
|
||||
$sourceUpdateRule = $fkDiff['source']['update_rule'] ?? 'NO ACTION';
|
||||
$sourceDeleteRule = $fkDiff['source']['delete_rule'] ?? 'NO ACTION';
|
||||
|
||||
$targetColumns = implode(', ', $fkDiff['target']['columns'] ?? []);
|
||||
$targetRefTable = $fkDiff['target']['referenced_table'] ?? '';
|
||||
$targetRefColumns = implode(', ', $fkDiff['target']['referenced_columns'] ?? []);
|
||||
$targetUpdateRule = $fkDiff['target']['update_rule'] ?? 'NO ACTION';
|
||||
$targetDeleteRule = $fkDiff['target']['delete_rule'] ?? 'NO ACTION';
|
||||
|
||||
$description[] = " - {$fkName}:";
|
||||
$description[] = " Source: ({$sourceColumns}) REFERENCES {$sourceRefTable} ({$sourceRefColumns})";
|
||||
$description[] = " ON UPDATE {$sourceUpdateRule} ON DELETE {$sourceDeleteRule}";
|
||||
$description[] = " Target: ({$targetColumns}) REFERENCES {$targetRefTable} ({$targetRefColumns})";
|
||||
$description[] = " ON UPDATE {$targetUpdateRule} ON DELETE {$targetDeleteRule}";
|
||||
}
|
||||
$description[] = "";
|
||||
}
|
||||
|
||||
return implode("\n", $description);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user