Repository: tobymao/sqlglot

Issue:
Clickhouse COLUMNS APPLY parsing error
I was trying to use dynamic columns selection with APPLY, but sqlglot fails to parse it

**Fully reproducible code snippet**

`select COLUMNS('.*field') APPLY(toUInt64) FROM ABC;`

Leads to sqlglot.errors.ParseError: Invalid expression / Unexpected token.

**Official Documentation**
https://clickhouse.com/docs/en/sql-reference/statements/select#apply



Discussion:
tobymao: is this the right approach? should we treat this similarly to window functions?
VaggelisD: @tobymao  Clickhouse is a bit lax with the rules, the grammar etc so I struggled a bit to not break any functionality as with the alias. Regarding window functions, which part would that be, reusing `_parse_query_modifiers()`?

Note that `APPLY` comes before the query's `FROM` so we must either parse it early on it's own (the approach here) or add it in `QUERY_MODIFIER_PARSERS` and call `_parse_query_modifiers()` in the place of my current while loop. 

Patch:
diff --git a/sqlglot/dialects/clickhouse.py b/sqlglot/dialects/clickhouse.py
index ec1984e9..c8671f69 100644
--- a/sqlglot/dialects/clickhouse.py
+++ b/sqlglot/dialects/clickhouse.py
@@ -756,6 +756,26 @@ class ClickHouse(Dialect):
         def _parse_constraint(self) -> t.Optional[exp.Expression]:
             return super()._parse_constraint() or self._parse_projection_def()
 
+        def _parse_alias(
+            self, this: t.Optional[exp.Expression], explicit: bool = False
+        ) -> t.Optional[exp.Expression]:
+            # In clickhouse "SELECT <expr> APPLY(...)" is a query modifier,
+            # so "APPLY" shouldn't be parsed as <expr>'s alias. However, "SELECT <expr> apply" is a valid alias
+            if self._match_pair(TokenType.APPLY, TokenType.L_PAREN, advance=False):
+                return this
+
+            return super()._parse_alias(this=this, explicit=explicit)
+
+        def _parse_expression(self) -> t.Optional[exp.Expression]:
+            this = super()._parse_expression()
+
+            # Clickhouse allows "SELECT <expr> [APPLY(func)] [...]]" modifier
+            while self._match_pair(TokenType.APPLY, TokenType.L_PAREN):
+                this = exp.Apply(this=this, expression=self._parse_var(any_token=True))
+                self._match(TokenType.R_PAREN)
+
+            return this
+
     class Generator(generator.Generator):
         QUERY_HINTS = False
         STRUCT_DELIMITER = ("(", ")")
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index d44b2c3d..95f3c01c 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -4922,6 +4922,10 @@ class ApproxDistinct(AggFunc):
     _sql_names = ["APPROX_DISTINCT", "APPROX_COUNT_DISTINCT"]
 
 
+class Apply(Func):
+    arg_types = {"this": True, "expression": True}
+
+
 class Array(Func):
     arg_types = {"expressions": False, "bracket_notation": False}
     is_var_len_args = True
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index 80c86288..e2029313 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -4336,3 +4336,9 @@ class Generator(metaclass=_Generator):
                 array_agg = f"{array_agg} FILTER(WHERE {this_sql} IS NOT NULL)"
 
         return array_agg
+
+    def apply_sql(self, expression: exp.Apply) -> str:
+        this = self.sql(expression, "this")
+        expr = self.sql(expression, "expression")
+
+        return f"{this} APPLY({expr})"


Test patch:
diff --git a/tests/dialects/test_clickhouse.py b/tests/dialects/test_clickhouse.py
index d203141d..aa6c4f1b 100644
--- a/tests/dialects/test_clickhouse.py
+++ b/tests/dialects/test_clickhouse.py
@@ -519,6 +519,14 @@ class TestClickhouse(Validator):
         self.validate_identity("SELECT TRIM(LEADING '(' FROM '(   Hello, world!   )')")
         self.validate_identity("current_timestamp").assert_is(exp.Column)
 
+        self.validate_identity("SELECT * APPLY(sum) FROM columns_transformers")
+        self.validate_identity("SELECT COLUMNS('[jk]') APPLY(toString) FROM columns_transformers")
+        self.validate_identity(
+            "SELECT COLUMNS('[jk]') APPLY(toString) APPLY(length) APPLY(max) FROM columns_transformers"
+        )
+        self.validate_identity("SELECT * APPLY(sum), COLUMNS('col') APPLY(sum) APPLY(avg) FROM t")
+        self.validate_identity("SELECT col apply", "SELECT col AS apply")
+
     def test_clickhouse_values(self):
         values = exp.select("*").from_(
             exp.values([exp.tuple_(1, 2, 3)], alias="subq", columns=["a", "b", "c"])