Repository: edgedb/edgedb

Issue:
UnresolvedReferenceError: reference to an undefined item default::null in default::MyType@is_initial_revision
I'm using a recursive type and it seems to cause some issues. It was working OK until I added the `is_initial_revision := .parent_revision is null;` computed field...

- EdgeDB Version: 5.2+ddd8aa4
- EdgeDB CLI Version: 5.0.0+ef02158
- OS Version: Windows 10

I'm trying to create a migration with this type in it:

```edgeql
type MyType {
  parent_revision: MyType;
  multi child_revisions := .<parent_revision[is MyType];
  is_latest_revision := count(.child_revisions) = 0;
  is_initial_revision := .parent_revision is null;
}
```

When I try to create a migration, I get the following error:

```shell
edgedb error: InternalServerError: UnresolvedReferenceError: reference to an undefined item default::null in default::MyType@is_initial_revision
  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:
      Traceback (most recent call last):
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/server/compiler_pool/worker.py", line 178, in compile
          units, cstate = COMPILER.compile_request(
                          ^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 857, in compile_request
          units, cstate = self.compile(
                          ^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 937, in compile
          unit_group = compile(ctx=ctx, source=source)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 2585, in compile
          return _try_compile(ctx=ctx, source=source)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 2663, in _try_compile
          comp, capabilities = _compile_dispatch_ql(
                               ^^^^^^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 2490, in _compile_dispatch_ql
          query = ddl.compile_dispatch_ql_migration(
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/server/compiler/ddl.py", line 386, in compile_dispatch_ql_migration
          return _start_migration(ctx, ql, in_script)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/server/compiler/ddl.py", line 466, in _start_migration
          target_schema = s_ddl.apply_sdl(
                          ^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/schema/ddl.py", line 591, in apply_sdl
          ddl_stmts = s_decl.sdl_to_ddl(target_schema, documents)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/edgeql/declarative.py", line 471, in sdl_to_ddl
          ordered = topological.sort(ddlgraph, allow_unresolved=False)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/common/topological.py", line 210, in sort
          items = sort_ex(graph, allow_unresolved=allow_unresolved)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/home/edgedb/.local/share/edgedb/portable/5.2/lib/python3.12/site-packages/edb/common/topological.py", line 134, in sort_ex
          raise UnresolvedReferenceError(
      edb.common.topological.UnresolvedReferenceError: reference to an undefined item default::null in default::MyType@is_initial_revision
```


Discussion:
Error presentation (ISE) is a bug, but the actual issue is that there is not a thing named `null` in EdgeDB.  You want `not exists .parent_revision` instead.
Thanks @elprans 
We should special-case the "null"
name to print an error with a hint that EdgeDB doesn't have null and explain what to do
Two things here:
1. The bug specifically seems to be in handling unresolved names in *type expressions* (which `null` is one of in `... is null`, since `E IS T` checks if the LHS is of type `T`). If you write `= null` instead we produce a proper error message
2. Yury's idea of giving a hint about `null` is a good one

Resolving patch:
diff --git a/edb/edgeql/tracer.py b/edb/edgeql/tracer.py
--- a/edb/edgeql/tracer.py
+++ b/edb/edgeql/tracer.py
@@ -28,6 +28,7 @@
 
 from contextlib import contextmanager
 from edb import errors
+from edb.common import parsing
 from edb.schema import links as s_links
 from edb.schema import name as sn
 from edb.schema import objects as so
@@ -656,12 +657,33 @@ def trace_Global(
     return tip
 
 
+def check_type_exists(
+    typename: sn.QualName,
+    ctx: TracerContext,
+    span: Optional[parsing.Span],
+    *,
+    hint: Optional[str] = None,
+) -> None:
+    if typename in ctx.objects:
+        return
+
+    try:
+        # Check if the typename is already in the schema
+        ctx.schema.get(typename, type=s_types.Type, sourcectx=span)
+    except errors.InvalidReferenceError as e:
+        if hint and not e.hint:
+            e.set_hint_and_details(hint, e.details)
+        raise e
+
+
 @trace.register
 def trace_TypeCast(node: qlast.TypeCast, *, ctx: TracerContext) -> None:
     trace(node.expr, ctx=ctx)
     if isinstance(node.type, qlast.TypeName):
         if not node.type.subtypes:
-            ctx.refs.add(ctx.get_ref_name(node.type.maintype))
+            typename: sn.QualName = ctx.get_ref_name(node.type.maintype)
+            check_type_exists(typename, ctx, node.type.span)
+            ctx.refs.add(typename)
 
 
 @trace.register
@@ -669,14 +691,25 @@ def trace_IsOp(node: qlast.IsOp, *, ctx: TracerContext) -> None:
     trace(node.left, ctx=ctx)
     if isinstance(node.right, qlast.TypeName):
         if not node.right.subtypes:
-            ctx.refs.add(ctx.get_ref_name(node.right.maintype))
+            typename: sn.QualName = ctx.get_ref_name(node.right.maintype)
+
+            hint: Optional[str] = None
+            if typename.name.lower() in ['null', 'none']:
+                hint = (
+                    'Did you mean to use `exists` to check if a set is empty?'
+                )
+            check_type_exists(typename, ctx, node.right.span, hint=hint)
+
+            ctx.refs.add(typename)
 
 
 @trace.register
 def trace_Introspect(node: qlast.Introspect, *, ctx: TracerContext) -> None:
     if isinstance(node.type, qlast.TypeName):
         if not node.type.subtypes:
-            ctx.refs.add(ctx.get_ref_name(node.type.maintype))
+            typename: sn.QualName = ctx.get_ref_name(node.type.maintype)
+            check_type_exists(typename, ctx, node.type.span)
+            ctx.refs.add(typename)
 
 
 @trace.register


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -2980,6 +2980,71 @@ def test_schema_query_parameter_02(self):
         global foo := <int64>$0
         """
 
+    @tb.must_fail(
+        errors.InvalidReferenceError,
+        "type 'test::C' does not exist",
+    )
+    def test_schema_unknown_typename_01(self):
+        """
+        type A;
+        type B {
+            link a -> A;
+            property x := <C>.a;
+        }
+        """
+
+    @tb.must_fail(
+        errors.InvalidReferenceError,
+        "type 'test::C' does not exist",
+    )
+    def test_schema_unknown_typename_02(self):
+        """
+        type A;
+        type B {
+            link a -> A;
+            property x := .a is C;
+        }
+        """
+
+    @tb.must_fail(
+        errors.InvalidReferenceError,
+        "type 'test::null' does not exist",
+        hint='Did you mean to use `exists` to check if a set is empty?'
+    )
+    def test_schema_unknown_typename_03(self):
+        """
+        type A;
+        type B {
+            link a -> A;
+            property x := .a is null;
+        }
+        """
+
+    @tb.must_fail(
+        errors.InvalidReferenceError,
+        "type 'test::NONE' does not exist",
+        hint='Did you mean to use `exists` to check if a set is empty?'
+    )
+    def test_schema_unknown_typename_04(self):
+        """
+        type A;
+        type B {
+            link a -> A;
+            property x := .a is NONE;
+        }
+        """
+
+    @tb.must_fail(
+        errors.InvalidReferenceError,
+        "type 'test::C' does not exist",
+    )
+    def test_schema_unknown_typename_05(self):
+        """
+        type B {
+            property x := (introspect C).name;
+        }
+        """
+
 
 class TestGetMigration(tb.BaseSchemaLoadTest):
     """Test migration deparse consistency.