Repository: edgedb/edgedb

Issue:
InternalServerError when trying to create an exclusive constraint at base type
<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: 2.1+52c90a7 (repl 2.1.0-dev.837+77d7d80)
- OS Version: Mac m1 Kernel Version 21.6.0

Run this: 
```sql
CREATE MIGRATION m1tnwlplgljiafydd3xfu7ks7syxrmp6pwyzigtxv3n4ssseibzfla
    ONTO initial
{
  CREATE TYPE default::MenuItem {
      CREATE MULTI LINK children -> default::MenuItem;
      CREATE REQUIRED PROPERTY name -> std::str;
      CREATE PROPERTY page -> std::str;
  };
  CREATE TYPE default::Menu {
      CREATE CONSTRAINT std::exclusive;
      CREATE REQUIRED LINK item -> default::MenuItem;
  };
};
```

Output:
```bash
Error: InternalServerError: syntax error at or near "."
  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
```


Discussion:
Oh. `exclusive`, when defined on a `type` requires an expression, e.g. `exclusive on (.foo)`.  
The confusing ISE is a bug, of course.
> Oh. exclusive, when defined on a type requires an expression

@elprans  Yup, problem is that it's not what `migration create` thinks.

I didn't generate the migration, EdgeDB gladly liked my `.esdl` and created that blob above.

Feel free to rename the issue into a **deeper docs and migration generation problem.**
> I didn't generate the migration, EdgeDB gladly liked my `.esdl` and created that blob above.

That's the bug I was referring to.  Your schema should have been rejected.

Resolving patch:
diff --git a/edb/schema/constraints.py b/edb/schema/constraints.py
--- a/edb/schema/constraints.py
+++ b/edb/schema/constraints.py
@@ -694,6 +694,7 @@ def _populate_concrete_constraint_attrs(
         from edb.ir import utils as ir_utils
         from . import pointers as s_pointers
         from . import links as s_links
+        from . import objtypes as s_objtypes
         from . import scalars as s_scalars
 
         bases = self.get_resolved_attribute_value(
@@ -717,14 +718,25 @@ def _populate_concrete_constraint_attrs(
         elif (base_subjectexpr is not None
                 and subjectexpr.text != base_subjectexpr.text):
             raise errors.InvalidConstraintDefinitionError(
-                f'subjectexpr is already defined for {name}'
+                f'subjectexpr is already defined for {name}',
+                context=sourcectx,
             )
 
         if (isinstance(subject_obj, s_scalars.ScalarType)
                 and constr_base.get_is_aggregate(schema)):
             raise errors.InvalidConstraintDefinitionError(
                 f'{constr_base.get_verbosename(schema)} may not '
-                f'be used on scalar types'
+                f'be used on scalar types',
+                context=sourcectx,
+            )
+
+        if (
+            subjectexpr is None
+            and isinstance(subject_obj, s_objtypes.ObjectType)
+        ):
+            raise errors.InvalidConstraintDefinitionError(
+                "constraints on object types must have an 'on' clause",
+                context=sourcectx,
             )
 
         if subjectexpr is not None:


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
@@ -9539,6 +9539,7 @@ async def test_edgeql_ddl_constraint_18(self):
                 else:
                     await op
 
+    @test.xerror('only object constraints may use EXCEPT')
     async def test_edgeql_ddl_constraint_19(self):
         # This is pretty marginal, but make sure we can distinguish
         # on and except in name creation;
@@ -9552,8 +9553,10 @@ async def test_edgeql_ddl_constraint_19(self):
             create type ExceptTest {
                 create property b -> bool;
                 create property e -> bool;
-                create constraint always_ok except (.e);
-                create constraint always_ok on (.e);
+                create link l -> Object {
+                    create constraint always_ok except (.e);
+                    create constraint always_ok on (.e);
+                };
             };
         """)
 
@@ -9599,6 +9602,16 @@ async def test_edgeql_ddl_constraint_22(self):
                 };
             """)
 
+    async def test_edgeql_ddl_constraint_23(self):
+        async with self.assertRaisesRegexTx(
+                edgedb.InvalidConstraintDefinitionError,
+                r"constraints on object types must have an 'on' clause"):
+            await self.con.execute("""
+                create type X {
+                    create constraint exclusive;
+                };
+            """)
+
     async def test_edgeql_ddl_constraint_check_01a(self):
         await self.con.execute(r"""
             create type Foo {
@@ -12757,7 +12770,7 @@ async def test_edgeql_ddl_drop_field_03(self):
             };
 
             CREATE TYPE Foo {
-                CREATE CONSTRAINT bogus;
+                CREATE CONSTRAINT bogus on (true);
             };
         """)
 
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -8745,7 +8745,7 @@ def test_schema_describe_except_01(self):
             type ExceptTest {
                 property e -> std::bool;
                 constraint always_ok on (.e);
-                constraint always_ok except (.e);
+                constraint always_ok on (.e) except (.e);
                 constraint expression on (true) except (.e);
                 index on (.id) except (.e);
             };
@@ -8757,8 +8757,8 @@ def test_schema_describe_except_01(self):
             create type test::ExceptTest {
                 create property e -> std::bool;
                 create constraint std::expression on (true) except (.e);
+                create constraint test::always_ok on (.e) except (.e);
                 create constraint test::always_ok on (.e);
-                create constraint test::always_ok except (.e);
                 create index on (.id) except (.e);
             };
             """,