Repository: edgedb/edgedb

Issue:
Add support for the validated "required" qualifier in computable declarations
A `REQUIRED` expression would be statically validated to be non-empty. This enables such things as required computables, which is useful for types and views. With this feature views can be reasoned about in a similar fashion to types with required links and properties.

e.g.
```
# if friends are not a required link, this would be valid
SELECT User {
    name,
    foo := REQUIRED (User.friends.name ?? 'n/a')
};

# assuming all links and properties are already required this is valid
SELECT Issue {
    title,
    foo := REQUIRED Issue.owner.email
};
```

An additional benefit is that if the schema changes and some required links or properties stop being required (say, `age` becomes non-required), the views and expressions that relied on this will fail compile-time validation and thus provide an early indication of something being broken.

Syntactically `REQUIRED` keyword would be used similarly to `DETACHED`.


Discussion:
With the rework of cardinality specification in #303, the `REQUIRED` as an expression is not necessary, and should be expressed as a `required` qualifier on the computable declaration, in line with how regular links and properties are defined:

```
SELECT Issue {
    required foo := Issue.owner.email 
};
```


@elprans was this implemented in the linked PR? Close if it was.
@vpetrovykh, please look into writing some tests for this.

Resolving patch:
diff --git a/edb/edgeql/compiler/stmtctx.py b/edb/edgeql/compiler/stmtctx.py
--- a/edb/edgeql/compiler/stmtctx.py
+++ b/edb/edgeql/compiler/stmtctx.py
@@ -603,11 +603,13 @@ def pend_pointer_cardinality_inference(
         callbacks = []
 
     # Convert the SchemaCardinality into Cardinality used for inference.
-    if specified_card is None:
+    if not specified_required and specified_card is None:
         sc = None
     else:
         sc = qltypes.Cardinality.from_schema_value(
-            specified_required, specified_card)
+            specified_required,
+            specified_card or qltypes.SchemaCardinality.ONE
+        )
 
     ctx.pending_cardinality[ptrcls] = context.PendingCardinality(
         specified_cardinality=sc,


Test patch:
diff --git a/tests/test_edgeql_select.py b/tests/test_edgeql_select.py
--- a/tests/test_edgeql_select.py
+++ b/tests/test_edgeql_select.py
@@ -523,6 +523,196 @@ async def test_edgeql_select_computable_18(self):
                 }]
             )
 
+    async def test_edgeql_select_computable_19(self):
+        await self.assert_query_result(
+            r"""
+            WITH MODULE test
+            SELECT Issue{
+                number,
+                required foo := 42,
+            }
+            FILTER Issue.number = '1';
+            """,
+            [{
+                'number': '1',
+                'foo': 42,
+            }]
+        )
+
+    async def test_edgeql_select_computable_20(self):
+        await self.assert_query_result(
+            r"""
+            WITH MODULE test
+            SELECT Issue{
+                number,
+                required foo := <int64>.number,
+                required single bar := <int64>.number,
+                required multi baz := <int64>.number,
+                optional te := <str>.time_estimate,
+            }
+            FILTER Issue.number = '1';
+            """,
+            [{
+                'number': '1',
+                'foo': 1,
+                'bar': 1,
+                'baz': {1},
+                'te': '3000',
+            }]
+        )
+
+    async def test_edgeql_select_computable_21(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r"possibly an empty set returned by an expression for "
+                r"a computable property 'foo' declared as 'required'",
+                _position=111):
+            await self.con.fetchall("""\
+                WITH MODULE test
+                SELECT Issue{
+                    number,
+                    required foo := <int64>{},
+                }
+                FILTER Issue.number = '1';
+            """)
+
+    async def test_edgeql_select_computable_22(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r"possibly an empty set returned by an expression for "
+                r"a computable property 'foo' declared as 'required'",
+                _position=111):
+            await self.con.fetchall("""\
+                WITH MODULE test
+                SELECT Issue{
+                    number,
+                    required single foo := <int64>{},
+                }
+                FILTER Issue.number = '1';
+            """)
+
+    async def test_edgeql_select_computable_23(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r"possibly an empty set returned by an expression for "
+                r"a computable property 'foo' declared as 'required'",
+                _position=111):
+            await self.con.fetchall("""\
+                WITH MODULE test
+                SELECT Issue{
+                    number,
+                    required multi foo := <int64>{},
+                }
+                FILTER Issue.number = '1';
+            """)
+
+    async def test_edgeql_select_computable_24(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r"possibly more than one element returned by an expression "
+                r"for a computable property 'foo' declared as 'single'",
+                _position=111):
+            await self.con.fetchall("""\
+                WITH MODULE test
+                SELECT Issue{
+                    number,
+                    required single foo := {1, 2},
+                }
+                FILTER Issue.number = '1';
+            """)
+
+    async def test_edgeql_select_computable_25(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r"possibly an empty set returned by an expression for "
+                r"a computable property 'foo' declared as 'required'",
+                _position=111):
+            await self.con.fetchall("""\
+                WITH MODULE test
+                SELECT Issue{
+                    number,
+                    required foo := <str>.time_estimate,
+                }
+                FILTER Issue.number = '1';
+            """)
+
+    async def test_edgeql_select_computable_26(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r"possibly an empty set returned by an expression for "
+                r"a computable property 'foo' declared as 'required'",
+                _position=111):
+            await self.con.fetchall("""\
+                WITH MODULE test
+                SELECT Issue{
+                    number,
+                    required single foo := <str>.time_estimate,
+                }
+                FILTER Issue.number = '1';
+            """)
+
+    async def test_edgeql_select_computable_27(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r"possibly an empty set returned by an expression for "
+                r"a computable property 'foo' declared as 'required'",
+                _position=111):
+            await self.con.fetchall("""\
+                WITH MODULE test
+                SELECT Issue{
+                    number,
+                    required multi foo := <str>.time_estimate,
+                }
+                FILTER Issue.number = '1';
+            """)
+
+    async def test_edgeql_select_computable_28(self):
+        await self.assert_query_result(
+            r"""
+            WITH MODULE test
+            SELECT Issue{
+                number,
+                required foo := .owner{
+                    name
+                },
+                required single bar := .owner{
+                    name
+                },
+                required multi baz := .owner{
+                    name
+                },
+            }
+            FILTER Issue.number = '1';
+            """,
+            [{
+                'number': '1',
+                'foo': {
+                    'name': 'Elvis'
+                },
+                'bar': {
+                    'name': 'Elvis'
+                },
+                'baz': [{
+                    'name': 'Elvis'
+                }],
+            }]
+        )
+
+    async def test_edgeql_select_computable_29(self):
+        with self.assertRaisesRegex(
+                edgedb.QueryError,
+                r"possibly an empty set returned by an expression for "
+                r"a computable link 'foo' declared as 'required'",
+                _position=111):
+            await self.con.fetchall("""\
+                WITH MODULE test
+                SELECT Issue{
+                    number,
+                    required multi foo := .owner.todo,
+                }
+                FILTER Issue.number = '1';
+            """)
+
     async def test_edgeql_select_match_01(self):
         await self.assert_query_result(
             r"""