Repository: edgedb/edgedb

Issue:
Add constraint syntax to object types
Currently we allow declaring constraints only on properties and links (syntactically), but not on object types.  This is largely an oversight, since we already have all the necessary mechanisms.


Discussion:
Does this mean something like this?
```
type Person {
    required property first_name -> str;
    required property last_name -> str;
    constraint exclusive on ((__subject__.first_name, __subject__.last_name));
}
```

> Does this mean something like this?

Yes, that's correct.

Resolving patch:
diff --git a/edb/edgeql/parser/grammar/sdl.py b/edb/edgeql/parser/grammar/sdl.py
--- a/edb/edgeql/parser/grammar/sdl.py
+++ b/edb/edgeql/parser/grammar/sdl.py
@@ -896,6 +896,8 @@ def reduce_CreateQualifiedComputableLink(self, *kids):
     ConcretePropertyShort,
     ConcreteLinkBlock,
     ConcreteLinkShort,
+    ConcreteConstraintBlock,
+    ConcreteConstraintShort,
     IndexDeclarationBlock,
     IndexDeclarationShort,
 )


Test patch:
diff --git a/tests/schemas/constraints.esdl b/tests/schemas/constraints.esdl
--- a/tests/schemas/constraints.esdl
+++ b/tests/schemas/constraints.esdl
@@ -109,6 +109,14 @@ type Object {
     property c_my_enum -> constraint_my_enum;
 }
 
+type ObjCnstr {
+    required property first_name -> str;
+    required property last_name -> str;
+    link label -> Label;
+    constraint exclusive on (__subject__.first_name);
+    constraint exclusive on (__subject__.label);
+}
+
 type UniqueName {
     property name -> str {
         constraint exclusive;
diff --git a/tests/schemas/constraints_migration/schema.esdl b/tests/schemas/constraints_migration/schema.esdl
--- a/tests/schemas/constraints_migration/schema.esdl
+++ b/tests/schemas/constraints_migration/schema.esdl
@@ -88,6 +88,12 @@ type Object {
     property c_enum -> constraint_enum;
 }
 
+type ObjCnstr {
+    required property first_name -> str;
+    required property last_name -> str;
+    constraint exclusive on ((__subject__.first_name, __subject__.last_name));
+}
+
 type UniqueName {
     property name -> str {
         constraint exclusive;
diff --git a/tests/schemas/constraints_migration/updated_schema.esdl b/tests/schemas/constraints_migration/updated_schema.esdl
--- a/tests/schemas/constraints_migration/updated_schema.esdl
+++ b/tests/schemas/constraints_migration/updated_schema.esdl
@@ -101,6 +101,14 @@ type Object {
     }
 }
 
+type ObjCnstr {
+    required property first_name -> str;
+    required property last_name -> str;
+    constraint exclusive on (__subject__.first_name) {
+        errmessage := "nope!";
+    }
+}
+
 type UniqueName {
     property name -> str {
         constraint exclusive;
diff --git a/tests/test_constraints.py b/tests/test_constraints.py
--- a/tests/test_constraints.py
+++ b/tests/test_constraints.py
@@ -352,6 +352,37 @@ async def test_constraints_exclusive_delegation(self):
                     };
                 """)
 
+    async def test_constraints_objects(self):
+        async with self._run_and_rollback():
+            with self.assertRaisesRegex(
+                    edgedb.ConstraintViolationError,
+                    "ObjCnstr violates exclusivity constraint"):
+                await self.con.execute("""
+                    INSERT test::ObjCnstr {
+                        first_name := "foo", last_name := "bar" };
+
+                    INSERT test::ObjCnstr {
+                        first_name := "foo", last_name := "baz" }
+            """)
+
+        async with self._run_and_rollback():
+            await self.con.execute("""
+                INSERT test::ObjCnstr {
+                    first_name := "foo", last_name := "bar",
+                    label := (INSERT test::Label {text := "obj_test" })
+                };
+            """)
+
+            with self.assertRaisesRegex(
+                    edgedb.ConstraintViolationError,
+                    "ObjCnstr violates exclusivity constraint"):
+                await self.con.execute("""
+                    INSERT test::ObjCnstr {
+                        first_name := "emarg", last_name := "hatch",
+                        label := (SELECT test::Label
+                                  FILTER .text = "obj_test" LIMIT 1) };
+                """)
+
 
 class TestConstraintsSchemaMigration(tb.QueryTestCase):
     ISOLATED_METHODS = False
@@ -514,6 +545,18 @@ async def test_constraints_exclusive_migration(self):
                     };
                 """)
 
+        async with self._run_and_rollback():
+            with self.assertRaisesRegex(
+                    edgedb.ConstraintViolationError,
+                    "nope!"):
+                await self.con.execute("""
+                    INSERT test::ObjCnstr {
+                        first_name := "foo", last_name := "bar" };
+
+                    INSERT test::ObjCnstr {
+                        first_name := "foo", last_name := "baz" }
+            """)
+
 
 class TestConstraintsDDL(tb.NonIsolatedDDLTestCase):
     async def test_constraints_ddl_01(self):