Repository: edgedb/edgedb

Issue:
Constraints get messed up when pointer is changed between single and multi
If we create a multi property with an exclusive constraint and then make it single, we aren't able to drop the constraint, property, or type anymore:
```
_localdev:main> create type B { create multi property x -> str { 
...............   create constraint exclusive; 
............... } };
OK: CREATE TYPE
_localdev:main> alter type B alter property x set single using ('1');
OK: ALTER TYPE
_localdev:main> drop type B;
edgedb error: InternalServerError: trigger "eb94e329-0417-11ef-9598-27c6b5059780;schemaconstr_instrigger" for table "eb92edec-0417-11ef-8595-95ce16677f5d" does not exist
  Server traceback:
      edb.errors.InternalServerError: trigger "eb94e329-0417-11ef-9598-27c6b5059780;schemaconstr_instrigger" for table "eb92edec-0417-11ef-8595-95ce16677f5d" does not exist
```

The other direction thankfully fails before anything gets corrupted:
```
_localdev:main> create type B { create property x -> str { 
...............   create constraint exclusive; 
............... } };
OK: CREATE TYPE
_localdev:main> alter type B alter property x set multi;
edgedb error: InternalServerError: cannot drop column be5e9730-0423-11ef-9df0-979f1042b227 of table edgedbpub."be5d1185-0423-11ef-89cc-01051615fa78" because other objects depend on it
  Server traceback:
      edb.errors.InternalServerError: cannot drop column be5e9730-0423-11ef-9df0-979f1042b227 of table edgedbpub."be5d1185-0423-11ef-89cc-01051615fa78" because other objects depend on it
```

The issue is that we aren't updating the constraints properly


Discussion:

Resolving patch:
diff --git a/edb/pgsql/dbops/constraints.py b/edb/pgsql/dbops/constraints.py
--- a/edb/pgsql/dbops/constraints.py
+++ b/edb/pgsql/dbops/constraints.py
@@ -21,8 +21,6 @@
 
 from typing import Optional, Sequence, List
 
-from edb.pgsql import ast as pgast
-
 from .. import common
 from . import base
 
@@ -30,10 +28,10 @@
 class Constraint(base.DBObject):
     def __init__(
         self,
-        subject_name: Sequence[str | pgast.Star],
+        subject_name: Sequence[str],
         constraint_name: Optional[str] = None,
     ):
-        self._subject_name = subject_name
+        self._subject_name = tuple(subject_name)
         self._constraint_name = constraint_name
 
     def get_type(self):
diff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py
--- a/edb/pgsql/delta.py
+++ b/edb/pgsql/delta.py
@@ -2285,6 +2285,9 @@ def apply(self, schema, context):
             return schema
 
         if subject is not None:
+            if pcontext := context.get(s_pointers.PointerCommandContext):
+                orig_schema = pcontext.original_schema
+
             bconstr = schemamech.compile_constraint(
                 subject, constraint, schema, self.span
             )
@@ -2343,14 +2346,15 @@ def apply(self, schema, context):
                 and not context.is_creating(subject)
                 and not context.is_deleting(subject)
             ):
-                op = self.enforce_constraint(
-                    constraint, schema, self.span
-                )
                 self.schedule_post_inhview_update_command(
                     schema,
                     context,
                     (lambda nschema, ncontext:
-                     op if nschema.has_object(subject.id) else None),
+                     self.enforce_constraint(
+                         constraint, nschema, self.span
+                     )
+                     if nschema.has_object(constraint.id)
+                     else None),
                     s_sources.SourceCommandContext)
 
             self.pgops.add(self.fixup_base_constraint_triggers(
@@ -4277,6 +4281,18 @@ def _alter_pointer_cardinality(
         if isinstance(source_op, sd.CreateObject):
             return
 
+        # If the pointer has any constraints, drop them now. We'll
+        # create them again at the end.
+        # N.B: Since the pointer is either starting or ending as multi,
+        # it can't have any object constraints referencing it.
+        # TODO?: Maybe we should handle the constraint by generating
+        # an alter in the front-end and running _alter_innards in the
+        # middle of this function. (After creations, before deletions.)
+        for constr in ptr.get_constraints(schema).objects(schema):
+            self.pgops.add(ConstraintCommand.delete_constraint(
+                constr, orig_schema
+            ))
+
         assert ptr_stor_info.table_name
         tab = q(*ptr_stor_info.table_name)
         target_col = ptr_stor_info.column_name
@@ -4415,6 +4431,11 @@ def _alter_pointer_cardinality(
             alter_table.add_operation(dbops.AlterTableDropColumn(col))
             self.pgops.add(alter_table)
 
+        for constr in ptr.get_constraints(schema).objects(schema):
+            self.pgops.add(ConstraintCommand.create_constraint(
+                constr, schema
+            ))
+
     def _alter_pointer_optionality(
         self,
         schema: s_schema.Schema,
diff --git a/edb/pgsql/deltadbops.py b/edb/pgsql/deltadbops.py
--- a/edb/pgsql/deltadbops.py
+++ b/edb/pgsql/deltadbops.py
@@ -543,13 +543,14 @@ def update_constraint_enabled(
 
     def drop_constraint(self, constraint: SchemaConstraintTableConstraint):
         if constraint.requires_triggers():
-            self.add_commands(self.drop_constr_trigger(self.name, constraint))
+            self.add_commands(self.drop_constr_trigger(
+                constraint._subject_name, constraint))
             proc_name = constraint.get_trigger_procname()
             self.add_commands(self.drop_constr_trigger_function(proc_name))
 
         # Drop the constraint normally from our table
         #
-        my_alter = dbops.AlterTable(self.name)
+        my_alter = dbops.AlterTable(constraint._subject_name)
 
         drop_constr = AlterTableDropMultiConstraint(constraint=constraint)
         my_alter.add_command(drop_constr)


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
@@ -3129,6 +3129,76 @@ async def test_edgeql_ddl_ptr_set_cardinality_01(self):
             ]
         )
 
+    async def test_edgeql_ddl_ptr_set_cardinality_02(self):
+        await self.con.execute(r"""
+            create type B {
+                create multi property x -> str {
+                    create constraint exclusive;
+                };
+            };
+            create type C extending B;
+        """)
+
+        await self.con.execute(r"""
+            alter type B alter property x set single using (select .x limit 1);
+        """)
+
+        await self.con.execute('''
+            insert B { x := 'a' };
+        ''')
+        async with self.assertRaisesRegexTx(
+            edgedb.ConstraintViolationError, ''
+        ):
+            await self.con.execute('''
+                insert B { x := 'a' };
+            ''')
+        async with self.assertRaisesRegexTx(
+            edgedb.ConstraintViolationError, ''
+        ):
+            await self.con.execute('''
+                insert C { x := 'a' };
+            ''')
+
+        await self.con.execute(r"""
+            drop type C;
+            drop type B;
+        """)
+
+    async def test_edgeql_ddl_ptr_set_cardinality_03(self):
+        await self.con.execute(r"""
+            create type B {
+                create property x -> str {
+                    create constraint exclusive;
+                }
+            };
+            create type C extending B;
+        """)
+
+        await self.con.execute(r"""
+            alter type B alter property x set multi;
+        """)
+
+        await self.con.execute('''
+            insert B { x := 'a' };
+        ''')
+        async with self.assertRaisesRegexTx(
+            edgedb.ConstraintViolationError, ''
+        ):
+            await self.con.execute('''
+                insert B { x := 'a' };
+            ''')
+        async with self.assertRaisesRegexTx(
+            edgedb.ConstraintViolationError, ''
+        ):
+            await self.con.execute('''
+                insert C { x := 'a' };
+            ''')
+
+        await self.con.execute(r"""
+            drop type C;
+            drop type B;
+        """)
+
     async def test_edgeql_ddl_ptr_set_cardinality_validation(self):
         await self.con.execute(r"""
             CREATE TYPE Bar;
@@ -3203,6 +3273,79 @@ async def test_edgeql_ddl_ptr_set_cardinality_validation(self):
                 ALTER TYPE Foo ALTER LINK l SET SINGLE USING (SELECT Bar)
             """)
 
+    async def test_edgeql_ddl_ptr_using_01(self):
+        await self.con.execute(r"""
+            create type B {
+                create property y -> str;
+                create property x -> str {
+                    create constraint exclusive;
+                };
+                create constraint exclusive on (.x);
+            };
+            create type C extending B;
+        """)
+
+        await self.con.execute(r"""
+            alter type B alter property x using (.y);
+        """)
+
+        await self.con.execute('''
+            insert B { y := 'a' };
+        ''')
+        async with self.assertRaisesRegexTx(
+            edgedb.ConstraintViolationError, ''
+        ):
+            await self.con.execute('''
+                insert B { y := 'a' };
+            ''')
+        async with self.assertRaisesRegexTx(
+            edgedb.ConstraintViolationError, ''
+        ):
+            await self.con.execute('''
+                insert C { y := 'a' };
+            ''')
+
+        await self.con.execute(r"""
+            drop type C;
+            drop type B;
+        """)
+
+    async def test_edgeql_ddl_ptr_using_02(self):
+        await self.con.execute(r"""
+            create type B {
+                create multi property y -> str;
+                create multi property x -> str {
+                    create constraint exclusive;
+                };
+            };
+            create type C extending B;
+        """)
+
+        await self.con.execute(r"""
+            alter type B alter property x using (.y);
+        """)
+
+        await self.con.execute('''
+            insert B { y := 'a' };
+        ''')
+        async with self.assertRaisesRegexTx(
+            edgedb.ConstraintViolationError, ''
+        ):
+            await self.con.execute('''
+                insert B { y := 'a' };
+            ''')
+        async with self.assertRaisesRegexTx(
+            edgedb.ConstraintViolationError, ''
+        ):
+            await self.con.execute('''
+                insert C { y := 'a' };
+            ''')
+
+        await self.con.execute(r"""
+            drop type C;
+            drop type B;
+        """)
+
     async def test_edgeql_ddl_ptr_set_required_01(self):
         await self.con.execute(r"""