diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d714ca7..309bb65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.0.2 under development -- no changes in this release. +- Bug #268: Fix foreign keys: support multiple foreign keys referencing to one table and possible null columns for reference (@Tigrov) ## 1.0.1 July 24, 2023 diff --git a/src/Schema.php b/src/Schema.php index 2e663e88..88dd18bc 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -20,6 +20,7 @@ use Yiisoft\Db\Schema\ColumnSchemaInterface; use Yiisoft\Db\Schema\TableSchemaInterface; +use function array_column; use function array_merge; use function count; use function explode; @@ -39,7 +40,7 @@ * seq:string, * table:string, * from:string, - * to:string, + * to:string|null, * on_update:string, * on_delete:string * } @@ -204,15 +205,35 @@ protected function loadTableForeignKeys(string $tableName): array DbArrayHelper::multisort($foreignKeysList, 'seq'); /** @psalm-var GroupedForeignKeyInfo $foreignKeysList */ - foreach ($foreignKeysList as $table => $foreignKey) { - $fk = (new ForeignKeyConstraint()) - ->columnNames(DbArrayHelper::getColumn($foreignKey, 'from')) - ->foreignTableName($table) - ->foreignColumnNames(DbArrayHelper::getColumn($foreignKey, 'to')) - ->onDelete($foreignKey[0]['on_delete'] ?? null) - ->onUpdate($foreignKey[0]['on_update'] ?? null); - - $result[] = $fk; + foreach ($foreignKeysList as $table => $foreignKeys) { + $foreignKeysById = DbArrayHelper::index($foreignKeys, null, ['id']); + + /** + * @psalm-var GroupedForeignKeyInfo $foreignKeysById + * @psalm-var int $id + */ + foreach ($foreignKeysById as $id => $foreignKey) { + if ($foreignKey[0]['to'] === null) { + $primaryKey = $this->getTablePrimaryKey($table); + + if ($primaryKey !== null) { + /** @psalm-var string $primaryKeyColumnName */ + foreach ((array) $primaryKey->getColumnNames() as $i => $primaryKeyColumnName) { + $foreignKey[$i]['to'] = $primaryKeyColumnName; + } + } + } + + $fk = (new ForeignKeyConstraint()) + ->name((string) $id) + ->columnNames(array_column($foreignKey, 'from')) + ->foreignTableName($table) + ->foreignColumnNames(array_column($foreignKey, 'to')) + ->onDelete($foreignKey[0]['on_delete']) + ->onUpdate($foreignKey[0]['on_update']); + + $result[] = $fk; + } } return $result; @@ -374,19 +395,18 @@ protected function findColumns(TableSchemaInterface $table): bool */ protected function findConstraints(TableSchemaInterface $table): void { - /** @psalm-var ForeignKeyInfo[] $foreignKeysList */ - $foreignKeysList = $this->getPragmaForeignKeyList($table->getName()); + /** @psalm-var ForeignKeyConstraint[] $foreignKeysList */ + $foreignKeysList = $this->getTableForeignKeys($table->getName(), true); foreach ($foreignKeysList as $foreignKey) { - $id = (int) $foreignKey['id']; - $fk = $table->getForeignKeys(); - - if (!isset($fk[$id])) { - $table->foreignKey($id, [$foreignKey['table'], $foreignKey['from'] => $foreignKey['to']]); - } else { - /** composite FK */ - $table->compositeForeignKey($id, $foreignKey['from'], $foreignKey['to']); - } + /** @var array $columnNames */ + $columnNames = (array) $foreignKey->getColumnNames(); + $columnNames = array_combine($columnNames, $foreignKey->getForeignColumnNames()); + + $foreignReference = array_merge([$foreignKey->getForeignTableName()], $columnNames); + + /** @psalm-suppress InvalidCast */ + $table->foreignKey((string) $foreignKey->getName(), $foreignReference); } } diff --git a/src/TableSchema.php b/src/TableSchema.php index 8251dc04..8d1a7b0b 100644 --- a/src/TableSchema.php +++ b/src/TableSchema.php @@ -11,6 +11,9 @@ */ final class TableSchema extends AbstractTableSchema { + /** + * @deprecated will be removed in version 2.0.0 + */ public function compositeForeignKey(int $id, string $from, string $to): void { $this->foreignKeys[$id][$from] = $to; diff --git a/tests/Provider/SchemaProvider.php b/tests/Provider/SchemaProvider.php index 6c76af51..5210b461 100644 --- a/tests/Provider/SchemaProvider.php +++ b/tests/Provider/SchemaProvider.php @@ -405,7 +405,7 @@ public static function constraints(): array $constraints['2: unique'][2][0]->name(AnyValue::getInstance()); $constraints['2: index'][2][2]->name(AnyValue::getInstance()); - $constraints['3: foreign key'][2][0]->name(null); + $constraints['3: foreign key'][2][0]->name('0'); $constraints['3: index'][2] = []; $constraints['4: primary key'][2]->name(null); diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index cf0cb310..e15c3f1b 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -162,6 +162,33 @@ public function testForeingKey(): void $this->assertSame('NO ACTION', $foreingKeys[1]->getOnUpdate()); } + public function testMultiForeingKeys(): void + { + $db = $this->getConnection(true); + $schema = $db->getSchema(); + $tableSchema = $schema->getTableSchema('foreign_keys_child'); + + $this->assertNotNull($tableSchema); + + $foreignKeys = $tableSchema->getForeignKeys(); + + $this->assertSame( + [ + [ + 'foreign_keys_parent', + 'y' => 'b', + 'z' => 'c', + ], + [ + 'foreign_keys_parent', + 'x' => 'a', + 'y' => 'b', + ], + ], + $foreignKeys + ); + } + /** * @throws Exception * @throws InvalidConfigException @@ -242,6 +269,9 @@ public function testGetTableForeignKeys(): void $this->assertSame(['C_id_1', 'C_id_2'], $tableForeingKeys[0]->getForeignColumnNames()); $this->assertSame('CASCADE', $tableForeingKeys[0]->getOnDelete()); $this->assertSame('CASCADE', $tableForeingKeys[0]->getOnUpdate()); + + $tableTwoForeignKeys = $schema->getTableForeignKeys('foreign_keys_child'); + $this->assertCount(2, $tableTwoForeignKeys); } /** diff --git a/tests/Support/Fixture/sqlite.sql b/tests/Support/Fixture/sqlite.sql index 611ce606..f54cf7ba 100644 --- a/tests/Support/Fixture/sqlite.sql +++ b/tests/Support/Fixture/sqlite.sql @@ -23,6 +23,8 @@ DROP TABLE IF EXISTS "T_constraints_1"; DROP TABLE IF EXISTS "T_upsert"; DROP TABLE IF EXISTS "T_upsert_1"; DROP TABLE IF EXISTS "T_constraints_check"; +DROP TABLE IF EXISTS "foreign_keys_parent"; +DROP TABLE IF EXISTS "foreign_keys_child"; CREATE TABLE "profile" ( id INTEGER NOT NULL, @@ -289,3 +291,21 @@ CREATE TABLE "T_constraints_check" "C_check_2" INT NOT NULL CHECK ("C_check_2" > 0), CONSTRAINT "CN_constraints_check" CHECK ("C_check_1" > "C_check_2") ); + +CREATE TABLE foreign_keys_parent +( + a INTEGER, + b INTEGER, + c INTEGER, + PRIMARY KEY(a, b), + UNIQUE (b, c) +); + +CREATE TABLE foreign_keys_child +( + x INTEGER, + y INTEGER, + z INTEGER, + FOREIGN KEY(x, y) REFERENCES foreign_keys_parent, + FOREIGN KEY(y, z) REFERENCES foreign_keys_parent(b, c) +);