Repository: edgedb/edgedb

Issue:
Default for column cannot be cast automatically to relative_duration_t
<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: 1.3+804c096
- OS Version: macOS Monterey 12.2.1

Steps to Reproduce:

It has been hard to deduce exactly which field this stems from, but I will attach two schemas, before and after, as well as the migration script that was created. The steps below describe the field that has given the most trouble during this migration, and is my guess as to where this is coming from. The other fields are referenced as well in the overview of my migration creation interaction:

1. Changed type of the field `Token::expiration` from an optional `int32` to a required `datetime`. 
2. Updated all pre-existing `Token::expiration` fields to be null (*pre-migration creation*)
3. Ran `edgedb migration create` with the following inputs:

```
➜  dropshop-api git:(demo-week) ✗ edgedb migration create
did you alter the type of property 'expiry_window' of object type 'default::Offer'? [y,n,l,c,b,s,q,?]
> y
Please specify a conversion expression to alter the type of property 'expiry_window':
cast_expr_1> <cal::relative_duration>'7 days'
did you alter the type of property 'expiry_window' of object type 'default::Reward'? [y,n,l,c,b,s,q,?]
> y
Please specify a conversion expression to alter the type of property 'expiry_window':
cast_expr> <cal::relative_duration>'30 days'
did you alter property 'expiration' of object type 'default::Token'? [y,n,l,c,b,s,q,?]
> y
Please specify an expression to populate existing objects in order to make property 'expiration' of object type 'default::Token' required:
fill_expr> 30
Please specify a conversion expression to alter the type of property 'expiration':
cast_expr> datetime_of_transaction() + <cal::relative_duration>(<str>.expiration
 ++ ' days')
did you alter object type 'default::Token'? [y,n,l,c,b,s,q,?]
> y
Please specify an expression to populate existing objects in order to make property 'time_created' of object type 'default::Token' required:
fill_expr> datetime_of_transaction()
```

4. Ran the created edgeql script (at bottom)
5. Received the below error:

```
edgedb error: InternalServerError: default for column "5e96ee02-d5f6-11ec-851e-2d7d45107583" cannot be cast automatically to type relative_duration_t
  Hint: This is most likely a bug in EdgeDB. Please consider opening an issue ticket at https://github.com/edgedb/edgedb/issues/new?template=bug_report.md
  Server traceback:
      edb.errors.InternalServerError: default for column "5e96ee02-d5f6-11ec-851e-2d7d45107583" cannot be cast automatically to type relative_duration_t
```

.edgeql file (couldn't attach):

```
CREATE MIGRATION m1aipxh2mtgi54usdiovjzf7bovyvewkselmkwqe4z3ofdksvllxrq
    ONTO m1lay6izjogbeqfz6smvqtn677fanz24ovlbepp3r7ytz6ntq3cpua
{
  ALTER TYPE default::Offer {
      ALTER PROPERTY expiry_window {
          SET default := (<cal::relative_duration>'7 days');
          SET TYPE cal::relative_duration USING (<cal::relative_duration>'7 days');
      };
  };
  ALTER TYPE default::Reward {
      ALTER PROPERTY expiry_window {
          SET default := (<cal::relative_duration>'30 days');
          SET TYPE cal::relative_duration USING (<cal::relative_duration>'30 days');
      };
  };
  ALTER TYPE default::Token {
      ALTER PROPERTY expiration {
          SET REQUIRED USING (30);
          SET TYPE std::datetime USING ((std::datetime_of_transaction() + <cal::relative_duration>(<std::str>.expiration ++ ' days')));
      };
  };
  ALTER TYPE default::Token {
      CREATE REQUIRED PROPERTY favorite -> std::bool {
          SET default := false;
      };
      CREATE REQUIRED PROPERTY time_created -> std::datetime {
          SET REQUIRED USING (std::datetime_of_transaction());
      };
  };
};

```



Discussion:
@msullivan, looks like we are forgetting to apply `cast_expr` to `default`.
Is there a manual workaround in the meantime? I assume it is coming from `Token::expiration`.
A workaround would be to remove the "default" from `expiry_window` and then re-add it in a subsequent migration.
> @msullivan, looks like we are forgetting to apply `cast_expr` to `default`.

apply `cast_expr` to default doesn't make sense, it runs in the context of the whole object.

Resolving patch:
diff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py
--- a/edb/pgsql/delta.py
+++ b/edb/pgsql/delta.py
@@ -3309,6 +3309,12 @@ def get_host(self, schema, context):
             if objtype:
                 return objtype
 
+    def is_sequence_ptr(self, ptr, schema):
+        return bool(
+            (tgt := ptr.get_target(schema))
+            and tgt.issubclass(schema, schema.get('std::sequence'))
+        )
+
     def get_pointer_default(self, ptr, schema, context):
         if ptr.is_pure_computable(schema):
             return None
@@ -3316,11 +3322,10 @@ def get_pointer_default(self, ptr, schema, context):
         default = ptr.get_default(schema)
         default_value = None
 
-        if default is not None:
+        if default is not None and ptr.is_link_property(schema):
             default_value = schemamech.ptr_default_to_col_default(
                 schema, ptr, default)
-        elif (tgt := ptr.get_target(schema)) and tgt.issubclass(
-                schema, schema.get('std::sequence')):
+        elif self.is_sequence_ptr(ptr, schema):
             # TODO: replace this with a generic scalar type default
             #       using std::nextval().
             seq_name = common.quote_literal(
@@ -3716,11 +3721,9 @@ def _alter_pointer_type(self, pointer, schema, orig_schema, context):
         changing_col_type = not is_link
 
         source_ctx = self.get_referrer_context_or_die(context)
+        ptr_op = self.get_parent_op(context)
         if is_multi:
-            if isinstance(self, sd.AlterObjectFragment):
-                source_op = self.get_parent_op(context)
-            else:
-                source_op = self
+            source_op = ptr_op
         else:
             source_op = source_ctx.op
 
@@ -3923,6 +3926,14 @@ def _alter_pointer_type(self, pointer, schema, orig_schema, context):
             self.pgops.add(dbops.Query(update_qry))
 
         if changing_col_type:
+            # In case the column has a default, clear it out before
+            # changing the type
+            if is_lprop or self.is_sequence_ptr(pointer, orig_schema):
+                alter_table.add_operation(
+                    dbops.AlterTableAlterColumnDefault(
+                        column_name=old_ptr_stor_info.column_name,
+                        default=None))
+
             alter_type = dbops.AlterTableAlterColumnType(
                 old_ptr_stor_info.column_name,
                 common.quote_type(new_type),
@@ -4996,19 +5007,13 @@ def _alter_innards(
             orig_def_val = self.get_pointer_default(prop, orig_schema, context)
             def_val = self.get_pointer_default(prop, schema, context)
 
-            if (
-                orig_def_val != def_val
-                and (
-                    (tgt := prop.get_target(orig_schema)) is None
-                    or not tgt.issubclass(
-                        orig_schema,
-                        schema.get('std::sequence'),
-                    )
-                )
-            ):
-                source_ctx = context.get_ancestor(
-                    s_sources.SourceCommandContext, self)
-                alter_table = source_ctx.op.get_alter_table(
+            if orig_def_val != def_val:
+                if prop.get_cardinality(schema).is_multi():
+                    source_op = self
+                else:
+                    source_op = context.get_ancestor(
+                        s_sources.SourceCommandContext, self).op
+                alter_table = source_op.get_alter_table(
                     schema, context, manual=True)
 
                 ptr_stor_info = types.get_pointer_storage_info(


Test patch:
diff --git a/tests/test_edgeql_ddl.py b/tests/test_edgeql_ddl.py
--- a/tests/test_edgeql_ddl.py
+++ b/tests/test_edgeql_ddl.py
@@ -2623,6 +2623,96 @@ async def test_edgeql_ddl_ptr_set_type_using_02(self):
             ]
         )
 
+    async def test_edgeql_ddl_ptr_set_type_using_03(self):
+        # check that defaults don't break things
+        await self.con.execute(r"""
+            create type Foo {
+                create property x -> str {
+                    set default := '';
+                };
+                create multi property y -> str {
+                    set default := '';
+                };
+            };
+        """)
+
+        await self.con.execute(r"""
+            alter type Foo {
+                alter property x {
+                    set default := 0;
+                    set type int64 using (<int64>.x);
+                };
+                alter property y {
+                    set default := 0;
+                    set type int64 using (<int64>.y);
+                };
+            };
+        """)
+
+        await self.con.execute(r"""
+            create type Bar {
+                create link l -> Foo {
+                    create property x -> str {
+                        set default := '';
+                    }
+                }
+            };
+        """)
+
+        await self.con.execute(r"""
+            alter type Bar {
+                alter link l {
+                    alter property x {
+                        set default := 0;
+                        set type int64 using (<int64>@x);
+                    }
+                }
+            };
+        """)
+
+    async def test_edgeql_ddl_ptr_set_type_using_04(self):
+        # check that defaults don't break things
+        await self.con.execute(r"""
+            create scalar type X extending sequence;
+            create type Foo {
+                create property x -> X;
+            };
+        """)
+
+        await self.con.execute(r"""
+            alter type Foo {
+                alter property x {
+                    set type array<str> using ([<str>.x]);
+                }
+            };
+        """)
+
+        await self.con.execute(r"""
+            create type Bar {
+                create property x -> int64;
+            };
+        """)
+        await self.con.execute(r"""
+            alter type Bar {
+                alter property x {
+                    set type X using (<X>.x);
+                }
+            };
+        """)
+
+        await self.con.execute(r"""
+            create type Baz {
+                create multi property x -> int64;
+            };
+        """)
+        await self.con.execute(r"""
+            alter type Baz {
+                alter property x {
+                    set type X using (<X>.x);
+                }
+            };
+        """)
+
     async def test_edgeql_ddl_ptr_set_type_validation(self):
         await self.con.execute(r"""