Skip to content

feat(snowflake): transpile Snowflake IDENTIFIER function to DuckDB#7723

Open
fivetran-kwoodbeck wants to merge 5 commits into
mainfrom
transpile/snowflake-identifier-function
Open

feat(snowflake): transpile Snowflake IDENTIFIER function to DuckDB#7723
fivetran-kwoodbeck wants to merge 5 commits into
mainfrom
transpile/snowflake-identifier-function

Conversation

@fivetran-kwoodbeck

Copy link
Copy Markdown
Collaborator

Snowflake's IDENTIFIER(), which resolves a string to a runtime identifier, previously fell through to exp.Anonymous. This folds it into a typed exp.Identifier node (flagged with identifier_func=True) so the Snowflake generator reconstructs IDENTIFIER('name') and the DuckDB generator emits a native identifier. It includes splitting dot-qualified names like IDENTIFIER('t1.col') into a proper exp.Dot chain.

Non-foldable args like IDENTIFIER($var) remain as exp.Anonymous so they round-trip correctly.

Behavior

Snowflake input DuckDB output
IDENTIFIER('col') col
IDENTIFIER('t1.col') t1.col
IDENTIFIER('"Col"') "Col"
IDENTIFIER($var) IDENTIFIER($var)

@github-actions

This comment was marked as outdated.

@georgesittas georgesittas self-assigned this Jun 10, 2026

@georgesittas georgesittas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @fivetran-kwoodbeck, I don't think this is the right approach. We should parse the IDENTIFIER(...) argument, and only when it's a string, otherwise you risk breaking case-sensitive identifiers. I wonder if we should just use to_column and to_table directly and produce semantically equivalent SQL without the IDENTIFIER call.

Falling back to an Anonymous node in other cases is good. I would also have a try-except block to make sure we gracefully handle parse failures and fall back to the IDENTIFIER(...) syntax.

Check this out too: https://github.com/fivetran/sqlglot-integration-tests/pull/452#pullrequestreview-4467142494.

Comment thread sqlglot/parsers/snowflake.py Outdated
any_token: bool = True,
tokens: Collection[TokenType] | None = None,
) -> exp.Expr | None:
if self._match_text_seq("IDENTIFIER", "("):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come we need both this and an entry in FUNCTION_PARSERS to handle IDENTIFIER(...)?

@fivetran-kwoodbeck fivetran-kwoodbeck Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_parse_identifier_function is used for calls such as SELECT IDENTIFIER('foo') FROM t whereas _parse_id_var is used for calls such as CREATE TABLE IDENTIFIER('foo')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you verify that this is still needed though? If we remove the IDENTIFIER()-related logic from this method, does the parser fail to handle SQL like your example?

@fivetran-kwoodbeck fivetran-kwoodbeck force-pushed the transpile/snowflake-identifier-function branch from 12b5a1f to d97ad27 Compare June 11, 2026 04:20
Comment thread sqlglot/parsers/snowflake.py Outdated
Comment thread sqlglot/parsers/snowflake.py Outdated
Comment thread sqlglot/parsers/snowflake.py Outdated
Comment thread sqlglot/parsers/snowflake.py Outdated
Comment thread sqlglot/generators/snowflake.py Outdated
@fivetran-kwoodbeck fivetran-kwoodbeck force-pushed the transpile/snowflake-identifier-function branch from d97ad27 to 12e2d66 Compare June 12, 2026 18:28
Comment thread sqlglot/expressions/core.py Outdated


class DynamicIdentifier(Expression):
arg_types = {"this": True}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
arg_types = {"this": True}
pass

Comment thread sqlglot/expressions/core.py Outdated
return self.name


class DynamicIdentifier(Expression):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class DynamicIdentifier(Expression):
# https://docs.snowflake.com/en/sql-reference/identifier-literal
class DynamicIdentifier(Expression):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this should inherit from Expression, but instead from Func. If we do that:

  • We can do a simple rename_func in Snowflake's generator
  • We don't need to have an entry in FUNCTION_PARSERS, only map "IDENTIFIER" to it in FUNCTIONS and the rest is taken care of.

Just a couple of simplifications. Since this is really a function, it also aligns better with intuition.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, will update

Comment thread sqlglot/generator.py Outdated
Comment on lines +1950 to +1951
if this and this.is_string:
return this.name

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you still need to parse this when transpiling it? maybe_parse(this.name).sql(self.dialect). Otherwise if you transpile into something like Databricks, for example, you'll emit "-delimited text which is a string literal, not an identifier.

Comment thread sqlglot/generator.py
this = expression.this
if this and this.is_string:
return this.name
return self.func("IDENTIFIER", this)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should call unsupported if control falls to this return statement.

any_token: bool = True,
tokens: Collection[TokenType] | None = None,
) -> exp.Expr | None:
if self._match_text_seq("IDENTIFIER", "("):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you verify that this is still needed though? If we remove the IDENTIFIER()-related logic from this method, does the parser fail to handle SQL like your example?

Comment thread sqlglot/parsers/snowflake.py Outdated
Comment on lines +1125 to +1126
def _fold_identifier_literal(self, arg: exp.Expr | None) -> exp.Expr:
return self.expression(exp.DynamicIdentifier(this=arg))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks unnecessary, let's inline this where it's needed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The branch is needed, if we remove it, the following test fails:

CREATE TABLE IDENTIFIER('foo') (COLUMN1 VARCHAR, COLUMN2 VARCHAR)

Comment thread sqlglot/expressions/core.py Outdated
return self.name


class DynamicIdentifier(Expression):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this should inherit from Expression, but instead from Func. If we do that:

  • We can do a simple rename_func in Snowflake's generator
  • We don't need to have an entry in FUNCTION_PARSERS, only map "IDENTIFIER" to it in FUNCTIONS and the rest is taken care of.

Just a couple of simplifications. Since this is really a function, it also aligns better with intuition.

@fivetran-kwoodbeck fivetran-kwoodbeck force-pushed the transpile/snowflake-identifier-function branch from 12e2d66 to bdeb4b5 Compare June 16, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants