Repository: edgedb/edgedb

Issue:
`InternalServerError` when trying to coalesce with an existing link property value


- EdgeDB Version: 5.3+a480d96
- EdgeDB CLI Version: 5.1.0+8ff9f85
- OS Version: macOS 14.4.1

Steps to Reproduce:

1. Migrate the schema
2. Add a person (`insert Person {name := 'Doc'};`)
3. Add a queue with that person at position 1 (`insert Queue {people := (select Person {@position := [1]})};`)
4. Add that person again to the queue by adding another position to their link property array (`update Queue set {people += (select .people {@position := (@position ?? []) ++ [2]} filter .name = 'Doc')};`

Schema:
```esdl
module default {
  type Person {
    required name: str;
  }
  type Queue {
    multi people: Person {
      position: array<int64>;
    }
  }
}
```

A single person may have multiple positions in a queue. Imagine a presale for event tickets that allows each person to purchase multiple tickets, but they must obtain a new position in the queue for each ticket. (I never said it was a well-designed event presale 😅) The goal here is to come up with a query that, without knowing whether a given person is already in the queue, could either add them to it with the desired position or add the desired new position to their existing `position` link property array. My idea was to coalesce the existing link property value with an empty set and concatenate an array with the new value with the result. That ended in an ISE:

```
edgedb error: InternalServerError: AssertionError:
  Hint: This is most likely a bug in EdgeDB. Please consider opening an issue ticket at https://github.com/edgedb/edgedb/issues/new?template=bug_report.md
  Server traceback:
      Traceback (most recent call last):
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/server/compiler_pool/worker.py", line 178, in compile
          units, cstate = COMPILER.compile_request(
                          ^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 858, in compile_request
          units, cstate = self.compile(
                          ^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 938, in compile
          unit_group = compile(ctx=ctx, source=source)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 2589, in compile
          return _try_compile(ctx=ctx, source=source)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 2667, in _try_compile
          comp, capabilities = _compile_dispatch_ql(
                               ^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 2562, in _compile_dispatch_ql
          query = _compile_ql_query(
                  ^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/server/compiler/compiler.py", line 1870, in _compile_ql_query
          ir = qlcompiler.compile_ast_to_ir(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/__init__.py", line 192, in wrapper
          return func(*args, **kwargs)
                 ^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/__init__.py", line 283, in compile_ast_to_ir
          ir_set = dispatch_mod.compile(tree, ctx=ctx)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/functools.py", line 909, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/stmt.py", line 655, in compile_UpdateQuery
          stmt.subject = compile_query_subject(
                         ^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/stmt.py", line 1530, in compile_query_subject
          view_scls, set = viewgen.process_view(
                           ^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/viewgen.py", line 167, in process_view
          view_scls, ir = _process_view(
                          ^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/viewgen.py", line 408, in _process_view
          pointer, ptr_set = _normalize_view_ptr_expr(
                             ^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/viewgen.py", line 1583, in _normalize_view_ptr_expr
          irexpr, sub_view_rptr = _compile_qlexpr(
                                  ^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/viewgen.py", line 1322, in _compile_qlexpr
          irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/functools.py", line 909, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/stmt.py", line 138, in compile_SelectQuery
          stmt.result = compile_result_clause(
                        ^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/stmt.py", line 1391, in compile_result_clause
          ir_result = compile_query_subject(
                      ^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/stmt.py", line 1530, in compile_query_subject
          view_scls, set = viewgen.process_view(
                           ^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/viewgen.py", line 167, in process_view
          view_scls, ir = _process_view(
                          ^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/viewgen.py", line 408, in _process_view
          pointer, ptr_set = _normalize_view_ptr_expr(
                             ^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/viewgen.py", line 1583, in _normalize_view_ptr_expr
          irexpr, sub_view_rptr = _compile_qlexpr(
                                  ^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/viewgen.py", line 1322, in _compile_qlexpr
          irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/functools.py", line 909, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/stmt.py", line 138, in compile_SelectQuery
          stmt.result = compile_result_clause(
                        ^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/stmt.py", line 1387, in compile_result_clause
          expr = dispatch.compile(result_expr, ctx=ectx)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/functools.py", line 909, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/expr.py", line 136, in compile_BinOp
          op_node = func.compile_operator(
                    ^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/func.py", line 381, in compile_operator
          arg_ir = polyres.compile_arg(
                   ^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/polyres.py", line 625, in compile_arg
          arg_ir = dispatch.compile(arg_ql, ctx=argctx)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/functools.py", line 909, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/expr.py", line 136, in compile_BinOp
          op_node = func.compile_operator(
                    ^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/func.py", line 381, in compile_operator
          arg_ir = polyres.compile_arg(
                   ^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/polyres.py", line 625, in compile_arg
          arg_ir = dispatch.compile(arg_ql, ctx=argctx)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/functools.py", line 909, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/stmt.py", line 138, in compile_SelectQuery
          stmt.result = compile_result_clause(
                        ^^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/stmt.py", line 1377, in compile_result_clause
          expr = setgen.new_array_set(
                 ^^^^^^^^^^^^^^^^^^^^^
        File "/Users/raddevon/Library/Application Support/edgedb/portable/5.3/lib/python3.12/site-packages/edb/edgeql/compiler/setgen.py", line 279, in new_array_set
          assert stype.is_array()
      AssertionError
```


Discussion:

Resolving patch:
diff --git a/edb/edgeql/compiler/stmt.py b/edb/edgeql/compiler/stmt.py
--- a/edb/edgeql/compiler/stmt.py
+++ b/edb/edgeql/compiler/stmt.py
@@ -1374,9 +1374,16 @@ def compile_result_clause(
                 span=result_expr.span,
             )
         elif astutils.is_ql_empty_array(result_expr):
+            type_hint: Optional[s_types.Type] = None
+            if (
+                sctx.empty_result_type_hint is not None
+                and sctx.empty_result_type_hint.is_array()
+            ):
+                type_hint = sctx.empty_result_type_hint
+
             expr = setgen.new_array_set(
                 [],
-                stype=sctx.empty_result_type_hint,
+                stype=type_hint,
                 ctx=sctx,
                 span=result_expr.span,
             )
diff --git a/edb/edgeql/compiler/viewgen.py b/edb/edgeql/compiler/viewgen.py
--- a/edb/edgeql/compiler/viewgen.py
+++ b/edb/edgeql/compiler/viewgen.py
@@ -1393,8 +1393,10 @@ def _compile_qlexpr(
         if should_set_partial_prefix:
             shape_expr_ctx.partial_path_prefix = source_set
 
-        if s_ctx.exprtype.is_mutation() and ptrcls is not None:
-            shape_expr_ctx.expr_exposed = context.Exposure.EXPOSED
+        if ptrcls is not None:
+            if s_ctx.exprtype.is_mutation():
+                shape_expr_ctx.expr_exposed = context.Exposure.EXPOSED
+
             shape_expr_ctx.empty_result_type_hint = \
                 ptrcls.get_target(ctx.env.schema)
 


Test patch:
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
@@ -6829,3 +6829,74 @@ async def test_edgeql_insert_coalesce_nulls_08(self):
             'select Note { note }',
             [{'note': "note"}],
         )
+
+    async def test_edgeql_insert_empty_array_01(self):
+        with self.assertRaisesRegex(
+            edgedb.QueryError,
+            "expression returns value of indeterminate type"
+        ):
+            await self.con.execute("""
+                insert InsertTest {
+                    name := [],
+                    l2 := 0,
+                };
+            """)
+
+    async def test_edgeql_insert_empty_array_02(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidPropertyTargetError,
+            r"invalid target for property 'name' "
+            r"of object type 'default::InsertTest': 'array<std::str>' "
+            r"\(expecting 'std::str'\)"
+        ):
+            await self.con.execute("""
+                insert InsertTest {
+                    name := ['a'] ++ [],
+                    l2 := 0,
+                };
+            """)
+
+    async def test_edgeql_insert_empty_array_03(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidPropertyTargetError,
+            r"invalid target for property 'name' "
+            r"of object type 'default::InsertTest': 'std::int64' "
+            r"\(expecting 'std::str'\)"
+        ):
+            await self.con.execute("""
+                insert InsertTest {
+                    name := array_unpack([1] ++ []),
+                    l2 := 0,
+                };
+            """)
+
+    async def test_edgeql_insert_empty_array_04(self):
+        with self.assertRaisesRegex(
+            edgedb.QueryError,
+            "expression returns value of indeterminate type"
+        ):
+            await self.con.execute("""
+                insert InsertTest {
+                    l2 := 0,
+                    subordinates := (
+                        select Subordinate {
+                            @comment := []
+                        }
+                    )
+                };
+            """)
+
+    async def test_edgeql_insert_empty_array_05(self):
+        await self.assert_query_result("""
+            insert Subordinate { name := 'hi' };
+            select ( insert InsertTest {
+                l2 := 0,
+                subordinates := (
+                    select Subordinate {
+                        @comment := array_join(['a'] ++ [], '')
+                    }
+                )
+            }) { l2, subordinates: { name, @comment } };
+            """,
+            [{'l2': 0, 'subordinates': [{'name': 'hi', '@comment': 'a'}]}],
+        )
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
@@ -4588,16 +4588,21 @@ async def test_edgeql_select_empty_04(self):
         )
 
     async def test_edgeql_select_empty_05(self):
-        with self.assertRaisesRegex(
-                edgedb.QueryError,
-                r'expression returns value of indeterminate type'):
-            await self.con.query(r"""
-                SELECT Issue {
-                    number,
-                    # the empty set is of an unspecified type
-                    time_estimate := {}
-                } ORDER BY .number;
-                """)
+        await self.assert_query_result(
+            r"""
+            SELECT Issue {
+                number,
+                # the empty set is of an unspecified type
+                time_estimate := {}
+            } ORDER BY .number;
+            """,
+            [
+                {'number': '1', 'time_estimate': None},
+                {'number': '2', 'time_estimate': None},
+                {'number': '3', 'time_estimate': None},
+                {'number': '4', 'time_estimate': None},
+            ],
+        )
 
     async def test_edgeql_select_empty_object_01(self):
         await self.assert_query_result(
diff --git a/tests/test_edgeql_update.py b/tests/test_edgeql_update.py
--- a/tests/test_edgeql_update.py
+++ b/tests/test_edgeql_update.py
@@ -3908,3 +3908,98 @@ async def test_edgeql_update_dunder_default_02(self):
                 {'a': [4]},
             ]
         )
+
+    async def test_edgeql_update_empty_array_01(self):
+        with self.assertRaisesRegex(
+            edgedb.QueryError,
+            "expression returns value of indeterminate type"
+        ):
+            await self.con.execute("""
+                update UpdateTest
+                set {
+                    name := [],
+                };
+            """)
+
+    async def test_edgeql_update_empty_array_02(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidPropertyTargetError,
+            r"invalid target for property 'name' "
+            r"of object type 'default::UpdateTest': 'array<std::str>' "
+            r"\(expecting 'std::str'\)"
+        ):
+            await self.con.execute("""
+                update UpdateTest
+                set {
+                    name := ['a'] ?? [],
+                };
+            """)
+
+    async def test_edgeql_update_empty_array_03(self):
+        with self.assertRaisesRegex(
+            edgedb.InvalidPropertyTargetError,
+            r"invalid target for property 'name' "
+            r"of object type 'default::UpdateTest': 'std::int64' "
+            r"\(expecting 'std::str'\)"
+        ):
+            await self.con.execute("""
+                insert UpdateTest {
+                    name := array_unpack([1] ?? []),
+                };
+            """)
+
+    async def test_edgeql_update_empty_array_04(self):
+        with self.assertRaisesRegex(
+            edgedb.QueryError,
+            "expression returns value of indeterminate type"
+        ):
+            await self.con.execute("""
+                update UpdateTest
+                set {
+                    annotated_status := (
+                        select Status {
+                            @note := []
+                        }
+                    )
+                };
+            """)
+
+    async def test_edgeql_update_empty_array_05(self):
+        await self.assert_query_result("""
+            select ( update UpdateTest
+                set {
+                    weighted_tags := (
+                        select Tag {
+                            @note := array_join(['a'] ++ [], '')
+                        }
+                    )
+                }
+            ) { name, weighted_tags: { name, @note } } ;
+            """,
+            [
+                {
+                    'name': 'update-test1',
+                    'weighted_tags': [
+                        {'name': 'fun', '@note': 'a'},
+                        {'name': 'boring', '@note': 'a'},
+                        {'name': 'wow', '@note': 'a'}
+                    ],
+                },
+                {
+                    'name': 'update-test2',
+                    'weighted_tags': [
+                        {'name': 'fun', '@note': 'a'},
+                        {'name': 'boring', '@note': 'a'},
+                        {'name': 'wow', '@note': 'a'}
+                    ],
+                },
+                {
+                    'name': 'update-test3',
+                    'weighted_tags': [
+                        {'name': 'fun', '@note': 'a'},
+                        {'name': 'boring', '@note': 'a'},
+                        {'name': 'wow', '@note': 'a'}
+                    ],
+                },
+            ],
+        )