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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View 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}'",
};
}
}

View 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;
}
}

View 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);
}
}

View 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);
}
}