Repository: edgedb/edgedb

Issue:
Unique constraint on abstract type doesn't work?
Here is the error:
```
edgedb> CREATE ABSTRACT TYPE Base { create property test1 -> str; create property test2 -> str; create constraint exclusive on ((.test1, .test2));};
OK: CREATE
edgedb> CREATE TYPE Test2 EXTENDING Base;
error: could not resolve partial path
  ┌─ query:1:28
  │
1 │ CREATE TYPE Test2 EXTENDING Base;
  │                            ^^^^^^^ error

edgedb> CREATE TYPE Test2 EXTENDING default::Base;
error: could not resolve partial path
  ┌─ query:1:28
  │
1 │ CREATE TYPE Test2 EXTENDING default::Base;
  │                            ^^^^^^^ error
```
Also note that error message is a way off, so I guess it refers to a constraint definition rather than the DDL here.

There are three possible action points:
1. Fix constraint to work
2. Or disable some kinds of constraints in abstract types (at least temporarily)
3. And figure out what's wrong with the error position and how we can fix that (I think we need to ship actual source code of the expression via error message to correctly point to the error).

**Update:** when using `create-migration` error pops up only on `migrate` the migration is created without errors.


Discussion:
FWIW in this case I think you wanted to use a ["delegated constraint"](https://www.edgedb.com/docs/edgeql/ddl/constraints#create-constraint).

@elprans I think it's a bug that we allow non-delegated constraints to be defined on abstract types, right?
I think non-delegated constraints on abstract types are a valid use case, albeit an uncommon one.  A non-delegated `exclusive` constraint on an abstract parent means that a value must be unique across all descendants as opposed to within a subset of each individual descendant type.

The original error should be a relatively easy fix.
> A non-delegated exclusive constraint on an abstract parent means that a value must be unique across all descendants as opposed to within a subset of each individual descendant type.

For my use case it wasn't important. But I did know nothing about delegated constraints. So if for any reason you decide that non-delegated constraint is hard to be implemented in short-term (or at all), we should error out on creation of constraint and give a hint about delegated one.
To be clear, the work item right now is just fixing the error message? Or is it actually supporting this?
Oh, I guess it hopefully won't be hard to fix since this works for properties.
This is a bug in constraint inheritance, which needs to be fixed.

Resolving patch:
diff --git a/edb/pgsql/common.py b/edb/pgsql/common.py
--- a/edb/pgsql/common.py
+++ b/edb/pgsql/common.py
@@ -189,6 +189,15 @@ def get_scalar_backend_name(id, module_id, catenate=True, *, aspect=None):
     return convert_name(name, aspect, catenate)
 
 
+def get_aspect_suffix(aspect):
+    if aspect == 'table':
+        return ''
+    elif aspect == 'inhview':
+        return 't'
+    else:
+        return aspect
+
+
 def get_objtype_backend_name(id, module_id, *, catenate=True, aspect=None):
     if aspect is None:
         aspect = 'table'
@@ -199,13 +208,7 @@ def get_objtype_backend_name(id, module_id, *, catenate=True, aspect=None):
 
     name = s_name.Name(module=str(module_id), name=str(id))
 
-    if aspect == 'table':
-        suffix = ''
-    elif aspect == 'inhview':
-        suffix = 't'
-    else:
-        suffix = aspect
-
+    suffix = get_aspect_suffix(aspect)
     return convert_name(name, suffix=suffix, catenate=catenate)
 
 
@@ -219,13 +222,7 @@ def get_pointer_backend_name(id, module_id, *, catenate=False, aspect=None):
 
     name = s_name.Name(module=str(module_id), name=str(id))
 
-    if aspect == 'table':
-        suffix = ''
-    elif aspect == 'inhview':
-        suffix = 't'
-    else:
-        suffix = aspect
-
+    suffix = get_aspect_suffix(aspect)
     return convert_name(name, suffix=suffix, catenate=catenate)
 
 
diff --git a/edb/pgsql/deltadbops.py b/edb/pgsql/deltadbops.py
--- a/edb/pgsql/deltadbops.py
+++ b/edb/pgsql/deltadbops.py
@@ -191,6 +191,7 @@ def get_trigger_proc_text(self):
             exprdata = expr['exprdata']
             origin_exprdata = origin_expr['exprdata']
 
+            schemaname, tablename = self.get_origin_table_name()
             text = '''
                 PERFORM
                     TRUE
@@ -201,8 +202,8 @@ def get_trigger_proc_text(self):
                 IF FOUND THEN
                   RAISE unique_violation
                       USING
-                          TABLE = '{table[1]}',
-                          SCHEMA = '{table[0]}',
+                          TABLE = '{tablename}',
+                          SCHEMA = '{schemaname}',
                           CONSTRAINT = '{constr}',
                           MESSAGE = '{errmsg}',
                           DETAIL = 'Key ({plain_expr}) already exists.';
@@ -210,7 +211,11 @@ def get_trigger_proc_text(self):
             '''.format(
                 plain_expr=origin_exprdata['plain'],
                 new_expr=exprdata['new'],
-                table=common.qname(*self.get_origin_table_name()),
+                table=common.qname(
+                    schemaname,
+                    tablename + "_" + common.get_aspect_suffix("inhview")),
+                schemaname=schemaname,
+                tablename=tablename,
                 constr=raw_constr_name,
                 errmsg=errmsg,
             )
@@ -226,13 +231,7 @@ def is_multiconstraint(self):
         return self._scope != 'row' and len(self._exprdata) > 1
 
     def requires_triggers(self):
-        return (
-            self._type != 'check'
-            and (
-                self.get_origin_table_name()
-                != self.get_subject_name(quote=False)
-            )
-        )
+        return self._type != 'check'
 
     def __repr__(self):
         return '<{}.{} {!r} at 0x{:x}>'.format(
diff --git a/edb/pgsql/schemamech.py b/edb/pgsql/schemamech.py
--- a/edb/pgsql/schemamech.py
+++ b/edb/pgsql/schemamech.py
@@ -272,11 +272,16 @@ def schema_constraint_to_backend_constraint(
         }
 
         if constraint_origin != constraint:
+            origin_path_prefix_anchor = (
+                qlast.Subject().name
+                if isinstance(origin_subject, s_types.Type) else None
+            )
             origin_ir = qlcompiler.compile_ast_to_ir(
                 constraint_origin.get_finalexpr(schema).qlast,
                 schema,
                 options=qlcompiler.CompilerOptions(
                     anchors={qlast.Subject().name: origin_subject},
+                    path_prefix_anchor=origin_path_prefix_anchor,
                     apply_query_rewrites=not context.stdmode,
                 ),
             )


Test patch:
diff --git a/tests/schemas/tree.esdl b/tests/schemas/tree.esdl
--- a/tests/schemas/tree.esdl
+++ b/tests/schemas/tree.esdl
@@ -35,8 +35,8 @@ type Eert {
         constraint exclusive;
     }
 
-    link parent := .<children[IS Eert];
-    multi link children -> Eert {
-        constraint exclusive;
-    }
+    # We need the limit 1 because children isn't exclusive and we can't
+    # have it be exclusive if we want to do an atomic swap...
+    link parent := (SELECT .<children[IS Eert] LIMIT 1);
+    multi link children -> Eert;
 }
diff --git a/tests/test_constraints.py b/tests/test_constraints.py
--- a/tests/test_constraints.py
+++ b/tests/test_constraints.py
@@ -182,6 +182,21 @@ async def test_constraints_exclusive_across_ancestry(self):
                     };
                 """)
 
+        async with self._run_and_rollback():
+            with self.assertRaisesRegex(
+                    edgedb.ConstraintViolationError,
+                    'name violates exclusivity constraint'):
+
+                await self.con.execute("""
+                    INSERT test::UniqueNameInherited {
+                        name := 'exclusive_name_across'
+                    };
+
+                    INSERT test::UniqueName {
+                        name := 'exclusive_name_across'
+                    };
+                """)
+
         async with self._run_and_rollback():
             await self.con.execute("""
                 INSERT test::UniqueName {
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
@@ -5196,6 +5196,27 @@ async def test_edgeql_ddl_constraint_07(self):
                     };
                 """)
 
+    async def test_edgeql_ddl_constraint_08(self):
+        # Test non-delegated object constraints on abstract types
+        await self.con.execute(r"""
+            CREATE TYPE test::Base {
+                CREATE PROPERTY x -> str {
+                    CREATE CONSTRAINT exclusive;
+                }
+            };
+            CREATE TYPE test::Foo EXTENDING test::Base;
+            CREATE TYPE test::Bar EXTENDING test::Base;
+
+            INSERT test::Foo { x := "a" };
+        """)
+
+        with self.assertRaisesRegex(
+                edgedb.ConstraintViolationError,
+                r'violates exclusivity constraint'):
+            await self.con.execute(r"""
+                INSERT test::Foo { x := "a" };
+            """)
+
     async def test_edgeql_ddl_constraint_alter_01(self):
         await self.con.execute(r"""
             CREATE TYPE test::ConTest01 {
diff --git a/tests/test_edgeql_insert.py b/tests/test_edgeql_insert.py
--- a/tests/test_edgeql_insert.py
+++ b/tests/test_edgeql_insert.py
@@ -2688,3 +2688,36 @@ async def test_edgeql_insert_nested_volatile_01(self):
             ''',
             [2]
         )
+
+    async def test_edgeql_insert_and_update_01(self):
+        # INSERTing something that would violate a constraint while
+        # fixing the violation is still supposed to be an error.
+        await self.con.execute('''
+            INSERT test::Person { name := 'foo' };
+        ''')
+
+        with self.assertRaisesRegex(edgedb.ConstraintViolationError,
+                                    "violates exclusivity constraint"):
+            await self.con.execute('''
+                SELECT (
+                    (UPDATE test::Person FILTER .name = 'foo'
+                        SET { name := 'foo' }),
+                    (INSERT test::Person { name := 'foo' })
+                )
+            ''')
+
+    async def test_edgeql_insert_and_delete_01(self):
+        # INSERTing something that would violate a constraint while
+        # fixing the violation is still supposed to be an error.
+        await self.con.execute('''
+            INSERT test::Person { name := 'foo' };
+        ''')
+
+        with self.assertRaisesRegex(edgedb.ConstraintViolationError,
+                                    "violates exclusivity constraint"):
+            await self.con.execute('''
+                SELECT (
+                    (DELETE test::Person FILTER .name = 'foo'),
+                    (INSERT test::Person { name := 'foo' })
+                )
+            ''')