Repository: tobymao/sqlglot

Issue:
Support for DuckDB 1.1 Unpack Feature
yesterday DuckDB 1.1 was released with some nice feature, you can now unpack columns:

```python
 sqlglot.parse_one(
        """SELECT id, struct_pack(*COLUMNS('m\d')) AS measurements
FROM many_measurements""",
        dialect="duckdb",
    )
```

However, this does not parse in SQLGlot yet.

Here's the docs: https://duckdb.org/2024/09/09/announcing-duckdb-110.html#unpacked-columns

Hints from issue comments:

Solution patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index a14c32d376..a7bc2ca4e8 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -6203,6 +6203,10 @@ class UnixToTimeStr(Func):
     pass
 
 
+class UnpackColumns(Func):
+    pass
+
+
 class Uuid(Func):
     _sql_names = ["UUID", "GEN_RANDOM_UUID", "GENERATE_UUID", "UUID_STRING"]
 
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 0e8888a6f0..746712b6fe 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -154,6 +154,7 @@ class Generator(metaclass=_Generator):
         exp.TransientProperty: lambda *_: "TRANSIENT",
         exp.Union: lambda self, e: self.set_operations(e),
         exp.UnloggedProperty: lambda *_: "UNLOGGED",
+        exp.UnpackColumns: lambda self, e: f"*{self.sql(e.this)}",
         exp.Uuid: lambda *_: "UUID()",
         exp.UppercaseColumnConstraint: lambda *_: "UPPERCASE",
         exp.VarMap: lambda self, e: self.func("MAP", e.args["keys"], e.args["values"]),
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index 31f2ff6e82..a7f8918113 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -841,14 +841,7 @@ class Parser(metaclass=_Parser):
         TokenType.TRUE: lambda self, _: self.expression(exp.Boolean, this=True),
         TokenType.FALSE: lambda self, _: self.expression(exp.Boolean, this=False),
         TokenType.SESSION_PARAMETER: lambda self, _: self._parse_session_parameter(),
-        TokenType.STAR: lambda self, _: self.expression(
-            exp.Star,
-            **{
-                "except": self._parse_star_op("EXCEPT", "EXCLUDE"),
-                "replace": self._parse_star_op("REPLACE"),
-                "rename": self._parse_star_op("RENAME"),
-            },
-        ),
+        TokenType.STAR: lambda self, _: self._parse_star_ops(),
     }
 
     PLACEHOLDER_PARSERS = {
@@ -7375,3 +7368,16 @@ def _parse_normalize(self) -> exp.Normalize:
             this=self._parse_bitwise(),
             form=self._match(TokenType.COMMA) and self._parse_var(),
         )
+
+    def _parse_star_ops(self) -> exp.Star | exp.UnpackColumns:
+        if self._match_text_seq("COLUMNS", "(", advance=False):
+            return exp.UnpackColumns(this=self._parse_function())
+
+        return self.expression(
+            exp.Star,
+            **{  # type: ignore
+                "except": self._parse_star_op("EXCEPT", "EXCLUDE"),
+                "replace": self._parse_star_op("REPLACE"),
+                "rename": self._parse_star_op("RENAME"),
+            },
+        )


Test patch:
diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py
index 98d1604bde..fbf81dd8fb 100644
--- a/tests/dialects/test_duckdb.py
+++ b/tests/dialects/test_duckdb.py
@@ -817,6 +817,15 @@ def test_duckdb(self):
 
         self.validate_identity("SELECT * FROM (DESCRIBE t)")
 
+        self.validate_identity("SELECT UNNEST([*COLUMNS('alias_.*')]) AS column_name")
+        self.validate_identity(
+            "SELECT COALESCE(*COLUMNS(*)) FROM (SELECT NULL, 2, 3) AS t(a, b, c)"
+        )
+        self.validate_identity(
+            "SELECT id, STRUCT_PACK(*COLUMNS('m\d')) AS measurements FROM many_measurements",
+            """SELECT id, {'_0': *COLUMNS('m\d')} AS measurements FROM many_measurements""",
+        )
+
     def test_array_index(self):
         with self.assertLogs(helper_logger) as cm:
             self.validate_all(