Repository: edgedb/edgedb

Issue:
Make scope tree unique_ids actually be unique
We do this by getting rid of the use of "temporary" scopes in
declare_view (an approach that I previewed in #1806).

The purpose of using the temporary scopes was I think to allow the use
of the extra_scope mechanism to place the scope tree for a WITH
binding *underneath* the scope tree of a use of it. Unfortunately this
could cause duplication of unique_ids when it got grafted into two
independent subqueries, as well as some of other issues when the
fusing didn't behave right.

Fortunately, all that extra_scope machinery wasn't really needed. It
seems to work basically just fine to stick it in the scope tree where
it appears.

This fixes a handful of bugs, but also introduces some failures:
 * test_edgeql_tree_update_05 (flipping two nodes) generates invalid
   SQL, but I think fixing weak namespace stripping should fix this.
 * test_edgeql_volatility_with_01 (now split into a new test)
   fails because we now fail to detect volatile WITH bindings
   in situations like `X := random(), Y := X`. Fixing this should
   be straightforward.
 * test_edgeql_select_tvariant_04 fails because we now fail to
   deduplicate computed link sets in some cases. I haven't dug
   into this one yet.

This is work towards fixing #1381.

I'm putting this up now to get eyes on it, but if we want to wait on
landing it for me to fix some of the remaining problems, that would
make sense.

This wound up being a very long and painful journey to a pretty small
patch.


Discussion:

Resolving patch:
diff --git a/edb/schema/functions.py b/edb/schema/functions.py
--- a/edb/schema/functions.py
+++ b/edb/schema/functions.py
@@ -2282,5 +2282,15 @@ def compile_function(
             ),
             context=body.qlast.context,
         )
+    elif (return_typemod is ft.TypeModifier.SingletonType
+            and ir.cardinality.can_be_zero()):
+        raise errors.InvalidFunctionDefinitionError(
+            f'return cardinality mismatch in function declared to return '
+            f'exactly one value',
+            details=(
+                f'Function may return an empty set.'
+            ),
+            context=body.qlast.context,
+        )
 
     return compiled


Test patch:
diff --git a/tests/test_edgeql_calls.py b/tests/test_edgeql_calls.py
--- a/tests/test_edgeql_calls.py
+++ b/tests/test_edgeql_calls.py
@@ -1357,7 +1357,7 @@ async def test_edgeql_calls_38(self):
             INSERT C38 { name := 'yay' };
             CREATE FUNCTION call38(
                 a: C38
-            ) -> str
+            ) -> OPTIONAL str
                 USING (
                     SELECT a.name
                 );
@@ -1384,8 +1384,8 @@ async def test_edgeql_calls_39(self):
     async def test_edgeql_calls_40(self):
         await self.con.execute('''
             CREATE TYPE Rectangle {
-                CREATE PROPERTY width -> int64;
-                CREATE PROPERTY height -> int64;
+                CREATE REQUIRED PROPERTY width -> int64;
+                CREATE REQUIRED PROPERTY height -> int64;
             };
 
             INSERT Rectangle { width := 2, height := 3 };
@@ -1437,12 +1437,12 @@ async def test_edgeql_calls_obj_01(self):
             CREATE TYPE Shape;
             CREATE TYPE FlatShape;
             CREATE TYPE Rectangle EXTENDING FlatShape {
-                CREATE PROPERTY w -> float64;
-                CREATE PROPERTY h -> float64;
+                CREATE REQUIRED PROPERTY w -> float64;
+                CREATE REQUIRED PROPERTY h -> float64;
             };
 
             CREATE TYPE Circle EXTENDING FlatShape {
-                CREATE PROPERTY r -> float64;
+                CREATE REQUIRED PROPERTY r -> float64;
             };
 
             # Use -1 as the error indicator, as we don't have the means
@@ -1572,7 +1572,7 @@ async def test_edgeql_calls_obj_02(self):
     async def test_edgeql_calls_obj_03(self):
         await self.con.execute("""
             CREATE TYPE Person {
-                CREATE PROPERTY name -> str;
+                CREATE REQUIRED PROPERTY name -> str;
             };
             CREATE FUNCTION fight(one: Person, two: Person) -> str
                 USING (one.name ++ " fights " ++ two.name);
@@ -1662,14 +1662,14 @@ async def test_edgeql_calls_obj_03(self):
 
     async def test_edgeql_calls_obj_04(self):
         await self.con.execute("""
-            CREATE FUNCTION thing(s: schema::Constraint) -> str
+            CREATE FUNCTION thing(s: schema::Constraint) -> OPTIONAL str
                 USING (s.name ++ s.expr);
 
             CREATE FUNCTION frob(s: schema::Object) -> str
                 USING ("ahhhh");
-            CREATE FUNCTION frob(s: schema::Constraint) -> str
+            CREATE FUNCTION frob(s: schema::Constraint) -> OPTIONAL str
                 USING (s.name ++ s.expr);
-            CREATE FUNCTION frob(s: schema::Pointer) -> str
+            CREATE FUNCTION frob(s: schema::Pointer) -> OPTIONAL str
                 USING (s.name ++ <str>s.required);
         """)
 
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
@@ -3438,7 +3438,7 @@ async def test_edgeql_ddl_function_04(self):
                 $$;
 
             CREATE FUNCTION my_edgeql_func2(s: std::str)
-                -> schema::ObjectType
+                -> OPTIONAL schema::ObjectType
                 USING EdgeQL $$
                     SELECT
                         schema::ObjectType
@@ -4299,6 +4299,19 @@ async def test_edgeql_ddl_function_33(self):
             }],
         )
 
+    async def test_edgeql_ddl_function_34(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidFunctionDefinitionError,
+            r"return cardinality mismatch"
+        ):
+            await self.con.execute(r"""
+                CREATE FUNCTION broken_edgeql_func25(
+                    a: std::int64) -> std::int64
+                USING EdgeQL $$
+                    SELECT a FILTER a > 0
+                $$;
+            """)
+
     async def test_edgeql_ddl_function_rename_01(self):
         await self.con.execute("""
             CREATE FUNCTION foo(s: str) -> str {
@@ -4656,7 +4669,7 @@ async def test_edgeql_ddl_function_volatility_08(self):
 
     async def test_edgeql_ddl_function_volatility_09(self):
         await self.con.execute('''
-            CREATE TYPE FuncVol { CREATE PROPERTY i -> int64 };
+            CREATE TYPE FuncVol { CREATE REQUIRED PROPERTY i -> int64 };
             CREATE FUNCTION obj_func(obj: FuncVol) -> int64 {
                 USING (obj.i)
             };
@@ -10334,7 +10347,7 @@ async def _simple_rename_ref_tests(self, ddl, cleanup=None, **kwargs):
     async def test_edgeql_ddl_rename_ref_function_01(self):
         await self._simple_rename_ref_tests(
             """
-            CREATE FUNCTION foo(x: Note) ->  str {
+            CREATE FUNCTION foo(x: Note) -> OPTIONAL str {
                 USING (SELECT ('Note note ' ++ x.note ++
                                (SELECT Note.note LIMIT 1)))
             }
@@ -10358,7 +10371,7 @@ async def test_edgeql_ddl_rename_ref_function_02(self):
                 CREATE PROPERTY name -> str;
             };
 
-            CREATE FUNCTION foo(x: Note, y: Name) -> str {
+            CREATE FUNCTION foo(x: Note, y: Name) -> OPTIONAL str {
                 USING (SELECT (x.note ++ " " ++ y.name))
             };
         """)
@@ -10400,7 +10413,7 @@ async def test_edgeql_ddl_rename_ref_function_02(self):
     async def test_edgeql_ddl_rename_ref_function_03(self):
         await self._simple_rename_ref_tests(
             """
-            CREATE FUNCTION foo(x: str) -> Note {
+            CREATE FUNCTION foo(x: str) -> OPTIONAL Note {
                 USING (SELECT Note FILTER .note = x LIMIT 1)
             }
             """,
@@ -10411,7 +10424,7 @@ async def test_edgeql_ddl_rename_ref_function_03(self):
     async def test_edgeql_ddl_rename_ref_function_04(self):
         await self._simple_rename_ref_tests(
             """
-            CREATE FUNCTION foo(x: str) -> Note {
+            CREATE FUNCTION foo(x: str) -> OPTIONAL Note {
                 USING (SELECT Note FILTER .note = x LIMIT 1)
             }
             """,
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1708,7 +1708,7 @@ def test_schema_recursive_05(self):
             self.run_ddl(schema, r'''
                 ALTER FUNCTION foo(v: int64) USING (
                     # This is very broken now
-                    1 + (SELECT Foo LIMIT 1).val
+                    assert_exists(1 + (SELECT Foo LIMIT 1).val)
                 );
             ''')
 
@@ -1718,7 +1718,7 @@ def test_schema_recursive_06(self):
                 property val := 1;
             }
 
-            function foo(v: int64) -> int64 using (
+            function foo(v: int64) -> optional int64 using (
                 1 + (SELECT Foo LIMIT 1).val
             );
         ''', modname='default')
@@ -2553,7 +2553,8 @@ def test_schema_get_migration_34(self):
 
     def test_schema_get_migration_35(self):
         schema = r'''
-            function bar() -> str using(SELECT <str>Object.id LIMIT 1);
+            function bar() -> optional str using(
+                SELECT <str>Object.id LIMIT 1);
         '''
 
         self._assert_migration_consistency(schema)
@@ -4535,7 +4536,7 @@ def test_schema_migrations_equivalence_function_16(self):
                 property bar -> array<int64>;
             };
 
-            function hello16() -> int64
+            function hello16() -> optional int64
                 using (
                     SELECT len((SELECT Foo LIMIT 1).bar)
                 )
@@ -4544,7 +4545,7 @@ def test_schema_migrations_equivalence_function_16(self):
                 property bar -> array<float64>;
             };
 
-            function hello16() -> int64
+            function hello16() -> optional int64
                 using (
                     SELECT len((SELECT Foo LIMIT 1).bar)
                 )
@@ -4559,7 +4560,7 @@ def test_schema_migrations_equivalence_function_17(self):
 
             type Bar;
 
-            function hello17() -> Bar
+            function hello17() -> optional Bar
                 using (
                     SELECT Bar
                     OFFSET len((SELECT Foo.bar LIMIT 1)) ?? 0
@@ -4572,7 +4573,7 @@ def test_schema_migrations_equivalence_function_17(self):
 
             type Bar;
 
-            function hello17() -> Bar
+            function hello17() -> optional Bar
                 using (
                     SELECT Bar
                     OFFSET len((SELECT Foo.bar LIMIT 1)) ?? 0
@@ -4764,7 +4765,7 @@ def test_schema_migrations_equivalence_recursive_06(self):
                     property val := 1;
                 }
 
-                function foo(v: int64) -> int64 using (
+                function foo(v: int64) -> optional int64 using (
                     1 + (SELECT Foo LIMIT 1).val
                 );
             """, r"""
@@ -4773,7 +4774,7 @@ def test_schema_migrations_equivalence_recursive_06(self):
                     property val := foo(1);
                 }
 
-                function foo(v: int64) -> int64 using (
+                function foo(v: int64) -> optional int64 using (
                     1 + (SELECT Foo LIMIT 1).val
                 );
             """])