Repository: ibis-project/ibis

Issue:
bug: ibis.struct doesn't work with `sqlglot<=20.1.0`
### What happened?

```python
import ibis

t = ibis.examples.penguins.fetch()
t2 = t.select(s=ibis.struct({"a": t.island}))
ibis.to_sql(t2)

# python -m pip install "sqlglot==20.1.0"
# SELECT
#   {'island': } AS "s"
# FROM "penguins" AS "t0"

# python -m pip install "sqlglot==20.2.0"
# SELECT
#   {'a': "t0"."island"} AS "s"
# FROM "penguins" AS "t0"
```

I think this means we need to make the dependency restriction on sqlglot more strict?

### What version of ibis are you using?

main

### What backend(s) are you using, if any?

_No response_

### Relevant log output

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
Thanks for reporting! Yeah, we will bump the minimum required version before we release 9.0.
We can leave this open as a tracking issue for that.
In the meantime I'll look into whether there's a way we can address this that is backwards compatible.

Resolving patch:
diff --git a/ibis/backends/duckdb/compiler.py b/ibis/backends/duckdb/compiler.py
--- a/ibis/backends/duckdb/compiler.py
+++ b/ibis/backends/duckdb/compiler.py
@@ -92,6 +92,16 @@ def _aggregate(self, funcname: str, *args, where):
             return sge.Filter(this=expr, expression=sge.Where(this=where))
         return expr
 
+    def visit_StructColumn(self, op, *, names, values):
+        return sge.Struct.from_arg_list(
+            [
+                sge.PropertyEQ(
+                    this=sg.to_identifier(name, quoted=self.quoted), expression=value
+                )
+                for name, value in zip(names, values)
+            ]
+        )
+
     def visit_ArrayDistinct(self, op, *, arg):
         return self.if_(
             arg.is_(NULL),
@@ -314,6 +324,18 @@ def visit_NonNullLiteral(self, op, *, value, dtype):
                 args.append(tz)
 
             return self.f[funcname](*args)
+        elif dtype.is_struct():
+            return sge.Struct.from_arg_list(
+                [
+                    sge.PropertyEQ(
+                        this=sg.to_identifier(k, quoted=self.quoted),
+                        expression=self.visit_Literal(
+                            ops.Literal(v, field_dtype), value=v, dtype=field_dtype
+                        ),
+                    )
+                    for field_dtype, (k, v) in zip(dtype.types, value.items())
+                ]
+            )
         else:
             return None
 


Test patch:
diff --git a/ibis/backends/tests/test_struct.py b/ibis/backends/tests/test_struct.py
--- a/ibis/backends/tests/test_struct.py
+++ b/ibis/backends/tests/test_struct.py
@@ -99,16 +99,15 @@ def test_null_literal(backend, con, field):
 @pytest.mark.notyet(
     ["flink"], reason="flink doesn't support creating struct columns from literals"
 )
-def test_struct_column(backend, alltypes, df):
+def test_struct_column(alltypes, df):
     t = alltypes
-    expr = ibis.struct(dict(a=t.string_col, b=1, c=t.bigint_col)).name("s")
-    assert expr.type() == dt.Struct(dict(a=dt.string, b=dt.int8, c=dt.int64))
+    expr = t.select(s=ibis.struct(dict(a=t.string_col, b=1, c=t.bigint_col)))
+    assert expr.s.type() == dt.Struct(dict(a=dt.string, b=dt.int8, c=dt.int64))
     result = expr.execute()
-    expected = pd.Series(
-        (dict(a=a, b=1, c=c) for a, c in zip(df.string_col, df.bigint_col)),
-        name="s",
+    expected = pd.DataFrame(
+        {"s": [dict(a=a, b=1, c=c) for a, c in zip(df.string_col, df.bigint_col)]}
     )
-    tm.assert_series_equal(result, expected)
+    tm.assert_frame_equal(result, expected)
 
 
 @pytest.mark.notimpl(["dask", "pandas", "postgres", "risingwave", "polars"])