Problem/Motivation
When deleting the first subgroup in a hierarchy level, \Drupal\subgroup\Entity\SubgroupHandlerBase::getChildren returns no results. The reason is a relationship with "right" field on the query, and the right field having an unexpected value., the group leaf data can get incorrectly erased thus leading to broken subgroup hierarchies.
Steps to reproduce
(Partially copied from #3182801-2: An existing subgroup cannot be deleted)
- Created 2 group types Sub 1, and Sub 2 as a tree (Sub 1 is parent to Sub 2)
- Added group Test 1 of type Sub 1
-Aadded subgroup Test 2 of type Sub 2 to group Test 1
- Added subgroup Test 3 of type Sub 2 to group Test 1
- Call programatically to \Drupal\subgroup\Entity\SubgroupHandlerBase::getChildren() with "Test 1" as parameter. You will get 2 result: "Test 2" and "Test 3". Visit any subgroup listing (i.e. a view): you will see Test2 and Test 3.
- Delete Test 2
- Call programatically to \Drupal\subgroup\Entity\SubgroupHandlerBase::getChildren() with "Test 1" as parameter. You will get an empty result.
Proposed resolution
As a woraround, we are removing the "right" condition on the query, but we should check why the right field has a wrong value.
Don't rely on the child "left" and "right" values to undo a tree structure.
See comment #4
The problem is related to this code on function doRemoveLeaf() on /src/Entity/SubgroupHandlerBase.php
// If the left and right values are 2 and 3 respectively it means we're
// removing the last child of a tree root. In this case, we unset the tree
// altogether.
if ($leaf->getLeft() === 2 && $leaf->getRight() === 3) {
$root = $this->storage->load($leaf->getTree());
$this->clearLeafData($root, TRUE);
}
The code states that "If the left and right values are 2 and 3 respectively it means we're removing the last child", but this is not true: it means we are removing the first
So, every time the first subgroup of a tree is removed, it will wipe the structure of the whole tree leading to unexpected results if there were any siblings still alive.
The proposed resolution is either to check wether there are more siblings left before calling clearLeafData or check the presence of descendants of the root
Remaining tasks
Test and review.
Fix failing tests.
User interface changes
None
API changes
None
Data model changes
None
| Comment | File | Size | Author |
|---|---|---|---|
| #25 | subgroup-3198179-25.patch | 1.78 KB | daneduijnkerke |
| #20 | subgroup-3198179-20.patch | 7.35 KB | kristiaanvandeneynde |
Issue fork subgroup-3198179
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
idiaz.ronceroHello;
I've found something interesting related to this issue.
Given a Group entity and three Subgroups of this group, after its creation they have the following data:
PARENT
CHILD
The desired structure is as follows: one parent (62), three children on the same level (63, 64, 65)
As you can see, every child has the correct depth and tree values.
If you start eliminating them from 65 up to 63, everything's fine.
But if you start from the group with ID 63 the whole tree will break because the left and (especially) right values will change in a way that methods like getChildren, hasDescendant, etc... won't work anymore.
This seems to be related to this code on
function doRemoveLeaf()on/src/Entity/SubgroupHandlerBase.phpThe code states that "If the left and right values are 2 and 3 respectively it means we're removing the last child", but this is false, at least with the previous data, since there are at least two siblings left (groups 64 and 65) and yet the clearLeafData method is called on the root. This is "wiping" the tree data even if we just deleted a single children that happened to be the first one to be created.
So, it seems to me that either the children's left and right data are wrong or the previous code is making a false assumtpion (that the user will delete the groups in the same order as they were created).
Comment #6
idiaz.ronceroI've added a new patch/MR with an alternate solution that handles the origin of the malformed data.
For reference, those are the steps that I followed to solve the bug on a clean D9 install and my reasoning of the solution:
1- Create a group and four subgroups
Result:
The Group parent has the following values:
The Childs have the following values.
This behavior is correct and establishes a tree. Note that the parent's Right value equals to the last child's Right value + 1.
2- Remove the last child (ID 5)
Result:
The Group parent has the following values:
The Childs have the following values.
This behavior is correct and the parent's Right value has been updated to reflect the new last child's Right value + 1. Now, the parent "embraces" subgroups from 0 (left) to 8 (right).
3- Remove the first child (GID 2)
Result:
The Group parent has the following values:
The Childs have the following values.
This behavior is wrong and is the source of the problem. The parent's tree data has been wiped so, from now on, all calls to handler methods like hasDescendants, isParent, etc... will fail.
As I said on the previous comment, the problem seems to be related with the detection of wether the removed Leaf of a tree is the last one. The code is relying on a comparison of
($leaf->getLeft() === 2 && $leaf->getRight() === 3), which implies that the user won't start by deleting the first created child (in my example, GID 2).The patch addresses this by changing to this comparison:
I have checked that, when this code executes, the hasDescendants() method correctly returns TRUE or FALSE depending on the remaining groups (in this point, the deleted group is not included on hasDescendants() and won't throw a false positive).
This allows the user to delete subgroups in any order, but it also introduces the need of an additional query (so it has a performance impact) and I haven't tested on more complex trees, with more nesting levels.
Comment #7
idiaz.ronceroComment #8
idiaz.ronceroTests are failing, i will check them as soon as possible.
Comment #9
idiaz.ronceroComment #12
idiaz.ronceroUpdated the title to reflect the actual problem.
Comment #13
idiaz.ronceroComment #14
kristiaanvandeneyndeYup, this is a bug and it only manifests itself when the leftmost child has no descendants. Otherwise, it would never have values 2 and 3. This is an oversight and needs to be fixed ASAP, good catch!
What needs to happen here is:
I'm hesitant about the update hook because if the original parent group was used again as a root, all hell breaks loose. So I'd rather put a code sample in a change record instead. Any suggestions would be welcome :)
Comment #15
kristiaanvandeneyndeHere's an idea. Should still be performant (because no entities loaded) and we still rely on left-2/right-3 values to save some processing power too.
Didn't use the fork because I didn't want to completely remove your work in a single commit and I can't quickly figure out how to have multiple forks for one issue. Needs tests for new method and to prove the broken scenario works now.
Comment #17
kristiaanvandeneyndeThink I figured the forks out, but doing patch just for now (so tests run during lunch).
Comment #18
kristiaanvandeneyndeThis adds tests, and reverses the test for last child removal so the leftmost child is removed first and the rightmost (now leftmost) child second. This makes sure that a left-2/right-3 child does not trigger a tree destruction unless it's the last descendant of its root.
Comment #20
kristiaanvandeneyndeForgot about stale entity properties in tests
Comment #22
kristiaanvandeneyndeWill tag a new release as this is important enough to warrant one.
Comment #23
idiaz.ronceroNice to see this solved!! My patch was mainly a workaround, but had no time to investigate a better solution. Many thanks!
Comment #25
daneduijnkerke commentedI think this fix does not work anymore. I'm not sure why or how but we should check for a descendants count of 0, not 1. When I create 1 group with 2 subgroups and specifically delete the first subgroup. My tree gets broken because it triggers clearLeafData(). We only want this to trigger when there are no children in the root. Here is a new patch: