Skip to content

Deprecate overwriting existing foreign key constraints#7125

Merged
morozov merged 1 commit intodoctrine:4.4.xfrom
santysisi:fix/duplicate-unnamed-fk-constraint-overwrite
Sep 17, 2025
Merged

Deprecate overwriting existing foreign key constraints#7125
morozov merged 1 commit intodoctrine:4.4.xfrom
santysisi:fix/duplicate-unnamed-fk-constraint-overwrite

Conversation

@santysisi
Copy link
Copy Markdown
Contributor

@santysisi santysisi commented Aug 31, 2025

Q A
Type bug
Fixed issues #7123

This PR introduces a deprecation notice when overwriting an existing foreign key constraint.

Summary

  • Adds a deprecation warning when a foreign key constraint with the same name already exists.
  • In Doctrine DBAL 5.0, this behavior will throw an exception instead of silently overwriting the constraint.

@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch from 10eac5c to bb86c8c Compare August 31, 2025 19:14
@santysisi
Copy link
Copy Markdown
Contributor Author

santysisi commented Aug 31, 2025

status: needs work

I think it's better to move that logic into the _addForeignKeyConstraint method.

@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch 3 times, most recently from 1d0aec6 to f6f54de Compare August 31, 2025 20:16
@santysisi
Copy link
Copy Markdown
Contributor Author

status: needs review

I ended up adding the logic to both _addForeignKeyConstraint and addForeignKeyConstraint.

Why did I do this?
The _addForeignKeyConstraint method is called by both addForeignKeyConstraint and _construct. If I only modified addForeignKeyConstraint, it wouldn't impact the behavior in _construct.

So why modify addForeignKeyConstraint as well, if the logic already exists in _addForeignKeyConstraint?
That’s because addForeignKeyConstraint creates a ForeignKeyConstraint with a name. For the logic in _addForeignKeyConstraint to take effect, the ForeignKeyConstraint must not have a name set. But since addForeignKeyConstraint assigns one, the check in _addForeignKeyConstraint would never trigger.

One possible solution could be to remove the name generation in addForeignKeyConstraint, but I believe that would be a breaking change (BC).

Please let me know if this solution is incorrect or if any changes are needed.
Thanks again! 😄

@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch from f6f54de to 09bc7da Compare August 31, 2025 20:22
@santysisi
Copy link
Copy Markdown
Contributor Author

Maybe I'm wrong, but I think the pipeline errors aren't related to this PR

@santysisi
Copy link
Copy Markdown
Contributor Author

should this PR be targeting the 4.3.x branch instead? 🤔

@morozov
Copy link
Copy Markdown
Member

morozov commented Sep 3, 2025

Maybe I'm wrong, but I think the pipeline errors aren't related to this PR

Yes, this is unrelated. Sorry about the inconvenience.

should this PR be targeting the 4.3.x branch instead? 🤔

That sounds right. You can change the base branch of this PR.

@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch from 09bc7da to f62278a Compare September 3, 2025 01:19
@santysisi santysisi changed the base branch from 4.4.x to 4.3.x September 3, 2025 01:19
@santysisi
Copy link
Copy Markdown
Contributor Author

Base branch changed to 4.3.x 😄

@santysisi
Copy link
Copy Markdown
Contributor Author

santysisi commented Sep 5, 2025

Hey! 👋 I saw this PR and went ahead and updated the tests to use Table::editor() where appropriate.

@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch 2 times, most recently from 64bae6c to 07d8eb8 Compare September 6, 2025 19:24
@morozov
Copy link
Copy Markdown
Member

morozov commented Sep 10, 2025

@santysisi, thanks for the PR. Could you let us know whether it was prepared with the help of an AI tool, and if so, to what extent? This will helps us better understand how to approach reviewing it.

@santysisi
Copy link
Copy Markdown
Contributor Author

Hi @morozov, thanks for your comment! Yes, I used an AI tool to help with writing comments (including PHPDoc) and to suggest some method and variable names. This comment is also written with the help of AI, as I'm still working on improving my English, and these tools help me communicate more clearly.

Please let me know if there's anything you'd like me to revise. I'm happy to make changes!

Comment thread src/Schema/Table.php Outdated
Comment thread tests/Schema/TableTest.php Outdated
Comment thread tests/Schema/TableTest.php Outdated
@santysisi
Copy link
Copy Markdown
Contributor Author

Hi @morozov, thanks a lot for taking the time to review the PR, I really appreciate it! 😊

If I understood correctly, the idea now is to trigger a deprecation notice only in case of a collision, but still continue with the current behavior (i.e., overwrite).
Could you please confirm that? 🙏

If so, I can update the PR this weekend to reflect that behavior.

Thanks again! ❤️

@morozov
Copy link
Copy Markdown
Member

morozov commented Sep 12, 2025

the idea now is to trigger a deprecation notice only in case of a collision, but still continue with the current behavior (i.e., overwrite).

Correct. This will become an exception in 5.0.x.

@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch from 07d8eb8 to 7807eb3 Compare September 14, 2025 22:41
@santysisi santysisi changed the base branch from 4.3.x to 4.4.x September 14, 2025 22:42
@santysisi santysisi changed the title Fix unnamed foreign keys overriding each other Deprecate overwriting existing foreign key constraints Sep 14, 2025
@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch from 7807eb3 to fa759c8 Compare September 14, 2025 22:45
@santysisi
Copy link
Copy Markdown
Contributor Author

Hi! 😄
I've updated the PR to trigger a deprecation. Additionally, I've changed the base branch, as well as the PR title and description.

@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch from fa759c8 to 5cedffd Compare September 14, 2025 23:50
Copy link
Copy Markdown
Member

@morozov morozov left a comment

Choose a reason for hiding this comment

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

We're getting close. Besides a few more comments, looks good.

Comment thread src/Schema/Table.php Outdated
Comment thread tests/Schema/TableTest.php Outdated
Comment thread UPGRADE.md Outdated
@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch from 5cedffd to 5cd827b Compare September 15, 2025 21:02
@santysisi
Copy link
Copy Markdown
Contributor Author

Thanks a lot for your suggestions, the changes have been made

Comment thread UPGRADE.md Outdated
Comment on lines +13 to +14
Adding a foreign key constraint with a name that matches an existing one, whether explicitly specified or
auto-generated, has been deprecated.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What should I do instead?

Copy link
Copy Markdown
Member

@morozov morozov Sep 16, 2025

Choose a reason for hiding this comment

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

Good question. In order to replace an existing constraint, drop it first via dropForeignKey().

@santysisi please add this to the upgrade notes.

Overwriting an existing foreign key constraint is now deprecated
This behavior will no longer be allowed in Doctrine DBAL 5.0, where
an exception will be thrown instead of silently overwriting the
constraint.
@santysisi santysisi force-pushed the fix/duplicate-unnamed-fk-constraint-overwrite branch from 5cd827b to 2e5dbaf Compare September 16, 2025 22:22
@morozov morozov merged commit 673492c into doctrine:4.4.x Sep 17, 2025
96 checks passed
@morozov
Copy link
Copy Markdown
Member

morozov commented Sep 17, 2025

Thanks, @santysisi. Congrats on your first contribution!

@santysisi
Copy link
Copy Markdown
Contributor Author

Thanks for your help! 😄

@morozov morozov added the Bug label Sep 18, 2025
@morozov
Copy link
Copy Markdown
Member

morozov commented Sep 18, 2025

@santysisi, I merged the changes from this and your other PR up into 5.0.x. If you're up for it, you can convert these deprecations to exceptions.

@santysisi
Copy link
Copy Markdown
Contributor Author

santysisi commented Sep 18, 2025

Yep! 😄
I'll be opening those PRs today ❤️

Edit: I don't have enough time today 😔, I will try to do it this weekend

@santysisi
Copy link
Copy Markdown
Contributor Author

Hi @morozov,

While working on the new PR (to replace the deprecation with throw and error), I encountered an issue (reproducible with SQLite — I’ve tested with SQLite and PostgreSQL only so far) in this method getForeignKeysInAlteredTable.

The problem stems from this change: #7116, specifically this part:
https://github.com/doctrine/dbal/blob/5.0.x/src/Schema/Table.php#L707-L710

Previously, we used unset($foreignKeys[$constraintName]), but since the method now returns a list, unsetting by key doesn’t work anymore.

Before proceeding with the final deprecation removal and introducing the new error, I think we should address this issue first.

I’d be happy to take a shot at fixing it! 😄

I see two possible approaches:

Adjust the existing method:
Keep the current method that returns a list, but calculate the index of the constraint in the list and unset() by position. Alternatively, rebuild the array to use the constraint names as keys.

Introduce a new method:
Add a separate method that returns the foreign keys as an associative array (like before), without modifying the current list-based method.

Let me know what you think!

@santysisi
Copy link
Copy Markdown
Contributor Author

Just as an example — if we don’t want to introduce a new method that returns an associative array with constraint names as keys, we could handle it like this:

$foreignKeys = [];
foreach ($oldTable->getForeignKeys() as $key => $foreignKey) {
    $constraintName = $foreignKey->getObjectName();
    
    if ($constraintName === null) {
        $foreignKeys[$key] = $foreignKey;
        continue;
    }

    $foreignKeys[strtolower($constraintName->getIdentifier()->getValue())] = $foreignKey;
}

This way, we preserve the ability to reference foreign keys by their constraint names (like before), while still using the current method that returns a list.

It’s a simple alternative to avoid breaking changes or adding a new method, instead of just doing:

$foreignKeys = $oldTable->getForeignKeys();

Another alternative could be a deeper refactor 🤔

@morozov
Copy link
Copy Markdown
Member

morozov commented Sep 21, 2025

Previously, we used unset($foreignKeys[$constraintName]), but since the method now returns a list, unsetting by key doesn’t work anymore.

Yeah, I noticed this issue as well. Indexing by name makes sense. The only thing is that your code still mixes name-based and positional keys, which is prone to conflicts (0 could be both a position of an unnamed constraint and a name of a named one).

Instead, you could introduce and populate a map of constraint names to their numeric keys in the $foreignKeys array (e.g. $keysByName) – only for those constraints that have a name. Then, while processing dropped FKs:

  1. Assert that it has a name
  2. Check if there's an entry in $keysByName for its name
  3. If there is, unset the corresponding key in $foreignKeys

janedbal added a commit to janedbal/orm that referenced this pull request Mar 10, 2026
When multiple STI child entities have a ManyToOne relation to the same
target entity using the same join column, SchemaTool called
addForeignKeyConstraint twice with the same constraint name, triggering
a DBAL deprecation ("Overwriting an existing foreign key constraint").

The fix skips the redundant addForeignKeyConstraint call when an
identical FK (same local columns, same foreign table, same foreign
columns) was already added.

See doctrine/dbal#7125
janedbal added a commit to janedbal/orm that referenced this pull request Apr 22, 2026
When multiple STI child entities have a ManyToOne relation to the same
target entity using the same join column, SchemaTool called
addForeignKeyConstraint twice with the same constraint name, triggering
a DBAL deprecation ("Overwriting an existing foreign key constraint").

The fix skips the redundant addForeignKeyConstraint call when an
identical FK (same local columns, same foreign table, same foreign
columns) was already added.

See doctrine/dbal#7125
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants