Repository: edgedb/edgedb

Issue:
Populate migration to empty fails
<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: 1.0-rc.3+2b303d3
- OS Version: macOS

Steps to Reproduce:

1. Apply the schema.
2. Run the following:

```
edgedb> start migration to {}; populate migration; DESCRIBE CURRENT MIGRATION; commit migration;
OK: START MIGRATION
edgedb error: SchemaDefinitionError: cannot alter property 'name' of object type 'default::Card' because this affects expression of property 'tag' of link 'avatar' of object type 'default::User'
  Detail: reference to a non-existent schema item e1e49cd0-5e02-11ec-9a87-c58a1362ecd1 in schema <FlatSchema gen:1 at 0x11856ae60>
```

<!-- If the issue is about a query error, please also provide your schema -->
Schema:

https://github.com/edgedb/edgedb/blob/5a1d973772ef9c116249a52fd04e595a6121d805/tests/schemas/cards.esdl


Discussion:
As a workaround, I have to run the following in order to have `START MIGRATION TO {} ...` succeed (with #3279):

```
ALTER TYPE User {
    DROP LINK avatar;
};
```
I have the failing initial schema reduced to
```
abstract type Named {
    required property name -> str {
        delegated constraint exclusive;
    }
}

type User {
    link avatar -> Card {
        property text -> str;
        property tag := .name ++ (("-" ++ @text) ?? "");
    }
}

type Card extending Named;

type SpecialCard extending Card;
```
And migrating to the same schema but without the exclusive constraint suffices to trigger the bug

Resolving patch:
diff --git a/edb/schema/ordering.py b/edb/schema/ordering.py
--- a/edb/schema/ordering.py
+++ b/edb/schema/ordering.py
@@ -130,12 +130,13 @@ def reconstruct_tree(
     # a tuple of indexes where each index represents relative
     # position within the tree rank.
     offsets: Dict[sd.Command, Tuple[int, ...]] = {}
-    # Object commands indexed by command type and object name,
-    # where each entry represents the latest seen command of the type
-    # for a particular object.  Implicit commands are not included in
-    # this mapping.
+    # Object commands indexed by command type and object name and
+    # implicitness, where each entry represents the latest seen
+    # command of the type for a particular object.  Implicit commands
+    # are included, but can only be attached to by other implicit
+    # commands.
     opindex: Dict[
-        Tuple[Type[sd.ObjectCommand[so.Object]], sn.Name],
+        Tuple[Type[sd.ObjectCommand[so.Object]], sn.Name, bool],
         sd.ObjectCommand[so.Object]
     ] = {}
 
@@ -194,8 +195,8 @@ def attach(
 
         for i in range(slice_start, len(opbranch)):
             op = opbranch[i]
-            if isinstance(op, sd.ObjectCommand) and not as_implicit:
-                ancestor_key = (type(op), op.classname)
+            if isinstance(op, sd.ObjectCommand):
+                ancestor_key = (type(op), op.classname, as_implicit)
                 opindex[ancestor_key] = op
 
             if op in offsets:
@@ -234,7 +235,7 @@ def maybe_replace_preceding(
             # ALTER isn't even defined for this object class, bail.
             return False
 
-        alter_key = ((alter_cmd_cls), op.classname)
+        alter_key = ((alter_cmd_cls), op.classname, False)
         alter_op = opindex.get(alter_key)
         if alter_op is None:
             # No preceding ALTER, bail.
@@ -262,7 +263,7 @@ def maybe_replace_preceding(
         attached_root = parents[alter_op]
         attached_root.replace(alter_op, op)
         opindex[alter_key] = op
-        opindex[type(op), op.classname] = op
+        opindex[type(op), op.classname, False] = op
         offsets[op] = offsets[alter_op]
         parents[op] = attached_root
 
@@ -296,7 +297,12 @@ def maybe_attach_to_preceding(
 
         for candidate in parent_candidates:
             for op_type in allowed_op_types:
-                parent_op = opindex.get((op_type, candidate))
+                parent_op = opindex.get((op_type, candidate, False))
+                # implicit ops are allowed to attach to other implicit
+                # ops. (Since we want them to chain properly in
+                # inheritance order.)
+                if parent_op is None and as_implicit:
+                    parent_op = opindex.get((op_type, candidate, True))
 
                 if (
                     parent_op is not None


Test patch:
diff --git a/tests/test_edgeql_data_migration.py b/tests/test_edgeql_data_migration.py
--- a/tests/test_edgeql_data_migration.py
+++ b/tests/test_edgeql_data_migration.py
@@ -10326,6 +10326,106 @@ async def test_edgeql_migration_inheritance_to_empty_01(self):
 
         await self.migrate('')
 
+    async def test_edgeql_migration_inheritance_to_empty_02(self):
+        await self.migrate(r'''
+            abstract type Named {
+                required property name -> str {
+                    delegated constraint exclusive;
+                }
+            }
+
+            type User {
+                link avatar -> Card {
+                    property text -> str;
+                    property tag := .name ++ (("-" ++ @text) ?? "");
+                }
+            }
+
+            type Card extending Named;
+
+            type SpecialCard extending Card;
+        ''')
+
+        await self.migrate('')
+
+    async def test_edgeql_migration_drop_constraint_01(self):
+        await self.migrate(r'''
+            abstract type Named {
+                required property name -> str {
+                    delegated constraint exclusive;
+                }
+            }
+
+            type User {
+                link avatar -> Card {
+                    property text -> str;
+                    property tag := .name ++ (("-" ++ @text) ?? "");
+                }
+            }
+
+            type Card extending Named;
+
+            type SpecialCard extending Card;
+        ''')
+
+        await self.migrate(r'''
+            abstract type Named {
+                required property name -> str;
+            }
+
+            type User {
+                link avatar -> Card {
+                    property text -> str;
+                    property tag := .name ++ (("-" ++ @text) ?? "");
+                }
+            }
+
+            type Card extending Named;
+
+            type SpecialCard extending Card;
+        ''')
+
+    async def test_edgeql_migration_drop_constraint_02(self):
+        await self.migrate(r'''
+            abstract type Named {
+                required property name -> str {
+                    delegated constraint exclusive;
+                }
+            }
+
+            type User {
+                link avatar -> Card {
+                    property text -> str;
+                    property tag := .name ++ (("-" ++ @text) ?? "");
+                }
+            }
+
+            type Card extending Named;
+
+            type SpecialCard extending Card;
+            type SpecialCard2 extending Card;
+            type VerySpecialCard extending SpecialCard, SpecialCard2;
+        ''')
+
+        await self.migrate(r'''
+            abstract type Named {
+                required property name -> str;
+            }
+
+            type User {
+                link avatar -> Card {
+                    property text -> str;
+                    property tag := .name ++ (("-" ++ @text) ?? "");
+                }
+            }
+
+            type Card extending Named;
+
+            type SpecialCard extending Card;
+            type SpecialCard2 extending Card;
+            type VerySpecialCard extending SpecialCard, SpecialCard2;
+        ''')
+
 
 class TestEdgeQLDataMigrationNonisolated(tb.DDLTestCase):
     TRANSACTION_ISOLATION = False