Repository: edgedb/edgedb

Issue:
Migration with cardinality dependency in default
With schema:

```
type C {
    required property val -> str {
        constraint exclusive;
    }
}

type ROLinksA {
    required link rol1 -> C {
        readonly := True;
        default := (SELECT C FILTER .val = 'D00');
    }
}
```

Migration fails:

```
edgedb> start migration to { module default {} }; populate migration; DESCRIBE CURRENT MIGRATION; commit migration;
OK: START MIGRATION
OK: POPULATE MIGRATION
{
  'ALTER TYPE default::C {
    ALTER PROPERTY val {
        DROP CONSTRAINT std::exclusive;
    };
};
DROP TYPE default::ROLinksA;
DROP TYPE default::C;
DROP MODULE test;',
}
edgedb error: SchemaDefinitionError: cannot alter property 'val' of object type 'default::C' because this affects default expression of link 'rol1' of object type 'default::ROLinksA'
  Detail: possibly more than one element returned by the default expression for link 'rol1' declared as 'single'
```

It would be nice if the populated migration could handle similar situations.


Discussion:

Resolving patch:
diff --git a/edb/edgeql/compiler/inference/cardinality.py b/edb/edgeql/compiler/inference/cardinality.py
--- a/edb/edgeql/compiler/inference/cardinality.py
+++ b/edb/edgeql/compiler/inference/cardinality.py
@@ -945,10 +945,11 @@ def extract_exclusive_filters(
     filter_set: irast.Set,
     scope_tree: irast.ScopeTreeNode,
     ctx: inference_context.InfCtx,
-) -> Iterator[Tuple[Tuple[s_pointers.Pointer, irast.Set], ...]]:
+) -> List[Tuple[Tuple[s_pointers.Pointer, irast.Set], ...]]:
 
     filtered_ptrs = extract_filters(result_set, filter_set, scope_tree, ctx)
 
+    results: List[Tuple[Tuple[s_pointers.Pointer, irast.Set], ...]] = []
     if filtered_ptrs:
         filtered_ptrs_map = dict(filtered_ptrs)
         schema = ctx.env.schema
@@ -958,17 +959,25 @@ def extract_exclusive_filters(
         for ptr, expr in filtered_ptrs:
             ptr = ptr.get_nearest_non_derived_parent(schema)
             ptr_set.add(ptr)
-            if ptr.is_exclusive(schema):
+            for constr in ptr.get_exclusive_constraints(schema):
                 # Bingo, got an equality filter on a pointer with a
                 # unique constraint
-                yield ((ptr, expr),)
+                results.append(((ptr, expr),))
+                # We need to track all schema refs, since an expression
+                # in the schema needs to depend on any constraint
+                # that affects its cardinality.
+                ctx.env.add_schema_ref(constr, None)
 
         # Then look at all the object exclusive constraints
         result_stype = ctx.env.set_types[result_set]
         obj_exclusives = get_object_exclusive_constraints(
             result_stype, ptr_set, ctx.env)
-        for _, obj_exc_ptrs in obj_exclusives.items():
-            yield tuple((ptr, filtered_ptrs_map[ptr]) for ptr in obj_exc_ptrs)
+        for constr, obj_exc_ptrs in obj_exclusives.items():
+            results.append(
+                tuple((ptr, filtered_ptrs_map[ptr]) for ptr in obj_exc_ptrs))
+            ctx.env.add_schema_ref(constr, None)
+
+    return results
 
 
 def get_object_exclusive_constraints(
@@ -1016,10 +1025,7 @@ def _analyse_filter_clause(
     scope_tree: irast.ScopeTreeNode,
     ctx: inference_context.InfCtx,
 ) -> qltypes.Cardinality:
-    exc_filters = extract_exclusive_filters(
-        result_set, filter_clause, scope_tree, ctx)
-
-    for _ in exc_filters:
+    if extract_exclusive_filters(result_set, filter_clause, scope_tree, ctx):
         return AT_MOST_ONE
     else:
         return result_card
diff --git a/edb/schema/ordering.py b/edb/schema/ordering.py
--- a/edb/schema/ordering.py
+++ b/edb/schema/ordering.py
@@ -27,6 +27,7 @@
 from edb.common import topological
 
 from . import delta as sd
+from . import expraliases as s_expraliases
 from . import functions as s_func
 from . import inheriting
 from . import name as sn
@@ -693,6 +694,11 @@ def write_ref_deps(
                 # Otherwise, things must be deleted _after_ their referrers
                 # have been deleted or altered.
                 deps.add(('delete', ref_name_str))
+                # (except for aliases, which in the collection case
+                # specifically need the old target deleted before the
+                # new one is created)
+                if not isinstance(ref, s_expraliases.Alias):
+                    deps.add(('alter', ref_name_str))
                 if type(ref) == type(obj):
                     deps.add(('rebase', ref_name_str))
 
diff --git a/edb/schema/pointers.py b/edb/schema/pointers.py
--- a/edb/schema/pointers.py
+++ b/edb/schema/pointers.py
@@ -664,7 +664,8 @@ def generic(self, schema: s_schema.Schema) -> bool:
     def get_referrer(self, schema: s_schema.Schema) -> Optional[so.Object]:
         return self.get_source(schema)
 
-    def is_exclusive(self, schema: s_schema.Schema) -> bool:
+    def get_exclusive_constraints(
+            self, schema: s_schema.Schema) -> Sequence[constraints.Constraint]:
         if self.generic(schema):
             raise ValueError(f'{self!r} is generic')
 
@@ -672,13 +673,17 @@ def is_exclusive(self, schema: s_schema.Schema) -> bool:
 
         ptr = self.get_nearest_non_derived_parent(schema)
 
+        constrs = []
         for constr in ptr.get_constraints(schema).objects(schema):
             if (constr.issubclass(schema, exclusive) and
                     not constr.get_subjectexpr(schema)):
 
-                return True
+                constrs.append(constr)
 
-        return False
+        return constrs
+
+    def is_exclusive(self, schema: s_schema.Schema) -> bool:
+        return bool(self.get_exclusive_constraints(schema))
 
     def singular(
         self,


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
@@ -10426,6 +10426,82 @@ async def test_edgeql_migration_drop_constraint_02(self):
             type VerySpecialCard extending SpecialCard, SpecialCard2;
         ''')
 
+    async def test_edgeql_migration_drop_constraint_03(self):
+        await self.migrate(r'''
+            type C {
+                required property val -> str {
+                    constraint exclusive;
+                }
+            }
+
+            type Foo {
+                required link foo -> C {
+                    default := (SELECT C FILTER .val = 'D00');
+                }
+            }
+        ''')
+
+        await self.migrate('')
+
+    async def test_edgeql_migration_drop_constraint_04(self):
+        await self.migrate(r'''
+            type C {
+                required property val -> str {
+                    constraint exclusive;
+                }
+            }
+
+            type Foo {
+                required link foo -> C {
+                    default := (SELECT C FILTER .val = 'D00');
+                }
+            }
+        ''')
+
+        await self.migrate(r'''
+            type C {
+                required property val -> str;
+            }
+
+            type Foo {
+                required multi link foo -> C {
+                    default := (SELECT C FILTER .val = 'D00');
+                }
+            }
+        ''')
+
+    async def test_edgeql_migration_drop_constraint_05(self):
+        await self.migrate(r'''
+            type C {
+                required property val -> str {
+                    constraint exclusive;
+                }
+                required property val2 -> str {
+                    constraint exclusive;
+                }
+            }
+
+            type Foo {
+                required link foo -> C {
+                    default := (SELECT C FILTER .val = 'D00');
+                }
+            }
+        ''')
+
+        await self.migrate(r'''
+            type C {
+                required property val2 -> str {
+                    constraint exclusive;
+                }
+            }
+
+            type Foo {
+                required link foo -> C {
+                    default := (SELECT C FILTER .val2 = 'D00');
+                }
+            }
+        ''')
+
 
 class TestEdgeQLDataMigrationNonisolated(tb.DDLTestCase):
     TRANSACTION_ISOLATION = False