Repository: edgedb/edgedb

Issue:
InvalidReferenceError when migrating to an empty schema
- EdgeDB Version: `2.8`
- EdgeDB CLI Version: Irrelevant
- OS Version: Irrelevant

Steps to Reproduce:

1. Migrate to the schema below using `start migration to ...; populate migration; commit migration;`.
2. Migrate to an empty schema in the same way.
3. If step 2 does not fail, retry steps 1 and 2. This bug may be nondeterministic.
4. At some iteration, step 2 will fail with `InvalidReferenceError: constraint 'std::exclusive' does not exist`.

This is a minimal, or near-minimal, reproduction.

Schema:

```
module default {     
    abstract type A;
    
    abstract type B {
        link z -> G {
            constraint exclusive;
        }
    }
    
    abstract type C extending B {
        link y -> H {
            constraint exclusive;
        }
    }
    
    abstract type D;
    
    type E extending D;
    
    type F extending C, B, D;
    
    type G extending D, A {
        link x := assert_single(.<z[IS B]);
    }
    
    type H extending B, D, A {
        link w := assert_single(.<y[IS C]);
    }
}
```



Discussion:
Further reduced version that I've found:
Start with schema
```
module default {
    abstract type A;
    abstract type B {
        link z -> A;
    };
    abstract type C extending B;
    abstract type D;
    type F extending C, B;
};
```
and run
```alter type B drop link z;```
produces
```link 'z' does not exist```

Which notably is not the same error as reported, but is the one I got when I tried...


Resolving patch:
diff --git a/edb/schema/delta.py b/edb/schema/delta.py
--- a/edb/schema/delta.py
+++ b/edb/schema/delta.py
@@ -194,7 +194,7 @@ def can_delete(obj: so.Object_T, name: sn.Name) -> bool:
             # how the delta in child objects is treated.
             order_x = cast(
                 Iterable[so.Object_T],
-                _sort_by_inheritance(
+                sort_by_inheritance(
                     new_schema,
                     cast(Iterable[so.InheritingObject], comparison_map),
                 ),
@@ -265,7 +265,7 @@ def can_delete(obj: so.Object_T, name: sn.Name) -> bool:
     deleted = old - {y for _, y in alter_pairs}
 
     if issubclass(sclass, so.InheritingObject):
-        deleted_order = _sort_by_inheritance(  # type: ignore
+        deleted_order = sort_by_inheritance(  # type: ignore[assignment]
             old_schema,
             cast(Iterable[so.InheritingObject], deleted),
         )
@@ -286,15 +286,15 @@ def can_delete(obj: so.Object_T, name: sn.Name) -> bool:
     return delta
 
 
-def _sort_by_inheritance(
+def sort_by_inheritance(
     schema: s_schema.Schema,
     objs: Iterable[so.InheritingObjectT],
-) -> Iterable[so.InheritingObjectT]:
+) -> Tuple[so.InheritingObjectT, ...]:
     graph = {}
     for x in objs:
         graph[x] = topological.DepGraphEntry(
             item=x,
-            deps=ordered.OrderedSet(x.get_bases(schema).objects(schema)),
+            deps=ordered.OrderedSet(x.get_ancestors(schema).objects(schema)),
             extra=False,
         )
 
diff --git a/edb/schema/referencing.py b/edb/schema/referencing.py
--- a/edb/schema/referencing.py
+++ b/edb/schema/referencing.py
@@ -1459,7 +1459,13 @@ def _propagate_ref_deletion(
                     details=f'{vn} is inherited from:\n- {pnames}'
                 )
 
-        for child in referrer.children(schema):
+        # Sort the children by reverse inheritance order amongst them.
+        # So if we are T and have children A and B and A <: B, we want to
+        # process A first, since we need to rebase it away from T, and then
+        # dropping A will also drop B.
+        for child in reversed(
+            sd.sort_by_inheritance(schema, referrer.children(schema))
+        ):
             assert isinstance(child, so.QualifiedObject)
             child_coll = child.get_field_value(schema, refdict.attr)
             fq_refname_in_child = self._classname_from_name(


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -7489,6 +7489,121 @@ def test_schema_migrations_except_01(self):
             """,
         ])
 
+    def test_schema_migrations_half_diamonds_00(self):
+        self._assert_migration_equivalence([
+            r"""
+                abstract type A;
+
+                abstract type B {
+                    link z -> G {
+                        constraint exclusive;
+                    }
+                }
+
+                abstract type C extending B {
+                    link y -> H {
+                        constraint exclusive;
+                    }
+                }
+
+                abstract type D;
+
+                type E extending D;
+
+                type F extending C, B, D;
+
+                type G extending D, A {
+                    link x := assert_single(.<z[IS B]);
+                }
+
+                type H extending B, D, A {
+                    link w := assert_single(.<y[IS C]);
+                }
+            """,
+            r"""
+            """,
+        ])
+
+    def test_schema_migrations_half_diamonds_01(self):
+        self._assert_migration_equivalence([
+            r"""
+                abstract type A;
+                abstract type B {
+                    link z -> A;
+                };
+                abstract type C extending B;
+                abstract type D;
+                type F extending C, B;
+            """,
+            r"""
+            """,
+        ])
+
+    def test_schema_migrations_half_diamonds_02(self):
+        self._assert_migration_equivalence([
+            r"""
+                abstract type A;
+                abstract type B {
+                    link z -> A;
+                };
+                abstract type C extending B;
+                abstract type C2 extending B;
+                abstract type D;
+                type F extending C, C2, B;
+            """,
+            r"""
+            """,
+        ])
+
+    def test_schema_migrations_half_diamonds_03(self):
+        self._assert_migration_equivalence([
+            r"""
+                abstract type A;
+                abstract type B {
+                    link z -> A;
+                };
+                abstract type C extending B;
+                abstract type C2 extending C;
+                abstract type D;
+                type F extending C, C2, B;
+            """,
+            r"""
+            """,
+        ])
+
+    def test_schema_migrations_half_diamonds_04(self):
+        self._assert_migration_equivalence([
+            r"""
+                abstract type A;
+                abstract type B {
+                    link z -> A;
+                };
+                abstract type C extending B;
+                abstract type C2 extending C;
+                abstract type D;
+                type F extending C2, B;
+            """,
+            r"""
+            """,
+        ])
+
+    def test_schema_migrations_half_diamonds_05(self):
+        self._assert_migration_equivalence([
+            r"""
+                abstract type A;
+                abstract type B {
+                    link z -> A;
+                };
+                abstract type C extending B;
+                abstract type C2 extending C;
+                abstract type D;
+                type F extending C, C2, B;
+                type F2 extending C, C2, B, F;
+            """,
+            r"""
+            """,
+        ])
+
 
 class TestDescribe(tb.BaseSchemaLoadTest):
     """Test the DESCRIBE command."""