Skip to content
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

Order of referenced child deletions not correct when referencing to itself #10348

Closed
Toflar opened this issue Dec 29, 2022 · 15 comments · Fixed by #10547 or #10913
Closed

Order of referenced child deletions not correct when referencing to itself #10348

Toflar opened this issue Dec 29, 2022 · 15 comments · Fixed by #10547 or #10913
Labels

Comments

@Toflar
Copy link

Toflar commented Dec 29, 2022

Bug Report

Q A
BC Break no
Version 2.14.0

Summary

The order in which the UoW wants to delete children is somehow missing a dependency when there is an association to the same class even if you specified cascade: ['remove'].
Given you have

  • Parent -> has an association to children with cascade: ['remove']
  • Child 1
  • Child 2 -> has an association to Child 1 with cascade: ['remove'].

Now you delete the parent.
Doctrine tries to remove Child 1 first instead of Child 2 causing a foreign constraint violation:

Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (test.child_entities, CONSTRAINT FK_DAEB194156A273CC FOREIGN KEY (origin_id) REFERENCES child_entities (id))

How to reproduce

I've created a test setup because I'm not familiar with the internals of ORM at all. So here we go:

  1. Clone https://github.com/Toflar/doctrine-orm-bug-reproducer
  2. Run composer install
  3. Adjust dummy mysql connection example in bootstrap.php.
  4. Run bin/doctrine orm:schema-tool:drop --force && bin/doctrine orm:schema-tool:create
  5. Run php test.php.

Expected behavior

It should not throw an exception. Doctrine should remove Child 2 first.

@derrabus derrabus added the Bug label Dec 29, 2022
@simonberger
Copy link
Contributor

simonberger commented Jan 2, 2023

This test is based on @Toflar ' s reproducer and can be picked up:
simonberger@2abff4c
I could also create a PR if requested. It fails with mysql but works with sqlite. I didn't test further.
It is no new bug and happened also in 2.11.

@Toflar
Copy link
Author

Toflar commented Jan 2, 2023

Thanks <3 Maybe sqlite is configured without foreign key support in the CI? 🤔

@simonberger
Copy link
Contributor

simonberger commented Jan 2, 2023

I just tested local on mac. This could be the reason. Maybe it even fails in CI. :)
I am interested to know and created the PR. We can close it again after seeing the results.

EDIT:
Sqlite succeeds also in the CI. MariaDB, PostgreSQL and MySQL all fail.

@mpdude
Copy link
Contributor

mpdude commented Feb 27, 2023

@Toflar could you please have a look at #10547?

I am not too confident it might fix this issue here, since I do not know if all the entities that need to be cascade-deleted are put into the \Doctrine\ORM\UnitOfWork::$entityDeletions list before the commit order is computed.

But anyway, you might want to give it a shot.

@mpdude
Copy link
Contributor

mpdude commented Feb 28, 2023

Ah, maybe this is #10548?

@Toflar
Copy link
Author

Toflar commented Feb 28, 2023

Hey @mpdude wow, the other PR is some impressive work! Maybe we can pick up @simonberger's reproducer (simonberger@2abff4c) in the CI as well, then we know if your PR fixes the problem :)

@simonberger
Copy link
Contributor

simonberger commented Mar 5, 2023

@mpdude Looks good that this issue is covered by your fix. I ran the tests on your branch and it succeeded.
I created a PR on your branch or just add it manually if you like.
mpdude#2

@mpdude
Copy link
Contributor

mpdude commented Mar 5, 2023

That's great news, although I am not sure if that's by incident. But I have this issue on my shortlist and think it can be fixed as well.

For the delete case, we need not look out for nullable associations (like we do at insert time), but find the associations that use SET NULL as candidates for breaking cycles.

@simonberger
Copy link
Contributor

While I don't have much knowledge about the implementation, the test at least looked senseful to me and should have some validity.

@mpdude
Copy link
Contributor

mpdude commented Mar 5, 2023

Definitely, it is! I did not want to put that into question, sorry if it sounded like that.

My thinking was that I see another problematic case pointed out by this issue here, and I think #10547 does not address this precisely enough. I’ll see if I can fine-tune the test a little to make it fail until that situation is addressed as well.

Thank you for the PR against my branch, that helps.

@simonberger
Copy link
Contributor

I also didn't read it like this and the tests are from toflar, I just converted them to phpunit.

@mpdude
Copy link
Contributor

mpdude commented Mar 6, 2023

I've taken a closer look at this and in fact, what you've found here is a variant of the deletion case from #10531:

graph LR;
    Child1 --> Parent;
    Child2 --> Parent;
    Child2 --> Child1;
Loading

Both associations (from Child to Parent as well as from Child to Child) are cascade: delete, so trying to delete the Parent instance will setup the entire graph for deletion.

All associations are nullable in the database, but that's of no relevance for the delete case, since the ORM does not currently use an "extra update" before DELETE to break cycles.

There is no database-level ON DELETE SET NULL, so the ORM will have to find a valid deletion order by itself.

This is, in fact, possible by working through Child2, Child1, Parent. But, the current 2.14.x code only looks at associations at the table level, deleting child rows before parent. It does not honor that a particular order must be followed between the entities of the child table itself. This is the #10531 issue, and it will be fixed by #10547.

mpdude added a commit to mpdude/doctrine2 that referenced this issue Mar 6, 2023
Co-authored-by: Matthias Pigulla <mp@webfactory.de>
@Toflar
Copy link
Author

Toflar commented Mar 6, 2023

Wohoo! Thanks so much for the investigation and the awesome work - that PR seems to solve a lot of issues! Kudos @mpdude! Let's hope your work can make it to a release soon!

@mpdude
Copy link
Contributor

mpdude commented Mar 6, 2023

Test case included at 3917692

@mpdude
Copy link
Contributor

mpdude commented Mar 6, 2023

Indeed there is another bug regarding the delete order, which I have filed as #10566

mpdude added a commit to mpdude/doctrine2 that referenced this issue Mar 11, 2023
Co-authored-by: Matthias Pigulla <mp@webfactory.de>
mpdude added a commit to mpdude/doctrine2 that referenced this issue Mar 11, 2023
Co-authored-by: Matthias Pigulla <mp@webfactory.de>
mpdude added a commit to mpdude/doctrine2 that referenced this issue May 31, 2023
mpdude added a commit to mpdude/doctrine2 that referenced this issue May 31, 2023
This is part of the series of issues fixed by doctrine#10547. In particular, the changes from doctrine#10566 were relevant.

See doctrine#10348 for the bug description.

Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
mpdude added a commit to mpdude/doctrine2 that referenced this issue May 31, 2023
This is part of the series of issues fixed by doctrine#10547. In particular, the changes from doctrine#10566 were relevant.

See doctrine#10348 for the bug description.

Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
mpdude added a commit to mpdude/doctrine2 that referenced this issue May 31, 2023
This is part of the series of issues fixed by doctrine#10547. In particular, the changes from doctrine#10566 were relevant.

See doctrine#10348 for the bug description.

Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
greg0ire added a commit that referenced this issue Jun 2, 2023
Add a test case to show #10348 has been fixed
@greg0ire greg0ire closed this as completed Aug 1, 2023
mpdude added a commit that referenced this issue Jan 12, 2024
…ion (#10913)

In order to resolve #10348, some changes were included in #10547 to improve the computed _delete_ order for entities. 

One assumption was that foreign key references with `ON DELETE SET NULL` or `... CASCADE` need not need to be taken into consideration when planning the deletion order, since the RDBMS would unset or cascade-delete such associations by itself when necessary. Only associations that do _not_ use RDBMS-level cascade handling would be sequenced, to make sure the referring entity is deleted before the referred-to one.

This assumption is wrong for `ON DELETE CASCADE`. The following examples give reasons why we need to also consider such associations, and in addition, we need to be able to deal with cycles formed by them.

In the following diagrams, `odc` means `ON DELETE CASCADE`, and `ref` is a regular foreign key with no extra `ON DELETE` semantics.

```mermaid
graph LR;
C-->|ref| B;
B-->|odc| A;
```

In this example, C must be removed before B and A. If we ignore the B->A dependency in the delete order computation, the result may not to be correct. ACB is not a working solution.

```mermaid
graph LR;
A-->|odc| B;
B-->|odc| A;
C-->|ref| B;
```

This is the situation in #10912. We have to deal with a cycle in the graph. C must be removed before A as well as B. If we ignore the B->A dependency (e.g. because we set it to "optional" to get away with the cycle), we might end up with an incorrect order ACB.

```mermaid
graph LR;
A-->|odc| B;
B-->|odc| A;
A-->|ref| C;
C-->|ref| B;
```

This example has no possible remove order. But, if we treat `odc` edges as optional, A -> C -> B would wrongly be deemed suitable.

```mermaid
graph LR;
A-->|ref| B;
B-->|odc| C;
C-->|odc| B;
D-->|ref| C;
```

Here, we must first remove A and D in any order; then, B and C in any order. If we treat one of the `odc` edges as optional, we might find the invalid solutions ABDC or DCAB.

#### Solution implemented in this PR

First, build a graph with a node for every to-be-removed entity, and edges for `ON DELETE CASCADE` associations between those entities. Then, use [Tarjan's algorithm](https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm) to find strongly connected components (SCCs) in this graph. The significance of SCCs is that whenever we remove one of the entities in a SCC from the database (no matter which one), the DBMS will immediately remove _all_ the other entities of that group as well.

For every SCC, pick one (arbitrary) entity from the group to represent all entities of that group. 

Then, build a second graph. Again we have nodes for all entities that are to be removed. This time, we insert edges for all regular (foreign key) associations and those with `ON DELETE CASCADE`. `ON DELETE SET NULL` can be left out. The edges are not added between the entities themselves, but between the entities representing the respective SCCs.

Also, for all non-trivial SCCs (those containing more than a single entity), add dependency edges to indicate that all entities of the SCC shall be processed _after_ the entity representing the group. This is to make sure we do not remove a SCC inadvertedly by removing one of its entities too early.

Run a topological sort on the second graph to get the actual delete order. Cycles in this second graph are a problem, there is no delete order.

Fixes #10912.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
5 participants