Repository: edgedb/edgedb

Issue:
Migration issue when cyclic dependency is altered
- EdgeDB Version: 1-beta.2

Steps to Reproduce:

1. Apply the following schema:
```
module default {
    type Article {
        required property deleted := (EXISTS (.<element[IS default::DeletionRecord]));
    };
    type Category {
        required property deleted := (EXISTS (.<element[IS default::DeletionRecord]));
    };
    type DeletionRecord {
        required link element -> (default::Article | default::Category) {
            on target delete  delete source;
            constraint std::exclusive;
        };
    };
};
```
2. Change to:
```
module default {
    abstract type Removable {
        property deleted := EXISTS(.<element[IS DeletionRecord]);
    }
    type Article extending Removable {
    };
    type Category extending Removable {
    };
    type DeletionRecord {
        required link element -> Removable {
            on target delete delete source;
            constraint std::exclusive;
        };
    };
};
```
3. Get the following error on `create-migration`:
```
did you create object type 'default::Removable'? [y,n,l,c,b,s,q,?]
y
error: 'default::Removable.<element' does not resolve to any known path
  ┌─ query:2:41
  │
2 │     CREATE PROPERTY deleted := (EXISTS (.<element[IS default::DeletionRecord]));
  │                                         ^^^^^^^^^ error

Rolling back last operation...
did you create object type 'default::Removable'? [y,n,l,c,b,s,q,?]
```

On the other hand, schema from (2) works fine if applied from scratch


Discussion:
Hm, we now reject the first schema with
```
ERROR: SchemaError: it is illegal to create a type union that causes a computable property 'deleted' to mix with other versions of the same property 'deleted'
```
Oh, but if I make the names different, and then try to migrate, it still fails, so.
> Hm, we now reject the first schema with
> 
> ```
> ERROR: SchemaError: it is illegal to create a type union that causes a computable property 'deleted' to mix with other versions of the same property 'deleted'
> ```

Actually I don't understand what it means. You can make an union of two types which have a property that is named the same, right? So what makes it broken? The fact that both (or any) are computed? Is the limitation fundamental or temporary? (i.e. I used to think that any property can be changed to computable, e.g. for backwards compatibility, this limitation seems arbitrary for me)
>  Is the limitation fundamental or temporary

Temporary, until the compiler learns to actually produce correct queries for polymorphic computables.
Good. Can we rephrase error to something like:
```
it is illegal to create a type union from multiple types having computable property `deleted`
```
Or:
```
multiple matches for `deleted`, each type in an union must have unique computable properties
```
> Can we rephrase error to something like

We prohibit mixing non-computables with computables as well, not just different computables, but I agree that the error message needs clarification.  I'd actually specialize the message to each prohibited combination (i.e. computable and non-computable and computable/computable).

\cc @vpetrovykh

Resolving patch:
diff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py
--- a/edb/pgsql/delta.py
+++ b/edb/pgsql/delta.py
@@ -2689,7 +2689,7 @@ def alter_pointer_default(self, pointer, orig_schema, schema, context):
             not orig_schema
             or pointer.get_default(orig_schema)
             or (tgt := pointer.get_target(orig_schema)) and tgt.issubclass(
-                schema, schema.get('std::sequence'))
+                orig_schema, schema.get('std::sequence'))
         ):
             return
 
diff --git a/edb/schema/ordering.py b/edb/schema/ordering.py
--- a/edb/schema/ordering.py
+++ b/edb/schema/ordering.py
@@ -452,6 +452,14 @@ def _break_down(
             assert isinstance(op, sd.ObjectCommand)
             strongrefs[sub_op.classname] = op.classname
 
+    # For SET TYPE and friends, we need to make sure that an alter
+    # (with children) makes it into the opmap so it is processed.
+    if (
+        isinstance(op, sd.AlterSpecialObjectField)
+        and not isinstance(op, referencing.AlterOwned)
+    ):
+        opmap[new_opbranch[-2]] = new_opbranch[:-1]
+
     opmap[op] = new_opbranch
 
 
@@ -481,15 +489,25 @@ def record_field_deps(
         parent_op: sd.ObjectCommand[so.Object],
     ) -> str:
         if isinstance(op.new_value, (so.Object, so.ObjectShell)):
-            nvn = op.new_value.get_name(new_schema)
+            obj = op.new_value
+            nvn = obj.get_name(new_schema)
             if nvn is not None:
                 deps.add(('create', str(nvn)))
                 deps.add(('alter', str(nvn)))
                 if nvn in renames_r:
                     deps.add(('rename', str(renames_r[nvn])))
 
+            if isinstance(obj, so.ObjectShell):
+                obj = obj.resolve(new_schema)
+            # For SET TYPE, we want to finish any rebasing into the
+            # target type before we change the type.
+            if isinstance(obj, so.InheritingObject):
+                for desc in obj.descendants(new_schema):
+                    deps.add(('rebase', str(desc.get_name(new_schema))))
+
         graph_key = f'{parent_op.classname}%%{op.property}'
         deps.add(('create', str(parent_op.classname)))
+        deps.add(('alter', str(parent_op.classname)))
 
         if isinstance(op.old_value, (so.Object, so.ObjectShell)):
             assert old_schema is not None


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -5976,6 +5976,39 @@ def test_schema_migrations_drop_from_one_parent_02(self):
             type Comment extending Text, Owned;
         """])
 
+    def test_schema_migrations_expression_ref_01(self):
+        self._assert_migration_equivalence([
+            r"""
+                type Article {
+                    required property deleted_a := (
+                        EXISTS (.<element[IS DeletionRecord]));
+                };
+                type Category {
+                    required property deleted_c := (
+                        EXISTS (.<element[IS DeletionRecord]));
+                };
+                type DeletionRecord {
+                    required link element -> (Article | Category) {
+                        on target delete delete source;
+                        constraint std::exclusive;
+                    };
+                };
+            """,
+            r"""
+                abstract type Removable {
+                    property deleted := EXISTS(.<element[IS DeletionRecord]);
+                }
+                type Article extending Removable;
+                type Category extending Removable;
+                type DeletionRecord {
+                    required link element -> Removable {
+                        on target delete delete source;
+                        constraint std::exclusive;
+                    };
+                };
+            """
+        ])
+
 
 class TestDescribe(tb.BaseSchemaLoadTest):
     """Test the DESCRIBE command."""