Repository: geldata/gel
Pull request: Fix a cluster of constraint and rename issues
Some things fixed:
 * Track renames while generating ASTs so we don't emit references
   to renamed objects
 * Make sure to include subjectexpr and args when generating
   ALTER CONSTRAINT asts
 * Correctly use the nearest *abstract* constraint when generating
   constraint names in all cases
 * Consistently handle which constraint properties are marked
   as inherited. (Though there is still something of a mess there.)
 * Handle renames of abstract constraints in situations with
   inheritance properly

Fixes #2307. Fixes #2311. Fixes some more not filed.

Problem statement:
"error: constraint 'std::max_len_value' does not exist" during create-migration
EdgeDB Version: EdgeDB 1.0-beta.1+g2eee1afe4.d20210301

Changing schema from:
```
abstract type Event {
  required property createdAt -> datetime {
    default := datetime_current();
  }

  required link user -> User;
}

type Post extending Event {
  required property content -> str {
    constraint min_len_value(1);
    constraint max_len_value(280);
  }
}
```
to: 
```
abstract type Event {
  required property createdAt -> datetime {
    default := datetime_current();
  }

  required link user -> User;
}

abstract type HasContent {
  required property content -> str {
    constraint min_len_value(1);
    constraint max_len_value(280);
  }
}

type Post extending Event, HasContent {
}

type Reply extending Event, HasContent {
  required link post -> Post;
}
```

Run `edgedb create-migration`:
```
did you create object type 'default::HasContent'? [y,n,l,c,b,s,q,?]
y
did you alter object type 'default::Post'? [y,n,l,c,b,s,q,?]
y
did you create object type 'default::Reply'? [y,n,l,c,b,s,q,?]
y
did you drop constraint 'std::max_len_value' of property 'content'? [y,n,l,c,b,s,q,?]
n
did you alter property 'content' of object type 'default::Post'? [y,n,l,c,b,s,q,?]
y
error: constraint 'std::max_len_value' does not exist
  ┌─ query:3:9
  │  
3 │ ╭         ALTER CONSTRAINT std::max_len_value(280) ON (std::len(<std::str>__subject__)) {
4 │ │             DROP OWNED;
5 │ │         };
  │ ╰─────────^ error

Rolling back last operation...
did you alter property 'content' of object type 'default::Post'? [y,n,l,c,b,s,q,?]
n
did you alter property 'content' of object type 'default::Post'? [y,n,l,c,b,s,q,?]
```

Bogus rename prompts for constraints when a parent type is renamed
Migrating from

```
    abstract type Named {
        required property name -> str;
    };

    type Org;

    abstract type OrgBound {
        required link org -> Org;
    };

    abstract type OrgUniquelyNamed
        extending Named, OrgBound
    {
        constraint exclusive on ((.name, .org))
    }
```

to

```
    abstract type Named {
        required property name -> str;
    };

    type Org;
    abstract type Resource;

    abstract type OrgBound {
        required link org -> Org;
    };

    abstract type OrgUniquelyNamedResource
        extending Named, Resource, OrgBound
    {
        delegated constraint exclusive on ((.name, .org))
    }
```

I get:

```
did you rename constraint 'std::exclusive' of object type 'default::OrgUniquelyNamed' to 'std::exclusive'? [y,n,l,c,b,s,q,?]
l
Following DDL statements will be applied:
    ALTER TYPE default::OrgUniquelyNamed {
        ALTER CONSTRAINT std::exclusive ON ((.name, .org)) {
            SET DELEGATED;
        };
    };
```

which is triggered because the generated mangled name of the `std::exclusive` has changed, but this isn't a visible name and so a prompt should not be generated.

I suspect a similar issue exists for indexes.

Hints from issue discussion:
If you answer yes to `did you drop constraint 'std::max_len_value' of property 'content'? [y,n,l,c,b,s,q,?]`, there's another error:
```
did you create object type 'default::HasContent'? [y,n,l,c,b,s,q,?]
y
did you alter object type 'default::Post'? [y,n,l,c,b,s,q,?]
y
did you create object type 'default::Reply'? [y,n,l,c,b,s,q,?]
y
did you drop constraint 'std::max_len_value' of property 'content'? [y,n,l,c,b,s,q,?]
y
error: cannot drop inherited constraint 'std::max_len_value' of property 'content' of object type 'default::Post'
  ┌─ query:3:9
  │
3 │         DROP CONSTRAINT std::max_len_value(280);
  │         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error
  │
  = constraint 'std::max_len_value' of property 'content' of object type 'default::Post' is inherited from:
    - property 'content' of object type 'default::HasContent'

Rolling back last operation...
did you drop constraint 'std::max_len_value' of property 'content'? [y,n,l,c,b,s,q,?]
n
did you alter property 'content' of object type 'default::Post'? [y,n,l,c,b,s,q,?]
```
Seems like it shouldn't ask that question if there's only one correct answer?
There are probably multiple issues here, but the migration should definitely not be asking about the dropped constraint, because it wasn't, in fact, dropped.
Also, the generated DDL is incorrect, because `OrgUniquelyNamed` was renamed to `OrgUniquelyNamedResource` in a previous step.

Commit message: Fix a cluster of constraint and rename issues

Some things fixed:
 * Track renames while generating ASTs so we don't emit references
   to renamed objects
 * Make sure to include subjectexpr and args when generating
   ALTER CONSTRAINT asts
 * Correctly use the nearest *abstract* constraint when generating
   constraint names in all cases
 * Consistently handle which constraint properties are marked
   as inherited. (Though there is still something of a mess there.)
 * Handle renames of abstract constraints in situations with
   inheritance properly

Fixes #2307. Fixes #2311. Fixes some more not filed.

Patch to review:
diff --git a/edb/schema/constraints.py b/edb/schema/constraints.py
index fb98c210664..82e9f444e92 100644
--- a/edb/schema/constraints.py
+++ b/edb/schema/constraints.py
@@ -139,7 +139,7 @@ def _maybe_fix_name(
         obj = schema.get(name, type=Constraint)
 
         if not obj.generic(schema):
-            base = obj.get_bases(schema).objects(schema)[0]
+            base = obj.get_nearest_generic_parent(schema)
             base_name = context.get_obj_name(schema, base)
 
             quals = list(sn.quals_from_fullname(name))
@@ -424,6 +424,43 @@ def _constraint_args_from_ast(
 
         return args
 
+    @classmethod
+    def as_inherited_ref_ast(
+        cls,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+        name: sn.Name,
+        parent: so.Object,
+    ) -> qlast.ObjectDDL:
+        assert isinstance(parent, Constraint)
+        astnode_cls = cls.referenced_astnode  # type: ignore
+        nref = cls.get_inherited_ref_name(schema, context, parent, name)
+        args = []
+
+        parent_args = parent.get_args(schema)
+        if parent_args:
+            parent_args = parent.get_args(schema)
+            assert parent_args is not None
+            for arg_expr in parent_args:
+                arg = edgeql.parse_fragment(arg_expr.text)
+                args.append(arg)
+
+        subj_expr = parent.get_subjectexpr(schema)
+        if (
+            subj_expr is None
+            # Don't include subjectexpr if it was inherited from an
+            # abstract constraint.
+            or parent.get_nearest_generic_parent(
+                schema).get_subjectexpr(schema) is not None
+        ):
+            subj_expr_ql = None
+        else:
+            subj_expr_ql = edgeql.parse_fragment(subj_expr.text)
+
+        astnode = astnode_cls(name=nref, args=args, subjectexpr=subj_expr_ql)
+
+        return cast(qlast.ObjectDDL, astnode)
+
     def compile_expr_field(
         self,
         schema: s_schema.Schema,
@@ -534,6 +571,7 @@ def get_ref_implicit_base_delta(
             if not b.get_abstract(schema)
             and b.generic(schema) and b.get_name(schema) != default_base
         ]
+        # assert not explicit_bases
 
         new_bases = implicit_bases + explicit_bases
         return inheriting.delta_bases(
@@ -809,6 +847,13 @@ def _populate_concrete_constraint_attrs(
         from edb.ir import ast as ir_ast
         from edb.ir import utils as ir_utils
 
+        bases = self.get_resolved_attribute_value(
+            'bases', schema=schema, context=context,
+        )
+        direct_base = bases.objects(schema)[0]
+        if not direct_base.generic(schema):
+            return
+
         constr_base = schema.get(name, type=Constraint)
 
         orig_subjectexpr = subjectexpr
@@ -966,10 +1011,9 @@ def _populate_concrete_constraint_attrs(
                     context=sourcectx
                 )
 
-        attrs['return_type'] = constr_base.get_return_type(schema)
-        attrs['return_typemod'] = constr_base.get_return_typemod(schema)
         attrs['finalexpr'] = final_expr
         attrs['params'] = constr_base.get_params(schema)
+        inherited['params'] = True
         attrs['abstract'] = False
 
         for k, v in attrs.items():
@@ -999,46 +1043,13 @@ def as_inherited_ref_cmd(
 
         subj_expr = bases[0].get_subjectexpr(schema)
         if subj_expr is not None:
-            cmd.set_attribute_value('subjectexpr', subj_expr)
+            cmd.set_attribute_value('subjectexpr', subj_expr, inherited=True)
 
-        return cmd
+        params = bases[0].get_params(schema)
+        if params is not None:
+            cmd.set_attribute_value('params', params, inherited=True)
 
-    @classmethod
-    def as_inherited_ref_ast(
-        cls,
-        schema: s_schema.Schema,
-        context: sd.CommandContext,
-        name: sn.Name,
-        parent: so.Object,
-    ) -> qlast.ObjectDDL:
-        assert isinstance(parent, Constraint)
-        astnode_cls = cls.referenced_astnode
-        nref = cls.get_inherited_ref_name(schema, context, parent, name)
-        args = []
-
-        parent_args = parent.get_args(schema)
-        if parent_args:
-            parent_args = parent.get_args(schema)
-            assert parent_args is not None
-            for arg_expr in parent_args:
-                arg = edgeql.parse_fragment(arg_expr.text)
-                args.append(arg)
-
-        subj_expr = parent.get_subjectexpr(schema)
-        if (
-            subj_expr is None
-            # Don't include subjectexpr if it was inherited from an
-            # abstract constraint. (Constraints will view it as
-            # not-inherited if it was copied from an implicit base.)
-            or 'subjectexpr' in parent.get_inherited_fields(schema)
-        ):
-            subj_expr_ql = None
-        else:
-            subj_expr_ql = edgeql.parse_fragment(subj_expr.text)
-
-        astnode = astnode_cls(name=nref, args=args, subjectexpr=subj_expr_ql)
-
-        return astnode
+        return cmd
 
     @classmethod
     def _cmd_tree_from_ast(
@@ -1074,17 +1085,17 @@ def _cmd_tree_from_ast(
                         'with defaults',
                         context=astnode.context)
 
-        if cmd.get_attribute_value('return_type') is None:
-            cmd.set_attribute_value(
-                'return_type',
-                schema.get('std::bool'),
-            )
+            if cmd.get_attribute_value('return_type') is None:
+                cmd.set_attribute_value(
+                    'return_type',
+                    schema.get('std::bool'),
+                )
 
-        if cmd.get_attribute_value('return_typemod') is None:
-            cmd.set_attribute_value(
-                'return_typemod',
-                ft.TypeModifier.SingletonType,
-            )
+            if cmd.get_attribute_value('return_typemod') is None:
+                cmd.set_attribute_value(
+                    'return_typemod',
+                    ft.TypeModifier.SingletonType,
+                )
 
         assert isinstance(astnode, (qlast.CreateConstraint,
                                     qlast.CreateConcreteConstraint))
@@ -1209,10 +1220,12 @@ def _canonicalize(
         # Concrete constraints are children of abstract constraints
         # and have names derived from the abstract constraints. We
         # unfortunately need to go update their names.
-        children = scls.children(schema)
+        children = scls.descendants(schema)
         for ref in children:
             if ref.get_abstract(schema):
                 continue
+            if ref.get_nearest_generic_parent(schema) != scls:
+                continue
 
             ref_name = ref.get_name(schema)
             quals = list(sn.quals_from_fullname(ref_name))
@@ -1233,6 +1246,7 @@ def _canonicalize(
 
 class AlterConstraintOwned(
     referencing.AlterOwned[Constraint],
+    ConstraintCommand,
     field='owned',
     referrer_context_class=ConsistencySubjectCommandContext,
 ):
@@ -1272,6 +1286,30 @@ def _cmd_tree_from_ast(
         cls._validate_subcommands(astnode)
         return cmd
 
+    def _get_ast(
+        self,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+        *,
+        parent_node: Optional[qlast.DDLOperation] = None,
+    ) -> Optional[qlast.DDLOperation]:
+        if self.scls.get_abstract(schema):
+            return super()._get_ast(schema, context, parent_node=parent_node)
+
+        # We need to make sure to include subjectexpr and args
+        # in the AST, since they are really part of the name.
+        op = self.as_inherited_ref_ast(
+            schema, context, self.scls.get_name(schema),
+            self.scls,
+        )
+        self._apply_fields_ast(schema, context, op)
+
+        if (op is not None and hasattr(op, 'commands') and
+                not op.commands):
+            return None
+
+        return op
+
     def validate_alter(
         self,
         schema: s_schema.Schema,
@@ -1327,4 +1365,10 @@ class RebaseConstraint(
     ConstraintCommand,
     referencing.RebaseReferencedInheritingObject[Constraint],
 ):
-    pass
+    def _get_bases_for_ast(
+        self,
+        schema: s_schema.Schema,
+        context: sd.CommandContext,
+        bases: Tuple[so.ObjectShell, ...],
+    ) -> Tuple[so.ObjectShell, ...]:
+        return ()
diff --git a/edb/schema/delta.py b/edb/schema/delta.py
index 3880be3bcaf..5f77f01c29c 100644
--- a/edb/schema/delta.py
+++ b/edb/schema/delta.py
@@ -843,6 +843,13 @@ def _get_ast(
     ) -> Optional[qlast.DDLOperation]:
         raise NotImplementedError
 
+    def _log_all_renames(self, context: CommandContext) -> None:
+        if isinstance(self, RenameObject):
+            context.early_renames[self.classname] = self.new_name
+
+        for subcmd in self.get_subcommands():
+            subcmd._log_all_renames(context)
+
     @classmethod
     def get_orig_expr_text(
         cls,
@@ -2108,8 +2115,9 @@ def _get_ast(
         astnode = self._get_ast_node(schema, context)
 
         if astnode.get_field('name'):
+            name = context.early_renames.get(self.classname, self.classname)
             op = astnode(
-                name=self._deparse_name(schema, context, self.classname),
+                name=self._deparse_name(schema, context, name),
             )
         else:
             op = astnode()
@@ -3191,6 +3199,10 @@ def _get_ast(
         ref = self._deparse_name(schema, context, self.new_name)
         ref.itemclass = None
         orig_ref = self._deparse_name(schema, context, self.classname)
+
+        # Ha, ha! Do it recursively to force any renames in children!
+        self._log_all_renames(context)
+
         if (orig_ref.module, orig_ref.name) != (ref.module, ref.name):
             return astnode(new_name=ref)
         else:
@@ -3240,23 +3252,6 @@ def _rename_cmd_from_ast(
         )
 
 
-class RenameQualifiedObject(AlterObjectFragment[so.Object_T]):
-
-    new_name = struct.Field(sn.QualName)
-
-    def _get_ast(
-        self,
-        schema: s_schema.Schema,
-        context: CommandContext,
-        *,
-        parent_node: Optional[qlast.DDLOperation] = None,
-    ) -> Optional[qlast.DDLOperation]:
-        astnode = self._get_ast_node(schema, context)
-        new_name = self.new_name
-        ref = qlast.ObjectRef(name=new_name.name, module=new_name.module)
-        return astnode(new_name=ref)
-
-
 class AlterObject(AlterObjectOrFragment[so.Object_T], Generic[so.Object_T]):
     _delta_action = 'alter'
 
diff --git a/edb/schema/functions.py b/edb/schema/functions.py
index 415f9c6daae..5ccd978cd9d 100644
--- a/edb/schema/functions.py
+++ b/edb/schema/functions.py
@@ -734,7 +734,7 @@ class CallableObject(
         ft.TypeModifier, compcoef=0.4, coerce=True)
 
     abstract = so.SchemaField(
-        bool, default=False, compcoef=0.909)
+        bool, default=False, inheritable=False, compcoef=0.909)
 
     def as_create_delta(
         self: CallableObjectT,
diff --git a/edb/schema/objects.py b/edb/schema/objects.py
index a98efd571e7..8121c075d23 100644
--- a/edb/schema/objects.py
+++ b/edb/schema/objects.py
@@ -3185,6 +3185,15 @@ def get_nearest_non_derived_parent(
             obj = obj.get_bases(schema).first(schema)
         return obj
 
+    def get_nearest_generic_parent(
+        self: DerivableInheritingObjectT,
+        schema: s_schema.Schema,
+    ) -> DerivableInheritingObjectT:
+        obj = self
+        while not obj.generic(schema):
+            obj = obj.get_bases(schema).first(schema)
+        return obj
+
 
 @markup.serializer.serializer.register(Object)
 @markup.serializer.serializer.register(ObjectCollection)
diff --git a/tests/test_edgeql_data_migration.py b/tests/test_edgeql_data_migration.py
index 7da1d28cf6b..932786014b0 100644
--- a/tests/test_edgeql_data_migration.py
+++ b/tests/test_edgeql_data_migration.py
@@ -198,6 +198,33 @@ async def migrate(
                 migration, populate=populate, module=module)
             await self.fast_forward_describe_migration(user_input=user_input)
 
+    async def interact(self, parts, check_complete=True):
+        for part in parts:
+            if isinstance(part, str):
+                prompt = part
+                ans = "y"
+                user_input = None
+            else:
+                prompt, ans = part[0:2]
+                user_input = part[2] if len(part) > 2 else None
+
+            await self.assert_describe_migration({
+                'proposed': {'prompt': prompt}
+            })
+
+            if ans == "y":
+                await self.fast_forward_describe_migration(
+                    limit=1, user_input=user_input)
+            else:
+                await self.con.execute('''
+                    ALTER CURRENT MIGRATION REJECT PROPOSED;
+                ''')
+
+        if check_complete:
+            await self.assert_describe_migration({
+                'complete': True
+            })
+
     async def test_edgeql_migration_simple_01(self):
         # Base case, ensuring a single SDL migration from a clean
         # state works.
@@ -4757,6 +4784,154 @@ async def test_edgeql_migration_rebase_01(self):
             }
         """)
 
+    async def test_edgeql_migration_rebase_02(self):
+        await self.migrate('''
+            type User;
+            abstract type Event {
+              required property createdAt -> datetime {
+                default := datetime_current();
+              }
+
+              required link user -> User;
+            }
+
+            type Post extending Event {
+              required property content -> str {
+                constraint min_len_value(1);
+                constraint max_len_value(280);
+              }
+            }
+        ''')
+
+        await self.start_migration('''
+            type User;
+            abstract type Event {
+              required property createdAt -> datetime {
+                default := datetime_current();
+              }
+
+              required link user -> User;
+            }
+
+            abstract type HasContent {
+              required property content -> str {
+                constraint min_len_value(1);
+                constraint max_len_value(280);
+              }
+            }
+
+            type Post extending Event, HasContent {
+            }
+
+            type Reply extending Event, HasContent {
+              required link post -> Post;
+            }
+        ''')
+
+        # N.B.: these prompts are OK but not canonical; if they are
+        # broken in favor of something better, just fix them.
+        await self.interact([
+            "did you create object type 'test::HasContent'?",
+            "did you alter object type 'test::Post'?",
+            "did you create object type 'test::Reply'?",
+        ])
+
+    async def test_edgeql_migration_rebase_03(self):
+        await self.migrate('''
+            abstract type Named {
+                required property name -> str;
+            };
+
+            type Org;
+
+            abstract type OrgBound {
+                required link org -> Org;
+            };
+
+            abstract type OrgUniquelyNamed
+                extending Named, OrgBound
+            {
+                constraint exclusive on ((.name, .org))
+            }
+        ''')
+
+        await self.start_migration('''
+            abstract type Named {
+                required property name -> str;
+            };
+
+            type Org;
+            abstract type Resource;
+
+            abstract type OrgBound {
+                required link org -> Org;
+            };
+
+            abstract type OrgUniquelyNamedResource
+                extending Named, Resource, OrgBound
+            {
+                delegated constraint exclusive on ((.name, .org))
+            }
+        ''')
+
+        # N.B.: these prompts are OK but not canonical; if they are
+        # broken in favor of something better, just fix them.
+        await self.interact([
+            ("did you drop object type 'test::OrgUniquelyNamed'?", "n"),
+            "did you create object type 'test::Resource'?",
+            "did you rename object type 'test::OrgUniquelyNamed' to "
+            "'test::OrgUniquelyNamedResource'?",
+
+            "did you alter constraint 'std::exclusive' of object type "
+            "'test::OrgUniquelyNamed'?",
+
+            "did you alter object type 'test::OrgUniquelyNamed'?",
+        ])
+
+    async def test_edgeql_migration_rename_01(self):
+        await self.migrate('''
+            type Foo;
+        ''')
+
+        await self.start_migration('''
+            type Bar {
+                property asdf -> str;
+            };
+        ''')
+
+        await self.interact([
+            "did you rename object type 'test::Foo' to 'test::Bar'?",
+            "did you create property 'asdf' of object type 'test::Foo'?",
+        ])
+
+    async def test_edgeql_migration_rename_02(self):
+        await self.migrate('''
+            type Foo {
+                property asdf -> str;
+            };
+            type Bar extending Foo {
+                overloaded property asdf -> str;
+            };
+        ''')
+
+        await self.start_migration('''
+            type Foo {
+                property womp -> str;
+            };
+            type Bar extending Foo {
+                overloaded property womp -> str {
+                    annotation title := "foo";
+                };
+            };
+        ''')
+
+        await self.interact([
+            "did you rename property 'asdf' of object type 'test::Foo' to "
+            "'womp'?" ,
+
+            "did you create annotation 'std::title' of property 'asdf'?",
+        ])
+
     async def test_edgeql_migration_eq_function_01(self):
         await self.migrate(r"""
             function hello01(a: int64) -> str
diff --git a/tests/test_edgeql_ddl.py b/tests/test_edgeql_ddl.py
index c1fe8742519..7cc85d52a74 100644
--- a/tests/test_edgeql_ddl.py
+++ b/tests/test_edgeql_ddl.py
@@ -7499,6 +7499,27 @@ async def test_edgeql_ddl_constraint_12(self):
                 }
             """)
 
+    async def test_edgeql_ddl_constraint_13(self):
+        await self.con.execute(r"""
+            CREATE ABSTRACT CONSTRAINT Lol {
+                USING ((__subject__ < 10));
+            };
+            CREATE TYPE Foo {
+                CREATE PROPERTY x -> int64 {
+                    CREATE CONSTRAINT Lol;
+                };
+            };
+            CREATE TYPE Bar EXTENDING Foo;
+        """)
+
+        await self.con.execute(r"""
+            ALTER ABSTRACT CONSTRAINT Lol RENAME TO Lolol;
+        """)
+
+        await self.con.execute(r"""
+            ALTER TYPE Foo DROP PROPERTY x;
+        """)
+
     async def test_edgeql_ddl_constraint_alter_01(self):
         await self.con.execute(r"""
             CREATE TYPE test::ConTest01 {
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 4e671685c7d..ec931f52e09 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -2155,10 +2155,6 @@ def test_schema_get_migration_42(self):
 
         self._assert_migration_consistency(schema)
 
-    @test.xfail('''
-        edb.errors.InvalidReferenceError:
-        constraint 'std::max_len_value' does not exist
-    ''')
     def test_schema_get_migration_43(self):
         schema = r'''
         type Base {
@@ -4320,6 +4316,31 @@ def test_schema_migrations_equivalence_constraint_02(self):
             type Derived extending Base;
         """])
 
+    @test.xfail('''
+        Argh, this doesn't work because Bar's constraint gets a
+        nonsense rebase because we can't understand the rename of
+        Lol.
+    ''')
+    def test_schema_migrations_equivalence_constraint_04(self):
+        self._assert_migration_equivalence([r"""
+            abstract constraint Lol { using (__subject__ < 10) };
+            type Foo {
+                property x -> int64 {
+                    constraint Lol;
+                }
+            }
+            type Bar extending Foo;
+
+        """, r"""
+            abstract constraint Lolol { using (__subject__ < 10) };
+            type Foo {
+                property x -> int64 {
+                    constraint Lolol;
+                }
+            }
+            type Bar extending Foo;
+        """])
+
     def test_schema_migrations_equivalence_constraint_03(self):
         self._assert_migration_equivalence([r"""
             type Base {
@@ -5247,29 +5268,46 @@ def test_schema_migrations_drop_parent_07(self):
         """])
 
     def test_schema_migrations_drop_owned_default_01(self):
-        self._assert_migration_equivalence([r"""
-            type Foo;
-            type Post {
-                required property createdAt -> datetime {
-                    default := datetime_current();
-                }
-                link whatever -> Foo {
-                    default := (SELECT Foo LIMIT 1);
-                }
-            }
-        """, r"""
-            type Foo;
-            abstract type Event {
-                required property createdAt -> datetime {
-                    default := datetime_current();
+        self._assert_migration_equivalence([
+            r"""
+                type Foo;
+                type Post {
+                    required property createdAt -> str {
+                        default := "asdf";
+                        constraint expression on (__subject__ != "!");
+                    }
+                    link whatever -> Foo {
+                        default := (SELECT Foo LIMIT 1);
+                    }
                 }
-                link whatever -> Foo {
-                    default := (SELECT Foo LIMIT 1);
+            """,
+            r"""
+                type Foo;
+                abstract type Event {
+                    required property createdAt -> str {
+                        default := "asdf";
+                        constraint expression on (__subject__ != "!");
+                    }
+                    link whatever -> Foo {
+                        default := (SELECT Foo LIMIT 1);
+                    }
                 }
-            }
 
-            type Post extending Event;
-        """])
+                type Post extending Event;
+            """,
+            r"""
+                type Foo;
+                type Post {
+                    required property createdAt -> str {
+                        default := "asdf";
+                        constraint expression on (__subject__ != "!");
+                    }
+                    link whatever -> Foo {
+                        default := (SELECT Foo LIMIT 1);
+                    }
+                }
+            """
+        ])
 
     def test_schema_migrations_computed_optionality_01(self):
         self._assert_migration_equivalence([r"""


Code review comments:
diff_hunk: @@ -534,6 +571,7 @@ def get_ref_implicit_base_delta(
             if not b.get_abstract(schema)
             and b.generic(schema) and b.get_name(schema) != default_base
         ]
+        # assert not explicit_bases
path: edb/schema/constraints.py
text: @user1:
Uncomment or delete?

@author:
Let's try uncommenting it, and if that works I'll delete the apparently dead code above it.

(The explicit_bases condition requires `get_abstract` and `generic` to disagree and I don't understand when that would happen?)

@author:
:heavy_check_mark: