From 69b5bbb09ae302532c6f6800879490dcd5a2ef37 Mon Sep 17 00:00:00 2001
From: Erwan Rouchet <rouchet@teklia.com>
Date: Thu, 1 Jul 2021 16:16:28 +0200
Subject: [PATCH] Ensure child path unicity in add_parent when there are
 multiple parents

---
 arkindex/documents/models.py                  | 24 ++++---
 arkindex/documents/tasks.py                   |  6 +-
 .../documents/tests/test_edit_elementpath.py  | 20 ++++++
 .../element_move_with_children.sql            | 71 +++----------------
 .../element_move_without_child.sql            | 15 ++--
 5 files changed, 51 insertions(+), 85 deletions(-)

diff --git a/arkindex/documents/models.py b/arkindex/documents/models.py
index 387a81502f..c4f60c2093 100644
--- a/arkindex/documents/models.py
+++ b/arkindex/documents/models.py
@@ -181,7 +181,8 @@ class Element(IndexableModel):
             # No existing parent, adding new line
             parent_paths = [[]]
 
-        new_paths = []
+        # Set of (element_id, path, ordering) tuples, to ensure path unicity
+        new_paths = set()
         for parent_path in parent_paths:
 
             # Build new full path and check it does not already exist
@@ -194,10 +195,10 @@ class Element(IndexableModel):
                 raise ValueError('Cannot add a descendant as a parent')
 
             # Try to immediately insert
-            new_paths.append(ElementPath(
-                element=self,
-                path=path,
-                ordering=order,
+            new_paths.add((
+                self.id,
+                tuple(path),
+                order,
             ))
 
         # Update children's paths in a non optimized way as this is pretty rare
@@ -215,10 +216,10 @@ class Element(IndexableModel):
 
                 # Add new paths to create for the children
                 for parent_path in parent_paths:
-                    new_paths.append(ElementPath(
-                        element=child.element,
-                        path=parent_path + [parent.id] + base_path,
-                        ordering=child.ordering,
+                    new_paths.add((
+                        child.element_id,
+                        tuple(parent_path + [parent.id] + base_path),
+                        child.ordering,
                     ))
 
             # Clear old children paths, if any leftover
@@ -226,7 +227,10 @@ class Element(IndexableModel):
                 ElementPath.objects.filter(id__in=to_delete).delete()
 
         # All new paths are finally created
-        ElementPath.objects.bulk_create(new_paths)
+        ElementPath.objects.bulk_create(
+            ElementPath(element_id=element_id, path=path, ordering=ordering)
+            for element_id, path, ordering in new_paths
+        )
 
     def get_next_order(self, type):
         """
diff --git a/arkindex/documents/tasks.py b/arkindex/documents/tasks.py
index 74ea767bb8..9ec17df031 100644
--- a/arkindex/documents/tasks.py
+++ b/arkindex/documents/tasks.py
@@ -170,7 +170,7 @@ def worker_results_delete(corpus_id: str, version_id: str, parent_id: str) -> No
 
 @job('high')
 def move_element(source: Element, destination: Element) -> None:
-    paths = ElementPath.objects.filter(element_id=source.id)
-    for path in paths:
-        Element.objects.get(id=path.path[-1]).remove_child(source)
+    parents = Element.objects.filter(id__in=source.paths.values('path__last'))
+    for parent in parents:
+        parent.remove_child(source)
     source.add_parent(destination)
diff --git a/arkindex/documents/tests/test_edit_elementpath.py b/arkindex/documents/tests/test_edit_elementpath.py
index 1c1ea24019..26fdea474f 100644
--- a/arkindex/documents/tests/test_edit_elementpath.py
+++ b/arkindex/documents/tests/test_edit_elementpath.py
@@ -316,3 +316,23 @@ class TestEditElementPath(FixtureTestCase):
         for child in ('B', 'D', 'E', 'F'):
             with self.assertRaises(Element.DoesNotExist):
                 elements[child].refresh_from_db()
+
+    def test_add_many_parents(self):
+        """
+        Ensure that add_parent still works when adding a lot of parents.
+        We previously had issues that were not detected in unit tests because we were not testing with enough parents!
+        """
+        parent_names = [f'Parent {i}' for i in range(50)]
+        elements = build_tree(
+            {
+                'Child 1': ['Element'],
+                'Child 2': ['Element'],
+                'Element': parent_names,
+            },
+            corpus=self.corpus,
+            type=self.element_type,
+        )
+
+        self.check_parents(elements, 'Element', *[[parent_name] for parent_name in parent_names])
+        self.check_parents(elements, 'Child 1', *[[parent_name, 'Element'] for parent_name in parent_names])
+        self.check_parents(elements, 'Child 2', *[[parent_name, 'Element'] for parent_name in parent_names])
diff --git a/arkindex/sql_validation/element_move_with_children.sql b/arkindex/sql_validation/element_move_with_children.sql
index ce66cc7e13..d33b7a2f17 100644
--- a/arkindex/sql_validation/element_move_with_children.sql
+++ b/arkindex/sql_validation/element_move_with_children.sql
@@ -1,10 +1,3 @@
-SELECT "documents_elementpath"."id",
-       "documents_elementpath"."element_id",
-       "documents_elementpath"."path",
-       "documents_elementpath"."ordering"
-FROM "documents_elementpath"
-WHERE "documents_elementpath"."element_id" = '{source_id}'::uuid;
-
 SELECT "documents_element"."id",
        "documents_element"."created",
        "documents_element"."updated",
@@ -14,8 +7,10 @@ SELECT "documents_element"."id",
        "documents_element"."zone_id",
        "documents_element"."worker_version_id"
 FROM "documents_element"
-WHERE "documents_element"."id" = '{parent_id}'::uuid
-LIMIT 21;
+WHERE "documents_element"."id" IN
+        (SELECT U0."path"[array_length(U0."path", 1)]
+         FROM "documents_elementpath" U0
+         WHERE U0."element_id" = '{source_id}'::uuid);
 
 SAVEPOINT "{savepoints[0]}";
 
@@ -170,54 +165,6 @@ SELECT "documents_elementpath"."id",
 FROM "documents_elementpath"
 WHERE "documents_elementpath"."path" @ > ARRAY['{source_id}'::uuid]::uuid[];
 
-SELECT "documents_element"."id",
-       "documents_element"."created",
-       "documents_element"."updated",
-       "documents_element"."corpus_id",
-       "documents_element"."type_id",
-       "documents_element"."name",
-       "documents_element"."zone_id",
-       "documents_element"."worker_version_id"
-FROM "documents_element"
-WHERE "documents_element"."id" = '{children_ids[0]}'::uuid
-LIMIT 21;
-
-SELECT "documents_element"."id",
-       "documents_element"."created",
-       "documents_element"."updated",
-       "documents_element"."corpus_id",
-       "documents_element"."type_id",
-       "documents_element"."name",
-       "documents_element"."zone_id",
-       "documents_element"."worker_version_id"
-FROM "documents_element"
-WHERE "documents_element"."id" = '{children_ids[1]}'::uuid
-LIMIT 21;
-
-SELECT "documents_element"."id",
-       "documents_element"."created",
-       "documents_element"."updated",
-       "documents_element"."corpus_id",
-       "documents_element"."type_id",
-       "documents_element"."name",
-       "documents_element"."zone_id",
-       "documents_element"."worker_version_id"
-FROM "documents_element"
-WHERE "documents_element"."id" = '{children_ids[2]}'::uuid
-LIMIT 21;
-
-SELECT "documents_element"."id",
-       "documents_element"."created",
-       "documents_element"."updated",
-       "documents_element"."corpus_id",
-       "documents_element"."type_id",
-       "documents_element"."name",
-       "documents_element"."zone_id",
-       "documents_element"."worker_version_id"
-FROM "documents_element"
-WHERE "documents_element"."id" = '{children_ids[3]}'::uuid
-LIMIT 21;
-
 DELETE
 FROM "documents_elementpath"
 WHERE "documents_elementpath"."id" IN ('{paths_ids[0]}'::uuid,
@@ -229,10 +176,10 @@ INSERT INTO "documents_elementpath" ("id",
                                      "element_id",
                                      "path",
                                      "ordering")
-VALUES ('{paths_ids[4]}'::uuid, '{source_id}'::uuid, ARRAY['{destination_id}'::uuid]::uuid[], 3),
+VALUES ('{paths_ids[4]}'::uuid, '{children_ids[2]}'::uuid, ARRAY['{destination_id}'::uuid,'{source_id}'::uuid]::uuid[], 0),
        ('{paths_ids[5]}'::uuid, '{children_ids[0]}'::uuid, ARRAY['{destination_id}'::uuid,'{source_id}'::uuid]::uuid[], 1),
-       ('{paths_ids[6]}'::uuid, '{children_ids[1]}'::uuid, ARRAY['{destination_id}'::uuid,'{source_id}'::uuid]::uuid[], 2),
-       ('{paths_ids[7]}'::uuid, '{children_ids[2]}'::uuid, ARRAY['{destination_id}'::uuid,'{source_id}'::uuid]::uuid[], 0),
-       ('{paths_ids[8]}'::uuid, '{children_ids[3]}'::uuid, ARRAY['{destination_id}'::uuid,'{source_id}'::uuid]::uuid[], 0);
+       ('{paths_ids[6]}'::uuid, '{source_id}'::uuid, ARRAY['{destination_id}'::uuid]::uuid[], 3),
+       ('{paths_ids[7]}'::uuid, '{children_ids[3]}'::uuid, ARRAY['{destination_id}'::uuid,'{source_id}'::uuid]::uuid[], 0),
+       ('{paths_ids[8]}'::uuid, '{children_ids[1]}'::uuid, ARRAY['{destination_id}'::uuid,'{source_id}'::uuid]::uuid[], 2);
 
-RELEASE SAVEPOINT "{savepoints[1]}"
\ No newline at end of file
+RELEASE SAVEPOINT "{savepoints[1]}"
diff --git a/arkindex/sql_validation/element_move_without_child.sql b/arkindex/sql_validation/element_move_without_child.sql
index 4f40c8fef6..8074398184 100644
--- a/arkindex/sql_validation/element_move_without_child.sql
+++ b/arkindex/sql_validation/element_move_without_child.sql
@@ -1,10 +1,3 @@
-SELECT "documents_elementpath"."id",
-       "documents_elementpath"."element_id",
-       "documents_elementpath"."path",
-       "documents_elementpath"."ordering"
-FROM "documents_elementpath"
-WHERE "documents_elementpath"."element_id" = '{source_id}'::uuid;
-
 SELECT "documents_element"."id",
        "documents_element"."created",
        "documents_element"."updated",
@@ -14,8 +7,10 @@ SELECT "documents_element"."id",
        "documents_element"."zone_id",
        "documents_element"."worker_version_id"
 FROM "documents_element"
-WHERE "documents_element"."id" = '{parent_id}'::uuid
-LIMIT 21;
+WHERE "documents_element"."id" IN
+        (SELECT U0."path"[array_length(U0."path", 1)]
+         FROM "documents_elementpath" U0
+         WHERE U0."element_id" = '{source_id}'::uuid);
 
 SAVEPOINT "{savepoints[0]}";
 
@@ -104,4 +99,4 @@ INSERT INTO "documents_elementpath" ("id",
                                      "ordering")
 VALUES ('{path_id}'::uuid, '{source_id}'::uuid, ARRAY['{destination_id}'::uuid]::uuid[], 3);
 
-RELEASE SAVEPOINT "{savepoints[1]}"
\ No newline at end of file
+RELEASE SAVEPOINT "{savepoints[1]}"
-- 
GitLab