Repository: edgedb/edgedb

Issue:
Migrating to a target schema produces an Internal Server Error
<!-- Please search existing issues to avoid creating duplicates. -->

<!--
For the EdgeDB Version: run `edgedb query 'select sys::get_version_as_str()'` from your project directory (or run `select sys::get_version_as_str();` in the EdgeDB interactive shell).
For the EdgeDB CLI Version: Run `edgedb --version` from anywhere
-->

- EdgeDB Version: 3.0-dev.7435+d20355f
- EdgeDB CLI Version: 3.0.0-dev.968+810f40a
- OS Version: macOS 13.2.1

When I migrate from the initial schema to the target schema below, I get an internal server error:

```
edgedb error: InternalServerError: relation "edgedbpub.40d8ab8a-e4ff-11ed-968f-1b7486ebb623" does not exist
  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: relation "edgedbpub.40d8ab8a-e4ff-11ed-968f-1b7486ebb623" does not exist
```

This is after the interactive planning which went like this:
```
did you create link 'user' of object type 'default::Post'? [y,n,l,c,b,s,q,?]
> y
Please specify an expression to populate existing objects in order to make link 'user' of object type 'default::Post' required:
fill_expr_1> select User limit 1
did you alter property 'created' of object type 'default::Timestamped'? [y,n,l,c,b,s,q,?]
> y
did you create trigger '__::enforce_post_wait' of object type 'default::Post'? [y,n,l,c,b,s,q,?]
> y
did you alter link 'posts' of object type 'default::User'? [y,n,l,c,b,s,q,?]
> y
```

I tried to come up with a minimal reproduction, but I'm still not clear which of the changes I made caused this. It doesn't appear to be changing a link into a computed backlink, and it wasn't adding a trigger. Maybe adding the particular trigger I added, or adding that trigger while changing a link to a computed backlink at the same time?

Steps to Reproduce:

1. Start a new project on nightly
2. Migrate to the initial schema
3. Migrate to the target schema

<!-- If the issue is about a query error, please also provide your schema -->

Initial Schema:
```
module default {
  abstract type Timestamped {
    created: datetime {
      default := datetime_current();
    }
  }
  type User extending Timestamped {
    required email: str {
      constraint regexp(r'/^\S+@\S+\.\S+$/');
      constraint exclusive;
    }
    name: str;
    multi posts: Post {
      constraint exclusive;
    }
  }
  type Post extending Timestamped {
    required body: str {
      constraint min_len_value(3);
      constraint max_len_value(280);
    }
  }
}
```

Target Schema:
```
module default {
  abstract type Timestamped {
    created: datetime {
      default := datetime_current();
      readonly := true;
    }
  }
  type User extending Timestamped {
    required email: str {
      constraint regexp(r'/^\S+@\S+\.\S+$/');
      constraint exclusive;
    }
    name: str;
    multi link posts := .<user[is Post];
  }
  type Post extending Timestamped {
    required body: str {
      constraint min_len_value(3);
      constraint max_len_value(280);
    }
    required user: User;
    trigger enforce_post_wait after insert for each do (
      assert(
        not exists (
          select detached Post
          filter .user = __new__.user
          and .created < __new__.created + <duration>'1 year'
        ),
        message := 'Too soon for this user to post'
      )
    )
  }
}
```


Discussion:
I believe this is also the error we've been seeing in the `edgedb/edgedb-js` CI on nightly builds.
I was able to get migrated to this through this sequence of migrations:

1. Add `Post.user` link and make `created` read only
2. Create trigger and drop `User.posts` link
3. Add `User.posts` computed backlink
Minimized:
```
module default {
  type User {
    multi posts: Post {
      constraint exclusive;
    }
  }
  type Post {
  }
}
```

```
module default {
  type User {
    multi link posts := .<user[is Post];
  }
  type Post {
    required user: User;
  }
}
```

I'm a little surprised by this because I was pretty sure we tested this idiom.

Resolving patch:
diff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py
--- a/edb/pgsql/delta.py
+++ b/edb/pgsql/delta.py
@@ -5203,11 +5203,12 @@ def _alter_innards(
 
         if not is_abs and (was_comp and not is_comp):
             self._create_link(link, schema, orig_schema, context)
-        elif not is_abs and (not was_comp and is_comp):
-            self._delete_link(link, schema, orig_schema, context)
 
         schema = super()._alter_innards(schema, context)
 
+        if not is_abs and (not was_comp and is_comp):
+            self._delete_link(link, schema, orig_schema, context)
+
         # We check whether otd has changed, rather than whether
         # it is an attribute on this alter, because it might
         # live on a nested SetOwned, for example.
@@ -5668,12 +5669,13 @@ def _alter_innards(
 
         if src and (was_comp and not is_comp):
             self._create_property(prop, src, schema, orig_schema, context)
-        elif src and (not was_comp and is_comp):
-            self._delete_property(
-                prop, src.scls, src.op, schema, orig_schema, context)
 
         schema = super()._alter_innards(schema, context)
 
+        if src and (not was_comp and is_comp):
+            self._delete_property(
+                prop, src.scls, src.op, schema, orig_schema, context)
+
         if self.metadata_only:
             return schema
 


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
@@ -11425,6 +11425,30 @@ async def test_edgeql_migration_backlink_overloaded(self):
             }
         ''')
 
+    async def test_edgeql_migration_to_computed_drop_exclusive(self):
+        await self.migrate(r'''
+            type User {
+              multi posts: Post {
+                constraint exclusive;
+              }
+              multi foo: int64 {
+                constraint exclusive;
+              }
+            }
+            type Post;
+        ''')
+
+        await self.migrate(r'''
+            type User {
+              multi link posts := .<user[is Post];
+              multi property foo := Post.num;
+            }
+            type Post {
+              user: User;
+              num: int64;
+            }
+        ''')
+
     async def test_edgeql_migration_alias_new_computed_01(self):
         await self.migrate(r'''
             global a_id -> str;