Repository: edgedb/edgedb

Issue:
Unable to resolve links in trigger inserting an inherited type
<!-- 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.7484+d93f47f
- EdgeDB CLI Version: EdgeDB CLI 3.0.0-dev.982+9a1586d
- OS Version: macOS 13.3.1 (22E261)

Steps to Reproduce:

1. Run `edgedb watch`. The following also does not work when running `edgedb migration create`.

```console
⠤ calculating diff
error: object type 'default::PostCommentedNotification' has no link or property 'post'
    ┌─ /Users/dew/packages/edgedb/dbschema/default.esdl:141:55
    │  
141 │       trigger new_notification after insert for each do (
    │ ╭───────────────────────────────────────────────────────^
142 │ │       insert PostCommentedNotification {
143 │ │         account := __new__.post.author,
144 │ │         post := __new__.post,
145 │ │         comment := __new__,
146 │ │       }
147 │ │     );
    │ ╰─────^ error

Schema migration error: cannot proceed until .esdl files are fixed
```

Schema:

```
  type Account {
    required property name -> str {
      default := "";
    };
  }

  abstract type Submission {
    required property content -> str;
  }

  type Post extending Submission {
    multi link comments := .<post[is Comment];
    property numComments := count(.comments);
  }

  type Comment extending Submission {
    required link post -> Post {
      on target delete allow;
    };

    trigger new_notification after insert for each do (
      insert PostCommentedNotification {
        account := __new__.post.author,
        post := __new__.post,
        comment := __new__,
      }
    );
  }

  abstract type Notification {
    required link account -> Account {
      on target delete delete source;
    };
    required property read -> bool {
      default := false;
    };
  }

  type PostCommentedNotification extending Notification {
    required link post -> Post {
      on target delete delete source;
    };
    required link comment -> Comment {
      on target delete delete source;
    };
  }
```


Discussion:
same here on 3.0-beta.1+4cdeba3

Here's my schema:

```
abstract type HasCreatedAt {
  required property createdAt -> datetime {
    default := datetime_current();
    readonly := true;
  };
}

scalar type Action extending enum<`Insert`, `Delete`>;

type Log extending HasCreatedAt {
  required link user -> User;
  required property action -> Action;
}

type User extending HasCreatedAt {
  required property username -> str {
    constraint exclusive;
  };

  trigger logInsert after insert for each do (
    insert Log {
      user := __new__,
      action := <Action>'Insert',
    }
  );
}
```
Okay turns out for mine it needs the Log type to come before where you use it in the schema. Looks like a bug with the compiler

Resolving patch:
diff --git a/edb/edgeql/tracer.py b/edb/edgeql/tracer.py
--- a/edb/edgeql/tracer.py
+++ b/edb/edgeql/tracer.py
@@ -456,37 +456,34 @@ def get_ref_name_startswith(
         return refs
 
 
+def _fork_context(ctx: TracerContext) -> TracerContext:
+    nctx = TracerContext(
+        schema=ctx.schema,
+        module=ctx.module,
+        objects=dict(ctx.objects),
+        anchors=ctx.anchors,
+        path_prefix=ctx.path_prefix,
+        modaliases=dict(ctx.modaliases),
+        params=ctx.params,
+        visited=ctx.visited,
+        local_modules=ctx.local_modules,
+    )
+    nctx.refs = ctx.refs
+
+    return nctx
+
+
 @contextmanager
 def alias_context(
     ctx: TracerContext,
     aliases: Optional[
         Sequence[Union[qlast.AliasedExpr, qlast.ModuleAliasDecl]]],
 ) -> Generator[TracerContext, None, None]:
-    nctx = None
-
-    def _fork_context() -> TracerContext:
-        nonlocal nctx
-
-        if nctx is None:
-            nctx = TracerContext(
-                schema=ctx.schema,
-                module=ctx.module,
-                objects=dict(ctx.objects),
-                anchors=ctx.anchors,
-                path_prefix=ctx.path_prefix,
-                modaliases=dict(ctx.modaliases),
-                params=ctx.params,
-                visited=ctx.visited,
-                local_modules=ctx.local_modules,
-            )
-            nctx.refs = ctx.refs
-
-        return nctx
+    ctx = _fork_context(ctx)
 
     for alias in (aliases or ()):
         # module and modalias in ctx needs to be amended
         if isinstance(alias, qlast.ModuleAliasDecl):
-            ctx = _fork_context()
             if alias.alias:
                 ctx.modaliases[alias.alias] = alias.module
             else:
@@ -494,7 +491,6 @@ def _fork_context() -> TracerContext:
                 ctx.module = alias.module
 
         elif isinstance(alias, qlast.AliasedExpr):
-            ctx = _fork_context()
             obj = trace(alias.expr, ctx=ctx)
             # Regardless of whether tracing the expression produces an
             # object, record the alias.
@@ -997,6 +993,13 @@ def trace_ShapeElement(node: qlast.ShapeElement, *,
     trace(node.compexpr, ctx=ctx)
 
 
+def _update_path_prefix(tip: Optional[ObjectLike], ctx: TracerContext) -> None:
+    if tip is not None:
+        tip_name = tip.get_name(ctx.schema)
+        assert isinstance(tip_name, sn.QualName)
+        ctx.path_prefix = tip_name
+
+
 @trace.register
 def trace_Select(
     node: qlast.SelectQuery,
@@ -1005,10 +1008,7 @@ def trace_Select(
 ) -> Optional[ObjectLike]:
     with alias_context(ctx, node.aliases) as ctx:
         tip = trace(node.result, ctx=ctx)
-        if tip is not None:
-            tip_name = tip.get_name(ctx.schema)
-            assert isinstance(tip_name, sn.QualName)
-            ctx.path_prefix = tip_name
+        _update_path_prefix(tip, ctx=ctx)
 
         # potentially SELECT uses an alias for the main result
         with result_alias_context(ctx, node, tip) as nctx:
@@ -1096,7 +1096,8 @@ def trace_SortExpr(node: qlast.SortExpr, *, ctx: TracerContext) -> None:
 @trace.register
 def trace_InsertQuery(node: qlast.InsertQuery, *, ctx: TracerContext) -> None:
     with alias_context(ctx, node.aliases) as ctx:
-        trace(qlast.Path(steps=[node.subject]), ctx=ctx)
+        tip = trace(qlast.Path(steps=[node.subject]), ctx=ctx)
+        _update_path_prefix(tip, ctx=ctx)
 
         for element in node.shape:
             trace(element, ctx=ctx)
@@ -1110,13 +1111,7 @@ def trace_UpdateQuery(
 ) -> Optional[ObjectLike]:
     with alias_context(ctx, node.aliases) as ctx:
         tip = trace(node.subject, ctx=ctx)
-
-        if tip is None:
-            return None
-
-        tip_name = tip.get_name(ctx.schema)
-        assert isinstance(tip_name, sn.QualName)
-        ctx.path_prefix = tip_name
+        _update_path_prefix(tip, ctx=ctx)
 
         # potentially UPDATE uses an alias for the main result
         with result_alias_context(ctx, node, tip) as nctx:
@@ -1136,10 +1131,7 @@ def trace_DeleteQuery(
 ) -> Optional[ObjectLike]:
     with alias_context(ctx, node.aliases) as ctx:
         tip = trace(node.subject, ctx=ctx)
-        if tip is not None:
-            tip_name = tip.get_name(ctx.schema)
-            assert isinstance(tip_name, sn.QualName)
-            ctx.path_prefix = tip_name
+        _update_path_prefix(tip, ctx=ctx)
 
         # potentially DELETE uses an alias for the main result
         with result_alias_context(ctx, node, tip) as nctx:


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -3595,6 +3595,44 @@ def test_schema_access_policy_parens_02(self):
 
         self._assert_migration_consistency(schema)
 
+    def test_schema_trigger_01(self):
+        schema = '''
+            type User {
+              trigger logInsert after insert for each do (
+                insert Log {
+                  user := __new__,
+                  action := 'Insert',
+                }
+              );
+            }
+
+            type Log {
+              required link user -> User;
+              required property action -> str;
+            }
+        '''
+
+        self._assert_migration_consistency(schema)
+
+    def test_schema_trigger_02(self):
+        schema = '''
+            type User {
+              trigger logInsert after insert for each do (
+                update Log set {
+                  user := __new__,
+                  action := 'Insert',
+                }
+              );
+            }
+
+            type Log {
+              required link user -> User;
+              required property action -> str;
+            }
+        '''
+
+        self._assert_migration_consistency(schema)
+
     def test_schema_pointer_kind_infer_01(self):
         tschema = r'''
         type Bar;