Repository: tobymao/sqlglot

Issue:
duckdb struct to trino row syntax is incorrect
**Fully reproducible code snippet**

```
In [12]: sg.parse_one("select {'a': 1}", read="duckdb").sql(dialect="trino")
Out[12]: "SELECT ROW('a' : 1)"
```

The generated code is a syntax error in trino:

```
❯ trino --catalog hive --schema default --execute "SELECT ROW('a' : 1)"
Query 20230915_181601_00052_vnzqz failed: line 1:16: mismatched input ':'. Expecting: '%', ')', '*', '+', ',', '-', '.', '/', 'AND', 'AT', 'OR', 'ORDER', '[', '||', <predicate>
SELECT ROW('a' : 1)
```

The code generated from `struct_pack` is syntactically valid but incorrect:

```
In [13]: sg.parse_one("select struct_pack(a := 1)", read="duckdb").sql(dialect="trino")
Out[13]: 'SELECT ROW(a = 1)'
```

**Official Documentation**

https://trino.io/docs/current/language/types.html#row



Discussion:
Can take a look at these issues shortly - currently checking out the one with the escape sequences.
Don't mind me 😄  I was just poking around sqlglot to see just exactly how much code we can delete from ibis once we move our backends over to it for sql generation.
I'm not 100% sure we can fully solve this problem, because it seems like Trino requires a `CAST` into a `ROW` that contains the necessary type information to associate the `ROW` values with names. Their example:

```sql
CAST(ROW(1, 2e0) AS ROW(x BIGINT, y DOUBLE))
```

We can do a basic type inference if the values in the DuckDB struct are literals, but if e.g. you have something like

```sql
SELECT {'a': x} FROM tbl
```

Then this will require a schema for the transformation to work properly.
IMO it would be acceptable to call `ROW(x)` on the result, which will preserve the structure sans struct field names:

Trino CLI:

```
trino:default> select row(x, y) from (values (1, 'a'), (2, 'b'), (3, 'c')) _ (x, y);
 _col0
--------
 {1, a}
 {2, b}
 {3, c}
(3 rows)
```
@cpcloud won't accessing the struct fields end up being broken this way though? E.g. `{'a': 1}.a` wouldn't work I think?
That's correct, but that seems better than generating incorrect code.

Personally would also be fine with an error message indicating the transformation isn't possible.
Gotcha, I think I'm going to display an error message for now; happy to accept a PR implementing a viable alternative.

Resolving patch:
diff --git a/sqlglot/dialects/presto.py b/sqlglot/dialects/presto.py
--- a/sqlglot/dialects/presto.py
+++ b/sqlglot/dialects/presto.py
@@ -343,7 +343,6 @@ class Generator(generator.Generator):
             exp.StrToMap: rename_func("SPLIT_TO_MAP"),
             exp.StrToTime: _str_to_time_sql,
             exp.StrToUnix: lambda self, e: f"TO_UNIXTIME(DATE_PARSE({self.sql(e, 'this')}, {self.format_time(e)}))",
-            exp.Struct: rename_func("ROW"),
             exp.StructExtract: struct_extract_sql,
             exp.Table: transforms.preprocess([_unnest_sequence]),
             exp.TimestampTrunc: timestamptrunc_sql,
@@ -369,6 +368,13 @@ class Generator(generator.Generator):
             exp.Xor: bool_xor_sql,
         }
 
+        def struct_sql(self, expression: exp.Struct) -> str:
+            if any(isinstance(arg, (exp.EQ, exp.Slice)) for arg in expression.expressions):
+                self.unsupported("Struct with key-value definitions is unsupported.")
+                return self.function_fallback_sql(expression)
+
+            return rename_func("ROW")(self, expression)
+
         def interval_sql(self, expression: exp.Interval) -> str:
             unit = self.sql(expression, "unit")
             if expression.this and unit.lower().startswith("week"):


Test patch:
diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py
--- a/tests/dialects/test_duckdb.py
+++ b/tests/dialects/test_duckdb.py
@@ -11,6 +11,9 @@ def test_duckdb(self):
             exp.select("*").from_("t").limit(exp.select("5").subquery()).sql(dialect="duckdb"),
         )
 
+        for struct_value in ("{'a': 1}", "struct_pack(a := 1)"):
+            self.validate_all(struct_value, write={"presto": UnsupportedError})
+
         for join_type in ("SEMI", "ANTI"):
             exists = "EXISTS" if join_type == "SEMI" else "NOT EXISTS"