Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
62 changes: 41 additions & 21 deletions src/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,7 +40,7 @@
* seq:string,
* table:string,
* from:string,
* to:string,
* to:string|null,
* on_update:string,
* on_delete:string
* }
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<string> $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);
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/TableSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/Provider/SchemaProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions tests/SchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand Down
20 changes: 20 additions & 0 deletions tests/Support/Fixture/sqlite.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
);