Repository: edgedb/edgedb

Issue:
No enforcement of newly created required multi pointers
```
edgedb> create type Foo { create multi property tags -> str; };
OK: CREATE TYPE
edgedb> insert Foo;
[{"id": "290bb6ac-a79c-11eb-9b61-b7d69260fef9"}]
edgedb> alter type Foo alter property tags set required;
OK: ALTER TYPE
edgedb> alter type Foo create required multi property tags2 -> str;
OK: ALTER TYPE
edgedb> select Foo {tags, tags2};
[{"tags": [], "tags2": []}]
```


Discussion:

Resolving patch:
diff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py
--- a/edb/pgsql/delta.py
+++ b/edb/pgsql/delta.py
@@ -2715,6 +2715,8 @@ def _alter_pointer_optionality(
         schema: s_schema.Schema,
         orig_schema: s_schema.Schema,
         context: sd.CommandContext,
+        *,
+        fill_expr: Optional[s_expr.Expression],
     ) -> None:
         new_required = self.scls.get_required(schema)
 
@@ -2735,11 +2737,28 @@ def _alter_pointer_optionality(
 
         ops = dbops.CommandGroup(priority=1)
 
-        if self.fill_expr is not None:
+        # For multi pointers, if there is no fill expression, we
+        # synthesize a bogus one so that an error will trip if there
+        # are any objects with empty values.
+        if fill_expr is None and is_multi:
+            if (
+                ptr.get_cardinality(schema).is_multi()
+                and fill_expr is None
+                and (target := ptr.get_target(schema))
+            ):
+                fill_ast = ql_ast.TypeCast(
+                    expr=ql_ast.Set(elements=[]),
+                    type=s_utils.typeref_to_ast(schema, target),
+                )
+                fill_expr = s_expr.Expression.from_ast(
+                    qltree=fill_ast, schema=schema
+                )
+
+        if fill_expr is not None:
             _, fill_sql_expr, orig_rel_alias, _ = (
                 self._compile_conversion_expr(
                     pointer=ptr,
-                    conv_expr=self.fill_expr,
+                    conv_expr=fill_expr,
                     schema=schema,
                     orig_schema=orig_schema,
                     context=context,
@@ -3400,12 +3419,12 @@ def apply(
             ptr_stor_info = types.get_pointer_storage_info(
                 link, resolve_type=False, schema=schema)
 
+            sets_required = bool(
+                self.get_subcommands(
+                    type=s_pointers.AlterPointerLowerCardinality))
+
             if ptr_stor_info.table_type == 'ObjectType':
                 default_value = self.get_pointer_default(link, schema, context)
-                sets_required = bool(
-                    self.get_subcommands(
-                        type=s_pointers.AlterPointerLowerCardinality))
-
                 cols = self.get_columns(
                     link, schema, default_value, sets_required)
                 table_name = common.get_backend_name(
@@ -3462,6 +3481,17 @@ def apply(
                     update_descendants=True,
                 )
 
+            # If we're creating a required multi pointer without a SET
+            # REQUIRED USING inside, run the alter_pointer_optionality
+            # path to produce an error if there is existing data.
+            if (
+                link.get_cardinality(schema).is_multi()
+                and link.get_required(schema)
+                and not sets_required
+            ):
+                self._alter_pointer_optionality(
+                    schema, schema, context, fill_expr=None)
+
         objtype = context.get(s_objtypes.ObjectTypeCommandContext)
 
         self.attach_alter_table(context)
@@ -3575,7 +3605,8 @@ def apply(
                 and not self.scls.is_endpoint_pointer(schema)
                 and orig_required != new_required
             ):
-                self._alter_pointer_optionality(schema, orig_schema, context)
+                self._alter_pointer_optionality(
+                    schema, orig_schema, context, fill_expr=self.fill_expr)
 
         return schema
 
@@ -3787,6 +3818,9 @@ def apply(
             ptr_stor_info = types.get_pointer_storage_info(
                 prop, resolve_type=False, schema=schema)
 
+            sets_required = bool(
+                self.get_subcommands(
+                    type=s_pointers.AlterPointerLowerCardinality))
             if (
                 (
                     not isinstance(src.scls, s_objtypes.ObjectType)
@@ -3805,9 +3839,6 @@ def apply(
                 )
 
                 default_value = self.get_pointer_default(prop, schema, context)
-                sets_required = bool(
-                    self.get_subcommands(
-                        type=s_pointers.AlterPointerLowerCardinality))
 
                 cols = self.get_columns(
                     prop, schema, default_value, sets_required)
@@ -3837,6 +3868,17 @@ def apply(
                         update_descendants=True,
                     )
 
+            # If we're creating a required multi pointer without a SET
+            # REQUIRED USING inside, run the alter_pointer_optionality
+            # path to produce an error if there is existing data.
+            if (
+                prop.get_cardinality(schema).is_multi()
+                and prop.get_required(schema)
+                and not sets_required
+            ):
+                self._alter_pointer_optionality(
+                    schema, schema, context, fill_expr=None)
+
         return schema
 
 
@@ -3941,7 +3983,8 @@ def apply(
                 and not self.scls.is_endpoint_pointer(schema)
                 and orig_required != new_required
             ):
-                self._alter_pointer_optionality(schema, orig_schema, context)
+                self._alter_pointer_optionality(
+                    schema, orig_schema, context, fill_expr=self.fill_expr)
 
         return schema
 


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
@@ -11115,6 +11115,74 @@ async def test_edgeql_ddl_new_required_pointer_05(self):
             ]
         )
 
+    async def test_edgeql_ddl_new_required_multi_pointer_01(self):
+        await self.con.execute(r"""
+            SET MODULE test;
+            CREATE TYPE Foo;
+            INSERT Foo;
+        """)
+
+        async with self.assertRaisesRegexTx(
+            edgedb.MissingRequiredError,
+            "missing value for required property 'name' of object type "
+            "'test::Foo'"
+        ):
+            await self.con.execute("""
+                ALTER TYPE Foo CREATE REQUIRED MULTI PROPERTY name -> str;
+            """)
+
+    async def test_edgeql_ddl_new_required_multi_pointer_02(self):
+        await self.con.execute(r"""
+            SET MODULE test;
+            CREATE TYPE Foo;
+            INSERT Foo;
+        """)
+
+        async with self.assertRaisesRegexTx(
+            edgedb.MissingRequiredError,
+            "missing value for required link 'link' of object type "
+            "'test::Foo'"
+        ):
+            await self.con.execute("""
+                ALTER TYPE Foo CREATE REQUIRED MULTI LINK link -> Object;
+            """)
+
+    async def test_edgeql_ddl_new_required_multi_pointer_03(self):
+        await self.con.execute(r"""
+            SET MODULE test;
+            CREATE TYPE Foo {
+                CREATE MULTI PROPERTY name -> str;
+            };
+            INSERT Foo;
+        """)
+
+        async with self.assertRaisesRegexTx(
+            edgedb.MissingRequiredError,
+            "missing value for required property 'name' of object type "
+            "'test::Foo'"
+        ):
+            await self.con.execute("""
+                ALTER TYPE Foo ALTER PROPERTY name SET REQUIRED;
+            """)
+
+    async def test_edgeql_ddl_new_required_multi_pointer_04(self):
+        await self.con.execute(r"""
+            SET MODULE test;
+            CREATE TYPE Foo {
+                CREATE MULTI LINK link -> Object;
+            };
+            INSERT Foo;
+        """)
+
+        async with self.assertRaisesRegexTx(
+            edgedb.MissingRequiredError,
+            "missing value for required link 'link' of object type "
+            "'test::Foo'"
+        ):
+            await self.con.execute("""
+                ALTER TYPE Foo ALTER LINK link SET REQUIRED;
+            """)
+
     async def test_edgeql_ddl_alter_union_01(self):
         await self.con.execute(r"""
             SET MODULE test;