-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Fix schema comparison on tables with multiple foreign keys referencing the same tables and columns #7131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 4.3.x
Are you sure you want to change the base?
Conversation
ff8c27e to
7874d16
Compare
7874d16 to
1b07c48
Compare
|
Thank you. I've merged a workaround for the broken DB2 CI. I'd like to see your changes tested against DB2 as well which is why I've rebased your branch. I hope that was okay for you. 🙂 |
|
Thank you for your PR. That is an interesting edge case indeed. What about the following scenario: I have a database with two identical foreign keys and compare it to a configured schema with only one foreign key. Would the comparator detect this and as a consequence drop one of the foreign keys? Can we cover this with a test maybe? |
a5a1696 to
5a74880
Compare
|
I've added the test case, removed the DB2 skip. |
|
I'm guessing the DB2 skip is necessary? https://github.com/doctrine/dbal/actions/runs/17487261825/job/49669823130 |
5a74880 to
b17da07
Compare
|
The fix itself looks fine. What I didn't know is that the comparator apparently doesn't detect renamed foreign key constraints. It compares them only by value, and if they match, it will unset the corresponding array keys in the old and new arrays. Here's a slightly modified original test which reproduces this issue: $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_1')
->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');
$orders = $orders->edit()
->setForeignKeyConstraints(
ForeignKeyConstraint::editor()
->setUnquotedName('articles_fk_2')
->setUnquotedReferencingColumnNames('article_id')
->setUnquotedReferencedTableName('articles')
->setUnquotedReferencedColumnNames('id')
->create(),
)
->create();
self::assertFalse(
$this->schemaManager->createComparator()
->compareTables($ordersActual, $orders)
->isEmpty(),
);What it does is:
It is expected that there is a diff, but there is none. I'm fine with merging the fix that only addresses the original issue, but I'm curious if we should fix the root cause: the constraints should be compared by value and name (unless either of the old/new is unnamed), not just by value – that's why the original issue exists. |
| if (strtolower($oldForeignKey->getName()) === strtolower($newForeignKey->getName())) { | ||
| $droppedForeignKeys[$oldKey] = $oldForeignKey; | ||
| $addedForeignKeys[$newKey] = $newForeignKey; | ||
| continue 2; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| ); | ||
| } | ||
|
|
||
| public function testDoubleForeignKeyConstraintComparedToSingle(): void |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| ) | ||
| ->setForeignKeyConstraints( | ||
| ForeignKeyConstraint::editor() | ||
| ->setUnquotedName('articles_fk') |
There was a problem hiding this comment.
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?
4.3.2Summary
When attempting to run a single migration, I encountered the following MySQL error:
This error is not caused by the migration itself, but by a table name that was not properly escaped.
This query wasn't even defined in the migration, it was introduced during execution of the migration via the following code: DbalExecutor.php#L144
In this case, the
users-tabletable had two foreign keys (FK_redactedandFK_redacted_2) pointing to the same referenced table and columns.I implemented a small fix in the foreign key change detection and added a test case to cover this scenario.
Note: I acknowledge that double foreign keys and table names with
-symbols are not best practice, but I was working with a legacy database.