Repository: edgedb/edgedb

Issue:
Changing type of overloaded link leads to weird errors during migration creation
- EdgeDB Version: 1-rc2
- OS Version: xubuntu 20.04.3

Steps to Reproduce:

Running `edgedb migration create --non-interactive --allow-unsafe` using the schema below returns the following error:

> edgedb error: SchemaError: link 'entity' of object type 'default::DogVersion' cannot be cast automatically from object type 'default::Versioned' to object type 'default::Dog'
  Hint: You might need to specify a conversion expression in a USING clause
  Server traceback: [...]

The interactive version is even worse, it gets stuck in an endless loop attempting to roll back a step.

Schema:
```
module default {
	abstract type Version {
		required link entity -> Versioned {
			readonly := true;
		}
	}

	abstract type Barks {
	}

	abstract type Versioned {
	}

	type DogVersion extending Version, Barks {
		overloaded required link entity -> Dog;
	}

	type Dog extending Versioned {
		multi link versions := (SELECT Dog.<entity[IS DogVersion]);
	}

	type CatVersion extending Version {
		overloaded required link entity -> Cat;
	}

	type Cat extending Versioned {
	}
}
```

This example is very fragile, even apparently unrelated changes like removing `Barks` from `Dog` or renaming types can lead to it successfully creating a migration.

The migration it produces when I remove `Barks` from `Dog`:

```
CREATE MIGRATION m1qqz546xpyqxoha4ipcw3upqbnzzudphdkihdojuu6bbui4r53gwq
    ONTO initial
{
  CREATE ABSTRACT TYPE default::Barks;
  CREATE ABSTRACT TYPE default::Versioned;
  CREATE TYPE default::Cat EXTENDING default::Versioned;
  CREATE ABSTRACT TYPE default::Version {
      CREATE REQUIRED LINK entity -> default::Versioned {
          SET readonly := true;
      };
  };
  CREATE TYPE default::CatVersion EXTENDING default::Version {
      ALTER LINK entity {
          SET OWNED;
          SET REQUIRED;
          SET TYPE default::Cat;
      };
  };
  CREATE TYPE default::Dog EXTENDING default::Versioned;
  CREATE TYPE default::DogVersion EXTENDING default::Version {
      ALTER LINK entity {
          SET OWNED;
          SET REQUIRED;
          SET TYPE default::Dog;
      };
  };
  ALTER TYPE default::Dog {
      CREATE MULTI LINK versions := (SELECT
          default::Dog.<entity[IS default::DogVersion]
      );
  };
};
```

I also had a variant where interactive mode asked for a conversion expression for such a link.


Discussion:
The particular migration complains because it generates the following DDL:

```
CREATE TYPE DogVersion EXTENDING default::Version ...;
CREATE TYPE default::Dog EXTENDING default::Versioned;
ALTER TYPE DogVersion ALTER LINK entity SET TYPE default::Dog;
```

Taken in isolation `ALTER TYPE` requires a `USING` conversion expression as you cannot simply change the type from `Versioned` to `Dog` because there might already be data in the link.   In this case, it's nonsensical of course, because the `ALTER` was generated from the initial creation.


@msullivan, I propose we note that `ALTER` is the result of breaking down a `CREATE` and automatically inject `USING (.link[IS NewTarget])` into any generated `SET TYPE`.  It'd also be great if we could place a check so that we never emit `SET TYPE` without `USING` if one is mandatory for the operation.
Would it be possible to use a type assertion which fails if an invalid link exists, instead of a filter which silently removes it?
Good question.  I think `assert_type` could be useful in this scenario, although I'd also consider adding a generic `assert(condition: bool, message: OPTIONAL str)` instead.

Resolving patch:
diff --git a/edb/schema/links.py b/edb/schema/links.py
--- a/edb/schema/links.py
+++ b/edb/schema/links.py
@@ -362,12 +362,38 @@ def _apply_field_ast(
                         assert isinstance(t, (so.Object, so.ObjectShell))
                         node.target = utils.typeref_to_ast(schema, t)
             else:
+                old_type = pointers.merge_target(
+                    self.scls,
+                    list(self.scls.get_bases(schema).objects(schema)),
+                    'target',
+                    ignore_local=True,
+                    schema=schema,
+                )
                 assert isinstance(op.new_value, (so.Object, so.ObjectShell))
+                new_type = (
+                    op.new_value.resolve(schema)
+                    if isinstance(op.new_value, so.ObjectShell)
+                    else op.new_value)
+
+                new_type_ast = utils.typeref_to_ast(schema, op.new_value)
+                cast_expr = None
+                # If the type isn't assignment castable, generate a
+                # USING with a nonsense cast. It shouldn't matter,
+                # since there should be no data to cast, but the DDL side
+                # of things doesn't know that since the command is split up.
+                if old_type and not old_type.assignment_castable_to(
+                        new_type, schema):
+                    cast_expr = qlast.TypeCast(
+                        type=new_type_ast,
+                        expr=qlast.Set(elements=[]),
+                    )
                 node.commands.append(
                     qlast.SetPointerType(
-                        value=utils.typeref_to_ast(schema, op.new_value),
+                        value=new_type_ast,
+                        cast_expr=cast_expr,
                     )
                 )
+
         elif op.property == 'on_target_delete':
             node.commands.append(qlast.OnTargetDelete(cascade=op.new_value))
         else:


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -2840,6 +2840,26 @@ def test_schema_get_migration_50(self):
 
         self._assert_migration_consistency(schema)
 
+    def test_schema_get_migration_51(self):
+        schema = r'''
+        abstract type Version {
+            required link entity -> Versioned;
+        }
+
+        abstract type Barks;
+        abstract type Versioned;
+
+        type DogVersion extending Version, Barks {
+            overloaded required link entity -> Dog;
+        }
+
+        type Dog extending Versioned {
+            multi link versions := (SELECT Dog.<entity[IS DogVersion]);
+        }
+        '''
+
+        self._assert_migration_consistency(schema)
+
     def test_schema_get_migration_multi_module_01(self):
         schema = r'''
             # The two declared types declared are from different