diff --git a/packages/database/src/Command/DiffCommand.php b/packages/database/src/Command/DiffCommand.php index fcbee57f..0996a3f9 100644 --- a/packages/database/src/Command/DiffCommand.php +++ b/packages/database/src/Command/DiffCommand.php @@ -13,10 +13,9 @@ use Marko\Database\Diff\SchemaDiff; use Marko\Database\Diff\TableDiff; use Marko\Database\Entity\EntityDiscovery; -use Marko\Database\Entity\EntityMetadataFactory; -use Marko\Database\Entity\SchemaBuilder; use Marko\Database\Exceptions\EntityException; use Marko\Database\Introspection\IntrospectorInterface; +use Marko\Database\Schema\SchemaRegistry; use Marko\Database\Schema\Table; /** @noinspection PhpUnused */ @@ -26,8 +25,7 @@ public function __construct( private EntityDiscovery $discovery, private IntrospectorInterface $introspector, - private EntityMetadataFactory $metadataFactory, - private SchemaBuilder $schemaBuilder, + private SchemaRegistry $schemaRegistry, private DiffCalculator $diffCalculator, private ProjectPaths $paths, ) {} @@ -77,15 +75,10 @@ public function execute( private function buildEntitySchema( array $entityClasses, ): array { - $schema = []; - - foreach ($entityClasses as $entityClass) { - $metadata = $this->metadataFactory->parse($entityClass); - $table = $this->schemaBuilder->build($metadata); - $schema[$table->name] = $table; - } + $this->schemaRegistry->clear(); + $this->schemaRegistry->registerEntities($entityClasses); - return $schema; + return $this->schemaRegistry->getTables(); } /** diff --git a/packages/database/src/Command/MigrateCommand.php b/packages/database/src/Command/MigrateCommand.php index d5bad656..0e47e678 100644 --- a/packages/database/src/Command/MigrateCommand.php +++ b/packages/database/src/Command/MigrateCommand.php @@ -13,14 +13,13 @@ use Marko\Database\Diff\SchemaDiff; use Marko\Database\Diff\SqlGeneratorInterface; use Marko\Database\Entity\EntityDiscovery; -use Marko\Database\Entity\EntityMetadataFactory; -use Marko\Database\Entity\SchemaBuilder; use Marko\Database\Exceptions\EntityException; use Marko\Database\Exceptions\MigrationException; use Marko\Database\Introspection\IntrospectorInterface; use Marko\Database\Migration\DataMigrator; use Marko\Database\Migration\MigrationGenerator; use Marko\Database\Migration\Migrator; +use Marko\Database\Schema\SchemaRegistry; use Marko\Database\Schema\Table; /** @noinspection PhpUnused */ @@ -33,8 +32,7 @@ public function __construct( private MigrationGenerator $migrationGenerator, private EntityDiscovery $entityDiscovery, private IntrospectorInterface $introspector, - private EntityMetadataFactory $metadataFactory, - private SchemaBuilder $schemaBuilder, + private SchemaRegistry $schemaRegistry, private DiffCalculator $diffCalculator, private SqlGeneratorInterface $sqlGenerator, private ProjectPaths $paths, @@ -251,15 +249,10 @@ private function calculateDiff(): SchemaDiff private function buildEntitySchema( array $entityClasses, ): array { - $schema = []; - - foreach ($entityClasses as $entityClass) { - $metadata = $this->metadataFactory->parse($entityClass); - $table = $this->schemaBuilder->build($metadata); - $schema[$table->name] = $table; - } + $this->schemaRegistry->clear(); + $this->schemaRegistry->registerEntities($entityClasses); - return $schema; + return $this->schemaRegistry->getTables(); } /** diff --git a/packages/database/tests/Command/DiffCommandTest.php b/packages/database/tests/Command/DiffCommandTest.php index a0348fcd..6afc868c 100644 --- a/packages/database/tests/Command/DiffCommandTest.php +++ b/packages/database/tests/Command/DiffCommandTest.php @@ -8,11 +8,16 @@ use Marko\Database\Diff\DiffCalculator; use Marko\Database\Diff\SchemaDiff; use Marko\Database\Diff\TableDiff; +use Marko\Database\Entity\EntityMetadataFactory; +use Marko\Database\Entity\SchemaBuilder; use Marko\Database\Schema\Column; use Marko\Database\Schema\Index; use Marko\Database\Schema\IndexType; +use Marko\Database\Schema\SchemaRegistry; use Marko\Database\Schema\Table; use Marko\Database\Tests\Command\Helpers; +use Marko\Database\Tests\Entity\Fixtures\ExtenderFactory\BasicExtenderEntity; +use Marko\Database\Tests\Entity\Fixtures\ExtenderFactory\ExtenderParentEntity; it('registers as db:diff command via #[Command] attribute', function (): void { $reflection = new ReflectionClass(DiffCommand::class); @@ -349,3 +354,29 @@ public function calculate( expect($exitCode2)->toBe(1); }); + +it('merges extender columns into parent table schema (regression for #66)', function (): void { + // Build the expected merged schema via the real SchemaRegistry so the + // introspector stub mirrors exactly what DiffCommand will compute. If + // DiffCommand bypasses the extender merge, the entity-side schema will + // be missing the extender's `extra` column and DiffCalculator will + // report it as a destructive drop. + $registry = new SchemaRegistry( + new EntityMetadataFactory(), + new SchemaBuilder(), + ); + $registry->registerEntities([ExtenderParentEntity::class, BasicExtenderEntity::class]); + $mergedTables = $registry->getTables(); + + expect($mergedTables['users']->columns)->toHaveCount(2); + + $command = Helpers::createDiffCommand( + tables: $mergedTables, + entities: [ExtenderParentEntity::class, BasicExtenderEntity::class], + ); + + ['output' => $output, 'exitCode' => $exitCode] = Helpers::executeDiffCommand($command); + + expect($output)->toContain('No changes detected') + ->and($exitCode)->toBe(0); +}); diff --git a/packages/database/tests/Command/Helpers.php b/packages/database/tests/Command/Helpers.php index 4e750fe0..c2f1bad1 100644 --- a/packages/database/tests/Command/Helpers.php +++ b/packages/database/tests/Command/Helpers.php @@ -15,6 +15,7 @@ use Marko\Database\Entity\EntityMetadataFactory; use Marko\Database\Entity\SchemaBuilder; use Marko\Database\Introspection\IntrospectorInterface; +use Marko\Database\Schema\SchemaRegistry; use Marko\Database\Schema\Table; /** @@ -221,16 +222,17 @@ public function lastInsertId(): int * Helper to create a DiffCommand with standard dependencies. * * @param array $tables Tables for introspector + * @param array $entities Entity classes for discovery */ public static function createDiffCommand( ?DiffCalculator $diffCalculator = null, array $tables = [], + array $entities = [], ): DiffCommand { return new DiffCommand( - discovery: self::createStubEntityDiscovery(), + discovery: self::createStubEntityDiscovery($entities), introspector: self::createStubIntrospector($tables), - metadataFactory: new EntityMetadataFactory(), - schemaBuilder: new SchemaBuilder(), + schemaRegistry: new SchemaRegistry(new EntityMetadataFactory(), new SchemaBuilder()), diffCalculator: $diffCalculator ?? new DiffCalculator(), paths: new ProjectPaths('/test'), ); diff --git a/packages/database/tests/Command/MigrateCommandTest.php b/packages/database/tests/Command/MigrateCommandTest.php index 34b9c4de..f75e607f 100644 --- a/packages/database/tests/Command/MigrateCommandTest.php +++ b/packages/database/tests/Command/MigrateCommandTest.php @@ -19,8 +19,11 @@ use Marko\Database\Schema\Column; use Marko\Database\Schema\ForeignKey; use Marko\Database\Schema\Index; +use Marko\Database\Schema\SchemaRegistry; use Marko\Database\Schema\Table; use Marko\Database\Tests\Command\Helpers; +use Marko\Database\Tests\Entity\Fixtures\ExtenderFactory\BasicExtenderEntity; +use Marko\Database\Tests\Entity\Fixtures\ExtenderFactory\ExtenderParentEntity; /** * Create a stub Migrator for testing. @@ -304,8 +307,7 @@ function createMigrateCommand( migrationGenerator: $generator ?? createMigrationGeneratorStub(), entityDiscovery: Helpers::createStubEntityDiscovery(), introspector: Helpers::createStubIntrospector(), - metadataFactory: new EntityMetadataFactory(), - schemaBuilder: new SchemaBuilder(), + schemaRegistry: new SchemaRegistry(new EntityMetadataFactory(), new SchemaBuilder()), diffCalculator: createMigrateDiffCalculator($diff ?? new SchemaDiff()), sqlGenerator: $sqlGenerator ?? createMigrateSqlGenerator(), paths: new ProjectPaths('/test'), @@ -684,8 +686,7 @@ function executeMigrateCommand( migrationGenerator: $generator, entityDiscovery: Helpers::createStubEntityDiscovery(), introspector: $introspector, - metadataFactory: new EntityMetadataFactory(), - schemaBuilder: new SchemaBuilder(), + schemaRegistry: new SchemaRegistry(new EntityMetadataFactory(), new SchemaBuilder()), diffCalculator: new DiffCalculator(), sqlGenerator: createMigrateSqlGenerator(), paths: new ProjectPaths('/test'), @@ -698,3 +699,53 @@ function executeMigrateCommand( expect($output)->toContain('Nothing to migrate') ->and($generator->generateCalled)->toBeFalse(); }); + +it('merges extender columns into parent table schema before computing diff (regression for #66)', function (): void { + // Capture the entitySchema passed to DiffCalculator so we can prove the + // extender's `extra` column was merged into the parent `users` table + // before the diff was computed. Pre-fix the command bypassed + // SchemaRegistry's extender merge and the extender column would be + // absent from $entitySchema['users']. + /** @var array|null $captured */ + $captured = null; + + $capturingCalculator = new class ($captured) extends DiffCalculator + { + public function __construct( + /** @noinspection PhpPropertyOnlyWrittenInspection - Reference property captures schema for assertion */ + private ?array &$captured, + ) {} + + public function calculate( + array $entitySchema, + array $databaseSchema, + ): SchemaDiff { + $this->captured = $entitySchema; + + return new SchemaDiff(); + } + }; + + $command = new MigrateCommand( + migrator: createMigratorStub(), + dataMigrator: createDataMigratorStub(), + migrationGenerator: createMigrationGeneratorStub(), + entityDiscovery: Helpers::createStubEntityDiscovery([ExtenderParentEntity::class, BasicExtenderEntity::class]), + introspector: Helpers::createStubIntrospector(), + schemaRegistry: new SchemaRegistry(new EntityMetadataFactory(), new SchemaBuilder()), + diffCalculator: $capturingCalculator, + sqlGenerator: createMigrateSqlGenerator(), + paths: new ProjectPaths('/test'), + isProduction: false, + ); + + executeMigrateCommand($command); + + expect($captured)->toHaveKey('users') + ->and($captured['users']->columns)->toHaveCount(2); + + $columnNames = array_map(fn (Column $c): string => $c->name, $captured['users']->columns); + + expect($columnNames)->toContain('id') + ->and($columnNames)->toContain('extra'); +});