Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
64 changes: 42 additions & 22 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 @@ -44,7 +45,7 @@
* seq:string,
* table:string,
* from:string,
* to:string,
* to:string|null,
* on_update:string,
* on_delete:string
* }
Expand All @@ -58,7 +59,7 @@
* seq:string,
* table:string,
* from:string,
* to:string,
* to:string|null,
* on_update:string,
* on_delete:string
* }
Expand Down Expand Up @@ -210,15 +211,35 @@ protected function loadTableForeignKeys(string $tableName): array
DbArrayHelper::multisort($foreignKeysList, 'seq');

/** @psalm-var NormalizePragmaForeignKeyList $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 NormalizePragmaForeignKeyList $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 @@ -380,19 +401,18 @@ protected function findColumns(TableSchemaInterface $table): bool
*/
protected function findConstraints(TableSchemaInterface $table): void
{
/** @psalm-var PragmaForeignKeyList $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((int) $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 @@ -392,7 +392,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 @@ -288,3 +290,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)
);