Skip to content

Commit 7a0a9ab

Browse files
authored
feat(snowflake): support invoking dynamically-resolved functions via IDENTIFIER() (#7796)
* feat(snowflake): support invoking dynamically-resolved functions via IDENTIFIER() * call unsupported to not naively emit function calls
1 parent 075eaf5 commit 7a0a9ab

5 files changed

Lines changed: 62 additions & 12 deletions

File tree

sqlglot/expressions/core.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
import sys
1111
import textwrap
1212
import typing as t
13+
from builtins import type as Type
1314
from collections import deque
15+
from collections.abc import Collection, Iterator, Mapping, MutableMapping, Sequence
1416
from copy import deepcopy
1517
from decimal import Decimal
1618
from functools import reduce
17-
from collections.abc import Iterator, Sequence, Collection, Mapping, MutableMapping
18-
from sqlglot._typing import E, T
19+
20+
from sqlglot._typing import E, GeneratorNoDialectArgs, ParserNoDialectArgs, T
1921
from sqlglot.errors import ParseError
2022
from sqlglot.helper import (
2123
camel_to_snake_case,
@@ -24,17 +26,15 @@
2426
to_bool,
2527
trait,
2628
)
27-
2829
from sqlglot.tokenizer_core import Token
29-
from builtins import type as Type
30-
from sqlglot._typing import GeneratorNoDialectArgs, ParserNoDialectArgs
3130

3231
if t.TYPE_CHECKING:
33-
from typing_extensions import Self, Unpack, Concatenate
32+
from typing_extensions import Concatenate, Self, Unpack
33+
34+
from sqlglot._typing import P
3435
from sqlglot.dialects.dialect import DialectType
3536
from sqlglot.expressions.datatypes import DATA_TYPE, DataType, DType, Interval
3637
from sqlglot.expressions.query import Select
37-
from sqlglot._typing import P
3838

3939
R = t.TypeVar("R")
4040

@@ -1808,8 +1808,10 @@ def output_name(self) -> str:
18081808

18091809

18101810
# https://docs.snowflake.com/en/sql-reference/identifier-literal
1811+
# "expressions" holds the arguments when the resolved identifier is invoked as a
1812+
# function, e.g. `IDENTIFIER('my_func')(1, 2)`
18111813
class DynamicIdentifier(Expression, Func):
1812-
pass
1814+
arg_types = {"this": True, "expressions": False}
18131815

18141816

18151817
class Opclass(Expression):
@@ -2425,7 +2427,8 @@ def convert(value: t.Any, copy: bool = False) -> Expr:
24252427

24262428
return _Array(expressions=[convert(v, copy=copy) for v in value])
24272429
if isinstance(value, dict):
2428-
from sqlglot.expressions.array import Array as _Array, Map as _Map
2430+
from sqlglot.expressions.array import Array as _Array
2431+
from sqlglot.expressions.array import Map as _Map
24292432

24302433
return _Map(
24312434
keys=_Array(expressions=[convert(k, copy=copy) for k in value]),

sqlglot/generator.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1960,7 +1960,14 @@ def index_sql(self, expression: exp.Index) -> str:
19601960
def dynamicidentifier_sql(self, expression: exp.DynamicIdentifier) -> str:
19611961
this = expression.this
19621962
if this and this.is_string:
1963-
return maybe_parse(this.name).sql(self.dialect)
1963+
resolved = maybe_parse(this.name).sql(self.dialect)
1964+
if "expressions" in expression.args:
1965+
# `IDENTIFIER(...)` invoked as a function, e.g. `IDENTIFIER('my_func')(1, 2)`
1966+
# We can't safely emit the call to other dialects since name/arg semantics may differ
1967+
self.unsupported(
1968+
"Transpiling dynamically-invoked IDENTIFIER() functions is unsupported"
1969+
)
1970+
return resolved
19641971
self.unsupported("IDENTIFIER() with non-literal arguments is not supported")
19651972
return self.func("IDENTIFIER", this)
19661973

sqlglot/generators/snowflake.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import typing as t
4+
from collections import defaultdict
45

56
from sqlglot import exp, generator, transforms
67
from sqlglot.dialects.dialect import (
@@ -34,7 +35,6 @@
3435
build_object_construct,
3536
)
3637
from sqlglot.tokens import TokenType
37-
from collections import defaultdict
3838

3939
if t.TYPE_CHECKING:
4040
from sqlglot._typing import E
@@ -475,7 +475,6 @@ class SnowflakeGenerator(generator.Generator):
475475
exp.DayOfWeekIso: rename_func("DAYOFWEEKISO"),
476476
exp.DayOfYear: rename_func("DAYOFYEAR"),
477477
exp.DotProduct: rename_func("VECTOR_INNER_PRODUCT"),
478-
exp.DynamicIdentifier: rename_func("IDENTIFIER"),
479478
exp.Explode: rename_func("FLATTEN"),
480479
exp.Extract: lambda self, e: self.func(
481480
"DATE_PART", map_date_part(e.this, self.dialect), e.expression
@@ -617,6 +616,13 @@ class SnowflakeGenerator(generator.Generator):
617616
),
618617
}
619618

619+
def dynamicidentifier_sql(self, expression: exp.DynamicIdentifier) -> str:
620+
this = self.func("IDENTIFIER", expression.this)
621+
if "expressions" in expression.args:
622+
# `IDENTIFIER(...)` invoked as a function, e.g. `IDENTIFIER('my_func')(1, 2)`
623+
return self.func(this, *expression.expressions, normalize=False)
624+
return this
625+
620626
def sortarray_sql(self, expression: exp.SortArray) -> str:
621627
asc = expression.args.get("asc")
622628
nulls_first = expression.args.get("nulls_first")

sqlglot/parsers/snowflake.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,31 @@ def _parse_table(
11561156

11571157
return table
11581158

1159+
def _parse_function_call(
1160+
self,
1161+
functions: dict[str, t.Callable] | None = None,
1162+
anonymous: bool = False,
1163+
optional_parens: bool = True,
1164+
any_token: bool = False,
1165+
) -> exp.Expr | None:
1166+
this = super()._parse_function_call(
1167+
functions=functions,
1168+
anonymous=anonymous,
1169+
optional_parens=optional_parens,
1170+
any_token=any_token,
1171+
)
1172+
1173+
# Snowflake can invoke a function whose name is dynamically resolved, e.g.
1174+
# `IDENTIFIER('my_func')(1, 2)`. The trailing argument list is the call's arguments.
1175+
#
1176+
# https://docs.snowflake.com/en/sql-reference/identifier-literal
1177+
if isinstance(this, exp.DynamicIdentifier) and self._match(
1178+
TokenType.L_PAREN, advance=False
1179+
):
1180+
this.set("expressions", self._parse_wrapped_csv(self._parse_lambda))
1181+
1182+
return this
1183+
11591184
def _parse_id_var(
11601185
self,
11611186
any_token: bool = True,

tests/dialects/test_snowflake.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,15 @@ def test_snowflake(self):
769769
self.validate_identity("SELECT KURTOSIS(x) OVER (PARTITION BY 1)")
770770
self.validate_identity("WITH x AS (SELECT 1 AS foo) SELECT foo FROM IDENTIFIER('x')")
771771
self.validate_identity("WITH x AS (SELECT 1 AS foo) SELECT IDENTIFIER('foo') FROM x")
772+
self.validate_identity("SELECT IDENTIFIER($my_function_name)()")
773+
self.validate_identity("SELECT IDENTIFIER('speed_of_light')()")
774+
self.validate_all(
775+
"SELECT IDENTIFIER('my_func')(1, 2)",
776+
write={
777+
"snowflake": "SELECT IDENTIFIER('my_func')(1, 2)",
778+
"duckdb": UnsupportedError,
779+
},
780+
)
772781
self.validate_identity("INITCAP('iqamqinterestedqinqthisqtopic', 'q')")
773782
self.validate_identity("OBJECT_CONSTRUCT(*)")
774783
self.validate_identity("SELECT CAST('2021-01-01' AS DATE) + INTERVAL '1 DAY'")

0 commit comments

Comments
 (0)