From 3d7cb69f413a9f1225beb89414c11fab696b2667 Mon Sep 17 00:00:00 2001 From: Ilya Beliaev Date: Thu, 3 Jul 2025 21:59:14 +0200 Subject: [PATCH 1/4] Refactor PivotProcessor: Replace composite primary key with auto-increment ID and preserve uniqueness via unique index - Replaces composite primary keys (e.g. role_id + customer_id) with a single 'id' column (bigInteger, auto-increment) - Converts the removed primary key columns into a unique index to retain logical constraints - Ensures compatibility with Laravel's Eloquent model usage on pivot tables --- src/Service/Generator/Compiler/Mapper/IndexMapper.php | 8 +++++++- src/Service/Generator/MigrationGenerator.php | 1 - .../Normalization/Processors/PivotProcessor.php | 10 +++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Service/Generator/Compiler/Mapper/IndexMapper.php b/src/Service/Generator/Compiler/Mapper/IndexMapper.php index 8af228e..3b21426 100644 --- a/src/Service/Generator/Compiler/Mapper/IndexMapper.php +++ b/src/Service/Generator/Compiler/Mapper/IndexMapper.php @@ -47,7 +47,13 @@ protected function generateIndex(IndexEntity $index): string $method .= "'" . $columns[0] . "'"; } - $method .= ", '" . $index->getName() . "')"; + $name = trim($index->getName()); + if ($name !== '') { + $method .= ", '" . $name . "'"; + } + + $method .= ')'; + $methods = [$method]; return $this->chainMethodsToString($methods); diff --git a/src/Service/Generator/MigrationGenerator.php b/src/Service/Generator/MigrationGenerator.php index 1ad30d3..e58f31e 100644 --- a/src/Service/Generator/MigrationGenerator.php +++ b/src/Service/Generator/MigrationGenerator.php @@ -130,7 +130,6 @@ public function generateMigrationForTable( $normalizationManager = $this->getNormalizationManager(); if ($normalizationManager) { - //dd($schemaResult); $schemaResult = $normalizationManager->normalize($schemaResult); } diff --git a/src/Service/Generator/Normalization/Processors/PivotProcessor.php b/src/Service/Generator/Normalization/Processors/PivotProcessor.php index 833ebed..de23d1a 100644 --- a/src/Service/Generator/Normalization/Processors/PivotProcessor.php +++ b/src/Service/Generator/Normalization/Processors/PivotProcessor.php @@ -3,6 +3,7 @@ namespace N3XT0R\MigrationGenerator\Service\Generator\Normalization\Processors; use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\FieldEntity; +use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\IndexEntity; use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\PrimaryKeyEntity; use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\ResultEntity; use N3XT0R\MigrationGenerator\Service\Generator\Normalization\Context\NormalizationContext; @@ -22,7 +23,8 @@ public function process(NormalizationContext $context): ResultEntity foreach ($results as $tableName => $definitions) { $primary = current($definitions['primaryKey']); - if ($primary instanceof PrimaryKeyEntity && count($primary->getColumns()) >= 2) { + $pkColumns = $primary->getColumns(); + if ($primary instanceof PrimaryKeyEntity && count($pkColumns) >= 2) { $idField = new FieldEntity(); $idField->setTable($tableName); $idField->setType('bigInteger'); @@ -33,9 +35,15 @@ public function process(NormalizationContext $context): ResultEntity 'nullable' => false ]); + $uniqueIndex = new IndexEntity(); + $uniqueIndex->setType('unique'); + $uniqueIndex->setColumns($pkColumns); + $idField->setColumnName('id'); $results[$tableName]['table'] = ['id' => $idField, ...$definitions['table']]; + $results[$tableName]['index'] = [$uniqueIndex, ...$definitions['index']]; unset($results[$tableName]['primaryKey']); + //dump($results); } } From 00791791716a75a931b3fa0c91aa23c95ac6df8d Mon Sep 17 00:00:00 2001 From: Ilya Beliaev Date: Thu, 3 Jul 2025 22:01:13 +0200 Subject: [PATCH 2/4] Refactor PivotProcessor: Replace composite primary key with auto-increment ID and preserve uniqueness via unique index - Replaces composite primary keys (e.g. role_id + customer_id) with a single 'id' column (bigInteger, auto-increment) - Converts the removed primary key columns into a unique index to retain logical constraints - Ensures compatibility with Laravel's Eloquent model usage on pivot tables --- .../Processors/PivotProcessor.php | 72 +++++++++++++------ 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/src/Service/Generator/Normalization/Processors/PivotProcessor.php b/src/Service/Generator/Normalization/Processors/PivotProcessor.php index de23d1a..c455b99 100644 --- a/src/Service/Generator/Normalization/Processors/PivotProcessor.php +++ b/src/Service/Generator/Normalization/Processors/PivotProcessor.php @@ -22,33 +22,63 @@ public function process(NormalizationContext $context): ResultEntity $results = $result->getResults(); foreach ($results as $tableName => $definitions) { + if (!isset($definitions['primaryKey'])) { + continue; + } + + /** @var PrimaryKeyEntity|null $primary */ $primary = current($definitions['primaryKey']); - $pkColumns = $primary->getColumns(); - if ($primary instanceof PrimaryKeyEntity && count($pkColumns) >= 2) { - $idField = new FieldEntity(); - $idField->setTable($tableName); - $idField->setType('bigInteger'); - $idField->setArguments(['autoIncrement' => true]); - $idField->setOptions([ - 'default' => null, - 'unsigned' => true, - 'nullable' => false - ]); - - $uniqueIndex = new IndexEntity(); - $uniqueIndex->setType('unique'); - $uniqueIndex->setColumns($pkColumns); - - $idField->setColumnName('id'); - $results[$tableName]['table'] = ['id' => $idField, ...$definitions['table']]; - $results[$tableName]['index'] = [$uniqueIndex, ...$definitions['index']]; - unset($results[$tableName]['primaryKey']); - //dump($results); + if (!$this->isCompositePrimaryKey($primary)) { + continue; } + + $results[$tableName] = $this->transformPivotTable($tableName, $definitions, $primary); } $result->setResults($results); $context->update($result); + return $result; } + + protected function isCompositePrimaryKey(?PrimaryKeyEntity $primary): bool + { + return $primary instanceof PrimaryKeyEntity && count($primary->getColumns()) >= 2; + } + + protected function transformPivotTable(string $tableName, array $definitions, PrimaryKeyEntity $primary): array + { + $definitions['table'] = $this->prependIdField($tableName, $definitions['table']); + unset($definitions['primaryKey']); + $definitions['index'][] = $this->createUniqueIndexFromPrimaryKey($primary); + + return $definitions; + } + + protected function prependIdField(string $tableName, array $fields): array + { + $idField = new FieldEntity(); + $idField->setTable($tableName); + $idField->setType('bigInteger'); + $idField->setArguments(['autoIncrement' => true]); + $idField->setOptions([ + 'default' => null, + 'unsigned' => true, + 'nullable' => false, + ]); + $idField->setColumnName('id'); + + return ['id' => $idField] + $fields; + } + + protected function createUniqueIndexFromPrimaryKey(PrimaryKeyEntity $primary): IndexEntity + { + $index = new IndexEntity(); + $index->setType('unique'); + $index->setIndexType('index'); + $index->setColumns($primary->getColumns()); + $index->setName(implode('_', $primary->getColumns()) . '_unique'); + + return $index; + } } \ No newline at end of file From b9ce335b757df49e41b964d31ee4081732eb3d19 Mon Sep 17 00:00:00 2001 From: Ilya Beliaev Date: Thu, 3 Jul 2025 22:17:20 +0200 Subject: [PATCH 3/4] test added --- .../Processors/PivotProcessorTest.php | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/Unit/Generator/Normalization/Processors/PivotProcessorTest.php diff --git a/tests/Unit/Generator/Normalization/Processors/PivotProcessorTest.php b/tests/Unit/Generator/Normalization/Processors/PivotProcessorTest.php new file mode 100644 index 0000000..ef8ed28 --- /dev/null +++ b/tests/Unit/Generator/Normalization/Processors/PivotProcessorTest.php @@ -0,0 +1,62 @@ +processor = new PivotProcessor(); + } + + public function test_pivot_processor_replaces_primary_with_id_and_adds_unique_index(): void + { + $pk = new PrimaryKeyEntity(); + $pk->setColumns(['role_id', 'user_id']); + + $roleId = new FieldEntity(); + $roleId->setType('bigInteger'); + $roleId->setColumnName('role_id'); + $roleId->setOptions(['nullable' => false, 'unsigned' => true, 'default' => null]); + + $userId = new FieldEntity(); + $userId->setType('bigInteger'); + $userId->setColumnName('user_id'); + $userId->setOptions(['nullable' => false, 'unsigned' => true, 'default' => null]); + + $result = new ResultEntity(); + $result->setTableName('role_customer'); + $result->setResults([ + 'role_customer' => [ + 'table' => [ + 'role_id' => $roleId, + 'user_id' => $userId, + ], + 'primaryKey' => ['PRIMARY' => $pk], + ], + ]); + + $context = new NormalizationContext($result); + $table = (new PivotProcessor())->process($context)->getResultByTable('role_customer'); + + $this->assertArrayHasKey('id', $table['table']); + $this->assertArrayNotHasKey('primaryKey', $table); + $this->assertNotEmpty($table['index']); + + $unique = array_filter($table['index'], fn($i) => $i->getType() === 'unique'); + $this->assertSame(['role_id', 'user_id'], array_values($unique)[0]->getColumns()); + } + + +} \ No newline at end of file From 6de3c792e7353414c910196bcef5547252d14bbc Mon Sep 17 00:00:00 2001 From: Ilya Beliaev Date: Thu, 3 Jul 2025 22:32:00 +0200 Subject: [PATCH 4/4] tests changed/added --- .../Compiler/Mapper/AbstractMapperTest.php | 6 +- .../Compiler/Mapper/FieldMapperTest.php | 2 +- .../Compiler/Mapper/ForeignKeyMapperTest.php | 2 +- .../Compiler/Mapper/IndexMapperTest.php | 2 +- .../Definition/AbstractDefinitionTest.php | 6 +- .../Entity/AbstractIndexEntityTest.php | 2 +- .../Definition/Entity/FieldEntityTest.php | 2 +- .../Entity/ForeignKeyEntityTest.php | 2 +- .../Entity/PrimaryKeyDefinitionTest.php | 2 +- .../Definition/Entity/ResultEntityTest.php | 2 +- .../Context/NormalizationContextTest.php | 87 +++++++++++++++++++ .../Processors/PivotProcessorTest.php | 2 +- tests/Unit/Sort/TopSortTest.php | 2 +- 13 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 tests/Unit/Generator/Normalization/Context/NormalizationContextTest.php diff --git a/tests/Unit/Generator/Compiler/Mapper/AbstractMapperTest.php b/tests/Unit/Generator/Compiler/Mapper/AbstractMapperTest.php index 6b0adce..e111368 100644 --- a/tests/Unit/Generator/Compiler/Mapper/AbstractMapperTest.php +++ b/tests/Unit/Generator/Compiler/Mapper/AbstractMapperTest.php @@ -6,7 +6,7 @@ use N3XT0R\MigrationGenerator\Service\Generator\Compiler\Mapper\AbstractMapper; use PHPUnit\Framework\Attributes\DataProvider; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class AbstractMapperTest extends TestCase { @@ -45,8 +45,8 @@ public static function chainMethodProvider(): array } /** - * @param array $methods - * @param string $expectedResult + * @param array $methods + * @param string $expectedResult */ #[DataProvider('chainMethodProvider')] public function testChainMethodsToStringWorks(array $methods, string $expectedResult): void diff --git a/tests/Unit/Generator/Compiler/Mapper/FieldMapperTest.php b/tests/Unit/Generator/Compiler/Mapper/FieldMapperTest.php index 0665bec..1385903 100644 --- a/tests/Unit/Generator/Compiler/Mapper/FieldMapperTest.php +++ b/tests/Unit/Generator/Compiler/Mapper/FieldMapperTest.php @@ -7,7 +7,7 @@ use N3XT0R\MigrationGenerator\Service\Generator\Compiler\Mapper\FieldMapper; use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\FieldEntity; use PHPUnit\Framework\Attributes\DataProvider; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class FieldMapperTest extends TestCase { diff --git a/tests/Unit/Generator/Compiler/Mapper/ForeignKeyMapperTest.php b/tests/Unit/Generator/Compiler/Mapper/ForeignKeyMapperTest.php index 09ff12a..edc038a 100644 --- a/tests/Unit/Generator/Compiler/Mapper/ForeignKeyMapperTest.php +++ b/tests/Unit/Generator/Compiler/Mapper/ForeignKeyMapperTest.php @@ -6,7 +6,7 @@ use N3XT0R\MigrationGenerator\Service\Generator\Compiler\Mapper\ForeignKeyMapper; use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\ForeignKeyEntity; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class ForeignKeyMapperTest extends TestCase { diff --git a/tests/Unit/Generator/Compiler/Mapper/IndexMapperTest.php b/tests/Unit/Generator/Compiler/Mapper/IndexMapperTest.php index 18d42a8..ab82bde 100644 --- a/tests/Unit/Generator/Compiler/Mapper/IndexMapperTest.php +++ b/tests/Unit/Generator/Compiler/Mapper/IndexMapperTest.php @@ -6,7 +6,7 @@ use N3XT0R\MigrationGenerator\Service\Generator\Compiler\Mapper\IndexMapper; use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\IndexEntity; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class IndexMapperTest extends TestCase { diff --git a/tests/Unit/Generator/Definition/AbstractDefinitionTest.php b/tests/Unit/Generator/Definition/AbstractDefinitionTest.php index c846354..95845f0 100644 --- a/tests/Unit/Generator/Definition/AbstractDefinitionTest.php +++ b/tests/Unit/Generator/Definition/AbstractDefinitionTest.php @@ -40,8 +40,8 @@ public function testSetAndGetResultAreSame(): void } /** - * @param string $attributeName - * @param bool $expectedResult + * @param string $attributeName + * @param bool $expectedResult * @testWith ["test", true] * ["test2", false] */ @@ -78,7 +78,7 @@ public function testSetAndGetSchemaAreSame(): void } /** - * @param bool $expectedResult + * @param bool $expectedResult * @testWith [true] * [false] */ diff --git a/tests/Unit/Generator/Definition/Entity/AbstractIndexEntityTest.php b/tests/Unit/Generator/Definition/Entity/AbstractIndexEntityTest.php index dc8b96c..4006b21 100644 --- a/tests/Unit/Generator/Definition/Entity/AbstractIndexEntityTest.php +++ b/tests/Unit/Generator/Definition/Entity/AbstractIndexEntityTest.php @@ -3,7 +3,7 @@ namespace Generator\Definition\Entity; use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\AbstractIndexEntity; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class AbstractIndexEntityTest extends TestCase { diff --git a/tests/Unit/Generator/Definition/Entity/FieldEntityTest.php b/tests/Unit/Generator/Definition/Entity/FieldEntityTest.php index 4770eab..528fd85 100644 --- a/tests/Unit/Generator/Definition/Entity/FieldEntityTest.php +++ b/tests/Unit/Generator/Definition/Entity/FieldEntityTest.php @@ -5,7 +5,7 @@ use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\FieldEntity; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class FieldEntityTest extends TestCase { diff --git a/tests/Unit/Generator/Definition/Entity/ForeignKeyEntityTest.php b/tests/Unit/Generator/Definition/Entity/ForeignKeyEntityTest.php index dea43a3..3e879fb 100644 --- a/tests/Unit/Generator/Definition/Entity/ForeignKeyEntityTest.php +++ b/tests/Unit/Generator/Definition/Entity/ForeignKeyEntityTest.php @@ -5,7 +5,7 @@ use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\ForeignKeyEntity; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class ForeignKeyEntityTest extends TestCase { diff --git a/tests/Unit/Generator/Definition/Entity/PrimaryKeyDefinitionTest.php b/tests/Unit/Generator/Definition/Entity/PrimaryKeyDefinitionTest.php index f3dbcb9..d2e5510 100644 --- a/tests/Unit/Generator/Definition/Entity/PrimaryKeyDefinitionTest.php +++ b/tests/Unit/Generator/Definition/Entity/PrimaryKeyDefinitionTest.php @@ -3,7 +3,7 @@ namespace Generator\Definition\Entity; use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\PrimaryKeyEntity; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class PrimaryKeyDefinitionTest extends TestCase { diff --git a/tests/Unit/Generator/Definition/Entity/ResultEntityTest.php b/tests/Unit/Generator/Definition/Entity/ResultEntityTest.php index 31c3640..0da83f0 100644 --- a/tests/Unit/Generator/Definition/Entity/ResultEntityTest.php +++ b/tests/Unit/Generator/Definition/Entity/ResultEntityTest.php @@ -5,7 +5,7 @@ use N3XT0R\MigrationGenerator\Service\Generator\Definition\Entity\ResultEntity; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class ResultEntityTest extends TestCase { diff --git a/tests/Unit/Generator/Normalization/Context/NormalizationContextTest.php b/tests/Unit/Generator/Normalization/Context/NormalizationContextTest.php new file mode 100644 index 0000000..f1174d4 --- /dev/null +++ b/tests/Unit/Generator/Normalization/Context/NormalizationContextTest.php @@ -0,0 +1,87 @@ +setTableName('test'); + $entity->setResults(['test' => ['foo' => 'bar']]); + + $context = new NormalizationContext($entity); + + $this->assertSame($entity, $context->getCurrent()); + $this->assertNotSame($entity, $context->getOriginal()); + $this->assertNotSame($entity, $context->getPrevious()); + + $this->assertEquals($entity->getResults(), $context->getOriginal()->getResults()); + $this->assertEquals($entity->getResults(), $context->getPrevious()->getResults()); + } + + public function testUpdateSetsPreviousAndCurrent(): void + { + $initial = new ResultEntity(); + $initial->setResults(['t1' => ['a' => 1]]); + $context = new NormalizationContext($initial); + + $updated = new ResultEntity(); + $updated->setResults(['t1' => ['a' => 2]]); + $context->update($updated); + + $this->assertSame($updated, $context->getCurrent()); + $this->assertNotSame($updated, $context->getPrevious()); + $this->assertEquals(['a' => 1], $context->getPrevious()->getResults()['t1']); + $this->assertEquals(['a' => 2], $context->getCurrent()->getResults()['t1']); + } + + public function testGetTableResultsReturnsExpectedData(): void + { + $entity = new ResultEntity(); + $entity->setResults([ + 'users' => ['id' => 'field'], + 'posts' => ['id' => 'field'] + ]); + + $context = new NormalizationContext($entity); + + $this->assertEquals(['id' => 'field'], $context->getTableResults('users')); + $this->assertEquals([], $context->getTableResults('nonExisting')); + } + + public function testHasChangedDetectsDifference(): void + { + $entity = new ResultEntity(); + $entity->setResults(['table' => ['a' => 1]]); + $context = new NormalizationContext($entity); + + $this->assertFalse($context->hasChanged()); + + $modified = new ResultEntity(); + $modified->setResults(['table' => ['a' => 2]]); + $context->update($modified); + + $this->assertTrue($context->hasChanged()); + } + + public function testDiffTableDetectsAddedAndRemovedKeys(): void + { + $entity = new ResultEntity(); + $entity->setResults(['example' => ['a' => 1, 'b' => 2]]); + + $context = new NormalizationContext($entity); + + $modified = new ResultEntity(); + $modified->setResults(['example' => ['b' => 2, 'c' => 3]]); + $context->update($modified); + + $diff = $context->diffTable('example'); + $this->assertEquals(['c'], $diff['added_keys']); + $this->assertEquals(['a'], $diff['removed_keys']); + } +} diff --git a/tests/Unit/Generator/Normalization/Processors/PivotProcessorTest.php b/tests/Unit/Generator/Normalization/Processors/PivotProcessorTest.php index ef8ed28..57c73d8 100644 --- a/tests/Unit/Generator/Normalization/Processors/PivotProcessorTest.php +++ b/tests/Unit/Generator/Normalization/Processors/PivotProcessorTest.php @@ -8,7 +8,7 @@ use N3XT0R\MigrationGenerator\Service\Generator\Normalization\Context\NormalizationContext; use N3XT0R\MigrationGenerator\Service\Generator\Normalization\Processors\PivotProcessor; use N3XT0R\MigrationGenerator\Service\Generator\Normalization\Processors\ProcessorInterface; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class PivotProcessorTest extends TestCase { diff --git a/tests/Unit/Sort/TopSortTest.php b/tests/Unit/Sort/TopSortTest.php index 73e04ab..dfe2470 100644 --- a/tests/Unit/Sort/TopSortTest.php +++ b/tests/Unit/Sort/TopSortTest.php @@ -5,7 +5,7 @@ use N3XT0R\MigrationGenerator\Service\Generator\Sort\TopSort; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; class TopSortTest extends TestCase {