Repository: edgedb/edgedb

Issue:
Bogus names and references in migrations are reported as ISEs
```
start migration to {
module test {
type Foo extending Bar;
}
};
```
is reported as `edb.common.topological.UnresolvedReferenceError: reference to an undefined item test::Bar in test::Foo`

and 
```
start migration to {
module test {
type Foo extending Foo;
}
};
```
is reported as a RecursionError


Discussion:

Resolving patch:
diff --git a/edb/edgeql/declarative.py b/edb/edgeql/declarative.py
--- a/edb/edgeql/declarative.py
+++ b/edb/edgeql/declarative.py
@@ -45,8 +45,12 @@
 from edb.edgeql import parser as qlparser
 from edb.edgeql import tracer as qltracer
 
+from edb.schema import annos as s_anno
+from edb.schema import constraints as s_constr
+from edb.schema import links as s_links
 from edb.schema import name as s_name
 from edb.schema import objects as s_obj
+from edb.schema import lproperties as s_lprops
 from edb.schema import schema as s_schema
 from edb.schema import sources as s_sources
 from edb.schema import types as s_types
@@ -524,7 +528,10 @@ def _trace_item_layout(
                 decl, obj=ptr, fq_name=ptr_name, ctx=ctx)
 
         elif isinstance(decl, qlast.CreateConcreteConstraint):
+            # Validate that the constraint exists at all.
+            _validate_schema_ref(decl, ctx=ctx)
             _, con_fq_name = ctx.get_fq_name(decl)
+
             con_name = s_name.QualName(
                 module=fq_name.module,
                 name=f'{fq_name.name}@{con_fq_name}',
@@ -532,6 +539,13 @@ def _trace_item_layout(
             ctx.objects[con_name] = qltracer.ConcreteConstraint(con_name)
             ctx.constraints[fq_name].add(con_name)
 
+        elif isinstance(decl, qlast.CreateAnnotationValue):
+            # Validate that the constraint exists at all.
+            _validate_schema_ref(decl, ctx=ctx)
+
+
+RECURSION_GUARD: Set[s_name.QualName] = set()
+
 
 def get_ancestors(
     fq_name: s_name.QualName,
@@ -542,9 +556,14 @@ def get_ancestors(
 
     # value already computed
     result = ancestors.get(fq_name, set())
-    if result:
+    if result is RECURSION_GUARD:
+        raise errors.InvalidDefinitionError(
+            f'{str(fq_name)!r} is defined recursively')
+    elif result:
         return result
 
+    ancestors[fq_name] = RECURSION_GUARD
+
     parent_set = parents.get(fq_name, set())
     # base case: include the parents
     result = set(parent_set)
@@ -998,7 +1017,7 @@ def _get_hard_deps(
 
 
 def _get_bases(
-    decl: qlast.DDLOperation,
+    decl: qlast.CreateObject,
     *,
     ctx: LayoutTraceContext
 ) -> List[s_name.QualName]:
@@ -1031,8 +1050,22 @@ def _get_bases(
 
         else:
             for base_ref in decl.bases:
-                base_name = ctx.get_ref_name(base_ref.maintype)
-                bases.append(base_name)
+                # Validate that the base actually exists.
+                tracer_type, real_type = _get_tracer_and_real_type(decl)
+                assert tracer_type is not None
+                assert real_type is not None
+                obj = _resolve_type_name(
+                    base_ref.maintype,
+                    tracer_type=tracer_type,
+                    real_type=real_type,
+                    ctx=ctx
+                )
+                name = obj.get_name(ctx.schema)
+                if not isinstance(name, s_name.QualName):
+                    qname = s_name.QualName.from_string(name.name)
+                else:
+                    qname = name
+                bases.append(qname)
 
     return bases
 
@@ -1049,21 +1082,15 @@ def _resolve_type_expr(
                 name=s_name.QualName(module='__coll__', name=texpr.name),
             )
         else:
-            refname = ctx.get_ref_name(texpr.maintype)
-            local_obj = ctx.objects.get(refname)
-            obj: qltracer.TypeLike
-            if local_obj is not None:
-                assert isinstance(local_obj, qltracer.Type)
-                obj = local_obj
-            else:
-                obj = _resolve_schema_ref(
-                    refname,
-                    type=s_types.Type,
-                    sourcectx=texpr.context,
+            return cast(
+                qltracer.TypeLike,
+                _resolve_type_name(
+                    texpr.maintype,
+                    tracer_type=qltracer.Type,
+                    real_type=s_types.Type,
                     ctx=ctx,
                 )
-
-            return obj
+            )
 
     elif isinstance(texpr, qlast.TypeOp):
 
@@ -1083,6 +1110,88 @@ def _resolve_type_expr(
         )
 
 
+def _resolve_type_name(
+    ref: qlast.BaseObjectRef,
+    *,
+    tracer_type: Type[qltracer.NamedObject],
+    real_type: Type[s_obj.Object_T],
+    ctx: LayoutTraceContext,
+) -> qltracer.ObjectLike:
+
+    refname = ctx.get_ref_name(ref)
+    local_obj = ctx.objects.get(refname)
+    obj: qltracer.ObjectLike
+    if local_obj is not None:
+        assert isinstance(local_obj, tracer_type)
+        obj = local_obj
+    else:
+        obj = _resolve_schema_ref(
+            refname,
+            type=real_type,
+            sourcectx=ref.context,
+            ctx=ctx,
+        )
+
+    return obj
+
+
+def _get_tracer_and_real_type(
+    decl: qlast.CreateObject,
+) -> Tuple[Optional[Type[qltracer.NamedObject]],
+           Optional[Type[s_obj.Object]]]:
+
+    tracer_type: Optional[Type[qltracer.NamedObject]] = None
+    real_type: Optional[Type[s_obj.Object]] = None
+
+    if isinstance(decl, (qlast.CreateObjectType,
+                         qlast.CreateScalarType)):
+        tracer_type = qltracer.Type
+        real_type = s_types.Type
+    elif isinstance(decl, (qlast.CreateConstraint,
+                           qlast.CreateConcreteConstraint)):
+        tracer_type = qltracer.Constraint
+        real_type = s_constr.Constraint
+    elif isinstance(decl, (qlast.CreateAnnotation,
+                           qlast.CreateAnnotationValue)):
+        tracer_type = qltracer.Annotation
+        real_type = s_anno.Annotation
+    elif isinstance(decl, (qlast.CreateProperty,
+                           qlast.CreateConcreteProperty)):
+        tracer_type = qltracer.Pointer
+        real_type = s_lprops.Property
+    elif isinstance(decl, (qlast.CreateLink,
+                           qlast.CreateConcreteLink)):
+        tracer_type = qltracer.Pointer
+        real_type = s_links.Link
+
+    return tracer_type, real_type
+
+
+def _validate_schema_ref(
+    decl: qlast.CreateObject,
+    *,
+    ctx: LayoutTraceContext,
+) -> None:
+    refname = ctx.get_ref_name(decl.name)
+    tracer_type, real_type = _get_tracer_and_real_type(decl)
+    if tracer_type is None:
+        # Bail out and rely on some other validation mechanism
+        return
+
+    local_obj = ctx.objects.get(refname)
+
+    if local_obj is not None:
+        assert isinstance(local_obj, tracer_type)
+    else:
+        assert real_type is not None
+        _resolve_schema_ref(
+            refname,
+            type=real_type,
+            sourcectx=decl.context,
+            ctx=ctx,
+        )
+
+
 def _resolve_schema_ref(
     name: s_name.Name,
     type: Type[s_obj.Object_T],


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -149,6 +149,29 @@ def test_schema_bad_prop_03(self):
             };
         """
 
+    @tb.must_fail(errors.InvalidReferenceError,
+                  "property 'test::bar' does not exist",
+                  position=59)
+    def test_schema_bad_prop_04(self):
+        """
+            abstract property foo extending bar;
+        """
+
+    @tb.must_fail(errors.InvalidReferenceError,
+                  "link 'test::bar' does not exist",
+                  position=55)
+    def test_schema_bad_prop_05(self):
+        """
+            abstract link foo extending bar;
+        """
+
+    @tb.must_fail(errors.InvalidDefinitionError,
+                  "'test::foo' is defined recursively")
+    def test_schema_bad_prop_06(self):
+        """
+            abstract link foo extending foo;
+        """
+
     @tb.must_fail(errors.InvalidReferenceError,
                   "type 'test::int' does not exist",
                   position=73)
@@ -198,6 +221,63 @@ def test_schema_bad_type_04(self):
             }
         """
 
+    @tb.must_fail(errors.InvalidReferenceError,
+                  "type 'test::Bar' does not exist",
+                  position=46)
+    def test_schema_bad_type_05(self):
+        """
+            type Foo extending Bar;
+        """
+
+    @tb.must_fail(errors.InvalidReferenceError,
+                  "type 'test::Bar' does not exist",
+                  position=66)
+    def test_schema_bad_type_06(self):
+        """
+            type Foo {
+                link val -> Bar;
+            };
+        """
+
+    @tb.must_fail(errors.InvalidReferenceError,
+                  "annotation 'test::bar' does not exist",
+                  position=54)
+    def test_schema_bad_type_07(self):
+        """
+            type Foo {
+                annotation bar := 'Bogus';
+            };
+        """
+
+    @tb.must_fail(errors.InvalidReferenceError,
+                  "constraint 'test::bogus' does not exist",
+                  position=96)
+    def test_schema_bad_type_08(self):
+        """
+            type Foo {
+                property val -> str {
+                    constraint bogus(5);
+                }
+            };
+        """
+
+    @tb.must_fail(errors.InvalidDefinitionError,
+                  "'test::Foo' is defined recursively")
+    def test_schema_bad_type_09(self):
+        """
+            type Foo extending Foo;
+        """
+
+    @tb.must_fail(errors.InvalidDefinitionError,
+                  "'test::Foo0' is defined recursively")
+    def test_schema_bad_type_10(self):
+        """
+            type Foo0 extending Foo1;
+            type Foo1 extending Foo2;
+            type Foo2 extending Foo3;
+            type Foo3 extending Foo0;
+        """
+
     def test_schema_computable_cardinality_inference_01(self):
         schema = self.load_schema("""
             type Object {