Skip to content
Open
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
15 changes: 9 additions & 6 deletions src/Schema/Comparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,14 +275,17 @@ public function compareTables(Table $oldTable, Table $newTable): TableDiff
foreach ($newForeignKeys as $newKey => $newForeignKey) {
if ($this->diffForeignKey($oldForeignKey, $newForeignKey) === false) {
unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
} else {
if (strtolower($oldForeignKey->getName()) === strtolower($newForeignKey->getName())) {
$droppedForeignKeys[$oldKey] = $oldForeignKey;
$addedForeignKeys[$newKey] = $newForeignKey;
continue 2;
Copy link
Member

@morozov morozov Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand, this is the fix. The rest of the lines have been mostly affected to satisfy the coding standard.

The essence of the fix is that once we have found a matching pair of new key / old key, we not only continue the inner loop, we continue the outer one as well. Otherwise, the other equal key will also match.

Is that correct?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the continue 2; is the actual change. It stops comparing the new key when a matching old key has been found.

}

unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
}
if (strtolower($oldForeignKey->getName()) !== strtolower($newForeignKey->getName())) {
continue;
}

$droppedForeignKeys[$oldKey] = $oldForeignKey;
$addedForeignKeys[$newKey] = $newForeignKey;

unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
}
}

Expand Down
183 changes: 183 additions & 0 deletions tests/Functional/Schema/DoubleForeignKeyConstraintTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Schema;

use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Types\Types;

class DoubleForeignKeyConstraintTest extends FunctionalTestCase
{
private AbstractSchemaManager $schemaManager;

protected function setUp(): void
{
$this->schemaManager = $this->connection->createSchemaManager();
}

public function testDoubleForeignKeyConstraint(): void
{
$platform = $this->connection->getDatabasePlatform();
if ($platform instanceof DB2Platform || $platform instanceof OraclePlatform) {
self::markTestSkipped('DB2 and Oracle do not allow multiple FKs with the same columns.');
}

$articles = Table::editor()
->setUnquotedName('articles')
->setColumns(
Column::editor()
->setUnquotedName('id')
->setTypeName(Types::INTEGER)
->create(),
)
->setPrimaryKeyConstraint(
PrimaryKeyConstraint::editor()
->setUnquotedColumnNames('id')
->create(),
)
->create();

$orders = Table::editor()
->setUnquotedName('orders')
->setColumns(
Column::editor()
->setUnquotedName('id')
->setTypeName(Types::INTEGER)
->create(),
Column::editor()
->setUnquotedName('article_id')
->setTypeName(Types::INTEGER)
->create(),
)
->setForeignKeyConstraints(
ForeignKeyConstraint::editor()
->setUnquotedName('articles_fk')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be named "articles_fk_1" for consistency?

->setUnquotedReferencingColumnNames('article_id')
->setUnquotedReferencedTableName('articles')
->setUnquotedReferencedColumnNames('id')
->create(),
ForeignKeyConstraint::editor()
->setUnquotedName('articles_fk_2')
->setUnquotedReferencingColumnNames('article_id')
->setUnquotedReferencedTableName('articles')
->setUnquotedReferencedColumnNames('id')
->create(),
)
->create();

$this->dropTableIfExists('orders');
$this->dropTableIfExists('articles');

$this->connection->createSchemaManager()
->createTable($articles);
$this->connection->createSchemaManager()
->createTable($orders);

$ordersActual = $this->schemaManager->introspectTable('orders');

self::assertTrue(
$this->schemaManager->createComparator()
->compareTables($ordersActual, $orders)
->isEmpty(),
);
}

public function testDoubleForeignKeyConstraintComparedToSingle(): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this test passes even without the fix.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the regression test @derrabus asked for.

{
$platform = $this->connection->getDatabasePlatform();
if ($platform instanceof DB2Platform || $platform instanceof OraclePlatform) {
self::markTestSkipped('DB2 and Oracle do not allow multiple FKs with the same columns.');
}

$articles = Table::editor()
->setUnquotedName('articles')
->setColumns(
Column::editor()
->setUnquotedName('id')
->setTypeName(Types::INTEGER)
->create(),
)
->setPrimaryKeyConstraint(
PrimaryKeyConstraint::editor()
->setUnquotedColumnNames('id')
->create(),
)
->create();

$orders = Table::editor()
->setUnquotedName('orders')
->setColumns(
Column::editor()
->setUnquotedName('id')
->setTypeName(Types::INTEGER)
->create(),
Column::editor()
->setUnquotedName('article_id')
->setTypeName(Types::INTEGER)
->create(),
)
->setForeignKeyConstraints(
ForeignKeyConstraint::editor()
->setUnquotedName('articles_fk')
->setUnquotedReferencingColumnNames('article_id')
->setUnquotedReferencedTableName('articles')
->setUnquotedReferencedColumnNames('id')
->create(),
ForeignKeyConstraint::editor()
->setUnquotedName('articles_fk_2')
->setUnquotedReferencingColumnNames('article_id')
->setUnquotedReferencedTableName('articles')
->setUnquotedReferencedColumnNames('id')
->create(),
)
->create();

$ordersCompare = Table::editor()
->setUnquotedName('orders')
->setColumns(
Column::editor()
->setUnquotedName('id')
->setTypeName(Types::INTEGER)
->create(),
Column::editor()
->setUnquotedName('article_id')
->setTypeName(Types::INTEGER)
->create(),
)
->setForeignKeyConstraints(
ForeignKeyConstraint::editor()
->setUnquotedName('articles_fk')
->setUnquotedReferencingColumnNames('article_id')
->setUnquotedReferencedTableName('articles')
->setUnquotedReferencedColumnNames('id')
->create(),
)
->create();

$this->dropTableIfExists('orders');
$this->dropTableIfExists('articles');

$this->connection->createSchemaManager()
->createTable($articles);
$this->connection->createSchemaManager()
->createTable($orders);

$ordersActual = $this->schemaManager->introspectTable('orders');

$diff = $this->schemaManager->createComparator()
->compareTables($ordersActual, $ordersCompare);

self::assertFalse($diff->isEmpty());
self::assertCount(0, $diff->getAddedForeignKeys());
self::assertCount(1, $diff->getDroppedForeignKeys());
self::assertSame('articles_fk_2', $diff->getDroppedForeignKeys()[0]->getName());
}
}