From 1348aec6aec314376b799823695250b9f2b74644 Mon Sep 17 00:00:00 2001 From: Kevin NGUYEN Date: Wed, 13 May 2026 09:45:35 +0200 Subject: [PATCH] fix: sort foreign keys in SchemaNormalizer to stabilize bean generation When a table has two foreign keys pointing to the same target table, the generated Base bean methods for the reverse relation (one-to-many) were reordered on every `tdbm:generate` run. Root cause: SchemaNormalizer::normalizeTable() wrote foreign_keys to the lock file in the order returned by DB introspection (AbstractSchemaManager::introspectSchema()). MySQL/MariaDB does not guarantee a stable ordering of FK constraints, so the lock file FK order could differ between environments or after schema changes. Since tdbm:generate writes the lock file then immediately reads it to generate beans, the bean method order inherited this non-determinism. The post-processing normalize-tdbm-yaml.php script sorted the lock file after the fact, but beans were already generated by then. Fix: apply ksort() on foreign_keys after collection in normalizeTable(), consistent with the existing ksort() on tables in normalize(). Guard with isset() for tables that have no foreign keys. Co-Authored-By: Claude Sonnet 4.6 --- src/SchemaVersionControl/SchemaNormalizer.php | 3 ++ .../SchemaNormalizerTest.php | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/SchemaVersionControl/SchemaNormalizerTest.php diff --git a/src/SchemaVersionControl/SchemaNormalizer.php b/src/SchemaVersionControl/SchemaNormalizer.php index bcda343e..136271b8 100644 --- a/src/SchemaVersionControl/SchemaNormalizer.php +++ b/src/SchemaVersionControl/SchemaNormalizer.php @@ -72,6 +72,9 @@ protected function normalizeTable(Table $table) foreach ($table->getForeignKeys() as $foreignKey) { $tableDesc['foreign_keys'][$foreignKey->getName()] = $this->normalizeForeignKeyConstraint($foreignKey); } + if (isset($tableDesc['foreign_keys'])) { + ksort($tableDesc['foreign_keys']); + } return $tableDesc; } diff --git a/tests/SchemaVersionControl/SchemaNormalizerTest.php b/tests/SchemaVersionControl/SchemaNormalizerTest.php new file mode 100644 index 00000000..2b5a363b --- /dev/null +++ b/tests/SchemaVersionControl/SchemaNormalizerTest.php @@ -0,0 +1,35 @@ +createTable('users'); + $users->addColumn('id', 'integer'); + $users->setPrimaryKey(['id']); + + $reviews = $schema->createTable('reviews'); + $reviews->addColumn('id', 'integer'); + $reviews->addColumn('reviewed_by', 'integer'); + $reviews->addColumn('reversed_by', 'integer'); + $reviews->setPrimaryKey(['id']); + // Add in Z→A order to prove sorting is applied, not insertion order + $reviews->addForeignKeyConstraint('users', ['reversed_by'], ['id'], [], 'FK_ZZZ'); + $reviews->addForeignKeyConstraint('users', ['reviewed_by'], ['id'], [], 'FK_AAA'); + + $normalizer = new SchemaNormalizer(); + $result = $normalizer->normalize($schema, new SchemaConfig()); + + $fkKeys = array_keys($result['tables']['reviews']['foreign_keys']); + $this->assertSame(['FK_AAA', 'FK_ZZZ'], $fkKeys); + } +}