Repository: edgedb/edgedb

Issue:
Named tuple params whose types also appear in the schema don't work

<!--
For the EdgeDB Version: run `edgedb query 'select sys::get_version_as_str()'` from your project directory (or run `select sys::get_version_as_str();` in the EdgeDB interactive shell).
For the EdgeDB CLI Version: Run `edgedb --version` from anywhere
-->

- EdgeDB Version: 3.0+7d903be
- EdgeDB CLI Version: 3.4.0+4d95a2c
- OS Version: macOs 13

Steps to Reproduce:

Schema:
```
 type Meeting {
    required title -> str {
      constraint exclusive;
    };

    multi recurrence: str;

    creator_meta: tuple<display_name: str, email: str, id: str>;
    multi attendees_meta: tuple<
        display_name: str,
        email: str,
        rsvp_status: str,
        rsvp_comment: str,
        is_optional: bool,
        is_event_organizer: bool,
        additional_guests: int64,
        provider_meta: json,
    >;
  }
};
```

Query with Typescript Query Builder:
```
  const insertQ = e.params({
    title: e.str,
    recurrence: e.array(e.str),
    creator_meta: e.tuple({
      display_name: e.str,
      email: e.str,
      id: e.str,
    }),
    attendees_meta: e.array(
      e.tuple({
        display_name: e.str,
        email: e.str,
        rsvp_status: e.str,
        rsvp_comment: e.str,
        is_optional: e.bool,
        is_event_organizer: e.bool,
        additional_guests: e.int64,
        provider_meta: e.json,
      }),
    ),
  }, (params) => {
    return e.insert(e.Meeting, {
      title: params.title,
      recurrence: e.array_unpack(params.recurrence),
      creator_meta: params.creator_meta,
      attendees_meta: e.array_unpack(params.attendees_meta),
    });
  });

  const insertResult = await insertQ.run(client, {
    title: "Test",
    recurrence: ["RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR", "EXDATE:20210101T000000Z"],
    creator_meta: {
      display_name: "John Doe",
      email: "example@gmail.com",
      id: "123",
    },
    attendees_meta: [
      {
        display_name: "John Doe",
        email: "example@hotmail.com",
        rsvp_status: "accepted",
        rsvp_comment: "I'll be there",
        is_optional: false,
        is_event_organizer: true,
        additional_guests: 0,
        provider_meta: {},
      },
    ],
  });
```

EdgeQL query:
```
WITH
  __param__title := <std::str>$title,
  __param__recurrence := <array<std::str>>$recurrence,
  __param__creator_meta := <tuple<display_name: std::str, email: std::str, id: std::str>>$creator_meta,
  __param__attendees_meta := <array<tuple<display_name: std::str, email: std::str, rsvp_status: std::str, rsvp_comment: std::str, is_optional: std::bool, is_event_organizer: std::bool, additional_guests: std::int64, provider_meta: std::json>>>$attendees_meta
SELECT (INSERT default::Meeting {
  title := __param__title,
  recurrence := std::array_unpack(__param__recurrence),
  creator_meta := __param__creator_meta,
  attendees_meta := std::array_unpack(__param__attendees_meta)
})
```

Error:
```
error: Uncaught InternalServerError: cannot cast type record to edgedbpub."ead3bd00-2258-11ee-9477-9570e39cc69a_t"
    const err = new errorType(message);
```




Discussion:
With the schema
```
 type Test {                                                              
    tup -> tuple<test: str>                                               
 }  
```
we get basically that error with
```
WITH
  p := <tuple<test: str>>$0
insert Test { tup := p };
```

With
```
WITH
  p := <tuple<test: str>>$0
select p
```
we get `record type has not been registered`

and with
```
select <tuple<test: str>>$0
```
we get `could not identify column "test" in record data type`

Resolving patch:
diff --git a/edb/edgeql/compiler/tuple_args.py b/edb/edgeql/compiler/tuple_args.py
--- a/edb/edgeql/compiler/tuple_args.py
+++ b/edb/edgeql/compiler/tuple_args.py
@@ -95,6 +95,7 @@
 from typing import *
 
 from edb import errors
+from edb.common.typeutils import not_none
 
 from edb.ir import ast as irast
 from edb.ir import typeutils as irtypeutils
@@ -121,7 +122,7 @@
 
 def _lmost_is_array(typ: irast.ParamTransType) -> bool:
     while isinstance(typ, irast.ParamTuple):
-        typ = typ.typs[0]
+        _, typ = typ.typs[0]
     return isinstance(typ, irast.ParamArray)
 
 
@@ -166,13 +167,14 @@ def trans(
             )
 
         elif irtypeutils.is_tuple(typ):
-            # HACK: We don't bother dealing with named tuples. We just
-            # generate anonymous tuples and it all still works out.
             return irast.ParamTuple(
                 typeref=typ,
                 idx=start,
                 typs=tuple(
-                    trans(t, in_array=in_array, depth=depth + 1)
+                    (
+                        t.element_name,
+                        trans(t, in_array=in_array, depth=depth + 1),
+                    )
                     for t in typ.subtypes
                 ),
             )
@@ -222,6 +224,21 @@ def _index(expr: qlast.Expr, idx: qlast.Expr) -> qlast.Indirection:
     return qlast.Indirection(arg=expr, indirection=[qlast.Index(index=idx)])
 
 
+def _make_tuple(
+    fields: Sequence[tuple[Optional[str], qlast.Expr]]
+) -> qlast.NamedTuple | qlast.Tuple:
+    is_named = fields and fields[0][0]
+    if is_named:
+        return qlast.NamedTuple(elements=[
+            qlast.TupleElement(name=qlast.ObjectRef(name=not_none(f)), val=e)
+            for f, e in fields
+        ])
+    else:
+        return qlast.Tuple(
+            elements=[e for _, e in fields]
+        )
+
+
 def make_decoder(
     ptyp: irast.ParamTransType,
     qparams: tuple[irast.Param, ...],
@@ -252,7 +269,7 @@ def mk(typ: irast.ParamTransType, idx: Optional[qlast.Expr]) -> qlast.Expr:
             return expr
 
         elif isinstance(typ, irast.ParamTuple):
-            return qlast.Tuple(elements=[mk(t, idx=idx) for t in typ.typs])
+            return _make_tuple([(f, mk(t, idx=idx)) for f, t in typ.typs])
 
         elif isinstance(typ, irast.ParamArray):
             inner_idx_alias, inner_idx = _get_alias('idx', ctx=ctx)
diff --git a/edb/ir/ast.py b/edb/ir/ast.py
--- a/edb/ir/ast.py
+++ b/edb/ir/ast.py
@@ -612,12 +612,12 @@ def flatten(self) -> tuple[typing.Any, ...]:
 
 @dataclasses.dataclass(eq=False)
 class ParamTuple(ParamTransType):
-    typs: tuple[ParamTransType, ...]
+    typs: tuple[tuple[typing.Optional[str], ParamTransType], ...]
 
     def flatten(self) -> tuple[typing.Any, ...]:
         return (
             (int(qltypes.TypeTag.TUPLE), self.idx)
-            + tuple(x.flatten() for x in self.typs)
+            + tuple(x.flatten() for _, x in self.typs)
         )
 
 


Test patch:
diff --git a/tests/schemas/casts.esdl b/tests/schemas/casts.esdl
--- a/tests/schemas/casts.esdl
+++ b/tests/schemas/casts.esdl
@@ -39,6 +39,7 @@ type Test {
     property p_float64 -> float64;
     property p_bigint -> bigint;
     property p_decimal -> decimal;
+    property p_tup -> tuple<test: str>;
 }
 
 type JSONTest {
@@ -79,4 +80,4 @@ type ScalarTest {
 
 type Person {
     property name -> str;
-}
\ No newline at end of file
+}
diff --git a/tests/test_edgeql_casts.py b/tests/test_edgeql_casts.py
--- a/tests/test_edgeql_casts.py
+++ b/tests/test_edgeql_casts.py
@@ -2883,6 +2883,43 @@ async def test_edgeql_casts_tuple_params_08(self):
             variables=(None, 11111),
         )
 
+    async def test_edgeql_casts_tuple_params_09(self):
+        await self.con.query('''
+            WITH
+              p := <tuple<test: str>>$0
+            insert Test { p_tup := p };
+        ''', ('foo',))
+
+        await self.assert_query_result(
+            '''
+            select Test { p_tup } filter exists .p_tup
+            ''',
+            [{'p_tup': {'test': 'foo'}}],
+        )
+        await self.assert_query_result(
+            '''
+            WITH
+              p := <tuple<test: str>>$0
+            select p
+            ''',
+            [{'test': 'foo'}],
+            variables=(('foo',),),
+        )
+        await self.assert_query_result(
+            '''
+            select <tuple<test: str>>$0
+            ''',
+            [{'test': 'foo'}],
+            variables=(('foo',),),
+        )
+        await self.assert_query_result(
+            '''
+            select <array<tuple<test: str>>>$0
+            ''',
+            [[{'test': 'foo'}, {'test': 'bar'}]],
+            variables=([('foo',), ('bar',)],),
+        )
+
     async def test_edgeql_cast_empty_set_to_array_01(self):
         await self.assert_query_result(
             r'''