Repository: edgedb/edgedb

Issue:
edb.errors.InternalServerError: column "target" is of type edgedbpub."71637d60-9aed-6158-3c48-7316b93c0197_t" but expression is of type bigint
<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: 1.0a8.dev5340+g76e0688b-2021012101~focal.nightly
- OS Version: Ubuntu

```
START MIGRATION TO {
    module default {
        type Movie {
            required property title -> str;
            multi property tags -> tuple<int64, str>;
        }
    }
}
```
```
INSERT Movie {title := 'hey', tags := {(10, 'foo'), (20, 'bar')}};
```
```
ERROR: InternalServerError: column "target" is of type edgedbpub."71637d60-9aed-6158-3c48-7316b93c0197_t" but expression is of type bigint
  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:
      edb.errors.InternalServerError: column "target" is of type edgedbpub."71637d60-9aed-6158-3c48-7316b93c0197_t" but expression is of type bigint
```


Discussion:
And something from the same schema (which is my real problem, I found the error above while trying to figure out a short reproducer for this);
```
test> SELECT Movie FILTER (10, 'foo') in .tags;
ERROR: InternalServerError: subquery has too few columns
  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:
      edb.errors.InternalServerError: subquery has too few columns

```

Resolving patch:
diff --git a/edb/pgsql/compiler/dml.py b/edb/pgsql/compiler/dml.py
--- a/edb/pgsql/compiler/dml.py
+++ b/edb/pgsql/compiler/dml.py
@@ -1094,7 +1094,7 @@ def process_link_update(
     assert rptr is not None
     ptrref = rptr.ptrref
     assert isinstance(ptrref, irast.PointerRef)
-    target_is_scalar = irtyputils.is_scalar(ir_set.typeref)
+    target_is_scalar = not irtyputils.is_object(ir_set.typeref)
     path_id = ir_set.path_id
 
     # The links in the dml class shape have been derived,
@@ -1545,6 +1545,7 @@ def process_link_values(
 
     path_id = ir_expr.path_id
 
+    target_ref: pgast.BaseExpr
     if shape_tuple is not None:
         for element in shape_tuple.elements:
             if not element.path_id.is_linkprop_path():
@@ -1566,6 +1567,7 @@ def process_link_values(
         if target_is_scalar:
             target_ref = pathctx.get_rvar_path_value_var(
                 input_rvar, path_id, env=ctx.env)
+            target_ref = output.output_as_value(target_ref, env=ctx.env)
         else:
             target_ref = pathctx.get_rvar_path_identity_var(
                 input_rvar, path_id, env=ctx.env)
diff --git a/edb/pgsql/compiler/relgen.py b/edb/pgsql/compiler/relgen.py
--- a/edb/pgsql/compiler/relgen.py
+++ b/edb/pgsql/compiler/relgen.py
@@ -1239,7 +1239,7 @@ def process_set_as_membership_expr(
         left_arg, right_arg = (a.expr for a in expr.args)
 
         newctx.expr_exposed = False
-        left_expr = dispatch.compile(left_arg, ctx=newctx)
+        left_out = dispatch.compile(left_arg, ctx=newctx)
 
         right_arg = irutils.unwrap_set(right_arg)
         # If the right operand of [NOT] IN is an array_unpack call,
@@ -1253,37 +1253,39 @@ def process_set_as_membership_expr(
         else:
             is_array_unpack = False
 
-        with newctx.subrel() as _, _.newscope() as subctx:
-            dispatch.compile(right_arg, ctx=subctx)
-            pathctx.get_path_value_output(
-                subctx.rel, right_arg.path_id,
-                env=subctx.env)
+        # We compile `x [NOT] IN y` into essentially
+        # `[NOT] EXISTS (SELECT ... WHERE x = y)`.
+        # We use this approach instead of using ANY/ALL or IN directly
+        # because those subquery-based approaches are finnicky in
+        # their treatment of tuples, which this approach sidesteps.
 
-            right_rel = subctx.rel
+        with newctx.subrel() as _, _.newscope() as subctx:
+            right_out = dispatch.compile(right_arg, ctx=subctx)
 
             if is_array_unpack:
-                right_rel = pgast.TypeCast(
-                    arg=right_rel,
-                    type_name=pgast.TypeName(
-                        name=pg_types.pg_type_from_ir_typeref(
-                            right_arg.typeref)
+                right_out = pgast.SubLink(
+                    type=pgast.SubLinkType.ANY,
+                    expr=pgast.TypeCast(
+                        arg=right_out,
+                        type_name=pgast.TypeName(
+                            name=pg_types.pg_type_from_ir_typeref(
+                                right_arg.typeref)
+                        )
                     )
                 )
 
+            cond = exprcomp.compile_operator(
+                expr,
+                [left_out, right_out],
+                ctx=subctx,
+            )
+
+            subctx.rel.where_clause = astutils.extend_binop(
+                subctx.rel.where_clause, cond)
+
     negated = str(expr.func_shortname) == 'std::NOT IN'
-    sublink_type = pgast.SubLinkType.ALL if negated else pgast.SubLinkType.ANY
-
-    set_expr = exprcomp.compile_operator(
-        expr,
-        [
-            left_expr,
-            pgast.SubLink(
-                type=sublink_type,
-                expr=right_rel,
-            ),
-        ],
-        ctx=ctx,
-    )
+    cmd = 'NOT EXISTS' if negated else 'EXISTS'
+    set_expr = astutils.new_unop(cmd, subctx.rel)
 
     pathctx.put_path_value_var_if_not_exists(
         stmt, ir_set.path_id, set_expr, env=ctx.env)
diff --git a/edb/server/defines.py b/edb/server/defines.py
--- a/edb/server/defines.py
+++ b/edb/server/defines.py
@@ -34,7 +34,7 @@
 EDGEDB_SPECIAL_DBS = {EDGEDB_TEMPLATE_DB, EDGEDB_SYSTEM_DB}
 
 # Increment this whenever the database layout or stdlib changes.
-EDGEDB_CATALOG_VERSION = 2021_02_19_00_00
+EDGEDB_CATALOG_VERSION = 2021_03_05_00_00
 
 # Resource limit on open FDs for the server process.
 # By default, at least on macOS, the max number of open FDs


Test patch:
diff --git a/tests/schemas/insert.esdl b/tests/schemas/insert.esdl
--- a/tests/schemas/insert.esdl
+++ b/tests/schemas/insert.esdl
@@ -194,6 +194,7 @@ type SelfRef {
 
 type CollectionTest {
     property some_tuple -> tuple<str, int64>;
+    multi property some_multi_tuple -> tuple<str, int64>;
     property str_array -> array<str>;
     property float_array -> array<float32>;
 }
diff --git a/tests/test_edgeql_functions.py b/tests/test_edgeql_functions.py
--- a/tests/test_edgeql_functions.py
+++ b/tests/test_edgeql_functions.py
@@ -452,6 +452,45 @@ async def test_edgeql_functions_array_unpack_05(self):
             [1],
         )
 
+    async def test_edgeql_functions_array_unpack_06(self):
+        # We have a special case optimization for "IN array_unpack" so
+        # it's worth testing it.
+
+        await self.assert_query_result(
+            r'''SELECT 1 IN array_unpack([1]);''',
+            [True],
+        )
+
+        await self.assert_query_result(
+            r'''SELECT 2 IN array_unpack([1]);''',
+            [False],
+        )
+
+        await self.assert_query_result(
+            r'''SELECT 2 NOT IN array_unpack([1]);''',
+            [True],
+        )
+
+        await self.assert_query_result(
+            r'''SELECT 1 IN array_unpack({[1,2,3], [4,5,6]});''',
+            [True],
+        )
+
+        await self.assert_query_result(
+            r'''SELECT 0 IN array_unpack({[1,2,3], [4,5,6]});''',
+            [False],
+        )
+
+        await self.assert_query_result(
+            r'''SELECT 1 NOT IN array_unpack({[1,2,3], [4,5,6]});''',
+            [False],
+        )
+
+        await self.assert_query_result(
+            r'''SELECT 0 NOT IN array_unpack({[1,2,3], [4,5,6]});''',
+            [True],
+        )
+
     async def test_edgeql_functions_enumerate_01(self):
         await self.assert_query_result(
             r'''SELECT [10, 20];''',
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
@@ -2159,6 +2159,50 @@ async def test_edgeql_insert_collection_03(self):
             ]
         )
 
+    async def test_edgeql_insert_collection_04(self):
+        await self.con.execute(r"""
+            INSERT test::CollectionTest {
+                some_tuple := ('huh', -1),
+                some_multi_tuple := ('foo', 0),
+            };
+
+            INSERT test::CollectionTest {
+                some_tuple := ('foo', 0),
+                some_multi_tuple := {('foo', 0), ('bar', 1)},
+            };
+        """)
+
+        await self.assert_query_result(
+            r"""
+                WITH MODULE test
+                SELECT count(
+                    CollectionTest FILTER ('bar', 1) IN .some_multi_tuple
+                );
+            """,
+            [1],
+        )
+
+        await self.assert_query_result(
+            r"""
+                WITH MODULE test
+                SELECT count(
+                    CollectionTest FILTER .some_tuple IN .some_multi_tuple
+                );
+            """,
+            [1],
+        )
+
+        await self.assert_query_result(
+            r"""
+                WITH MODULE test
+                SELECT count(
+                    CollectionTest FILTER ('foo', '0') IN
+                    <tuple<str, str>>.some_multi_tuple
+                );
+            """,
+            [2],
+        )
+
     async def test_edgeql_insert_in_conditional_bad_01(self):
         with self.assertRaisesRegex(
                 edgedb.QueryError,