Skip to content

fix(snowflake): recognise query sources with leading comments - #377

Open
barveP wants to merge 4 commits into
apache:mainfrom
barveP:fix/snowflake-query-source
Open

barveP wants to merge 4 commits into
apache:mainfrom
barveP:fix/snowflake-query-source

Conversation

@barveP

@barveP barveP commented Sep 10, 2026

Copy link
Copy Markdown

Summary

_parse_source in the Snowflake converter decided "query or table" with a fixed prefix check (SELECT , SELECT\n, SELECT\t, WITH …). A query that starts with a --, // or /* */ comment, uses \r\n after the keyword, is wrapped in parentheses, or has no whitespace after SELECT fell through to the relation path, which split the text on dots and emitted an uppercased database/schema/table with no warning:

source: |
  -- revenue source
  SELECT amount FROM db.schema.orders

became base_table: {database: "-- REVENUE SOURCE\nSELECT AMOUNT FROM DB", schema: SCHEMA, table: ORDERS}.

This PR:

  • adds _is_query_source, which skips leading whitespace, SQL comments and opening parentheses, then matches SELECT/WITH only when the next character cannot continue an unquoted identifier. SELECT_RESULTS and SELECT$ARCHIVE stay relations; SELECT*FROM, SELECT/*c*/ and CRLF after the keyword are queries. Query text is emitted verbatim as base_table.definition, which Snowflake documents for query-backed tables.
  • makes the relation path require three valid quoted or unquoted identifiers and raise OssieConversionError otherwise, so unrecognised text can no longer become a database name.

No behaviour change for existing physical-table or plain-query inputs: all 107 existing tests pass unchanged, and 23 tests are added (query shapes, relation-shaped garbage rejection, a keyword-prefixed table name and a $ identifier as controls, and one end-to-end conversion). The change applies cleanly alongside #338. The Honeydew and NVIDIA converters have the same prefix check and will get follow-up PRs referencing #376.

Related Issues

Fixes #376

Checklist

Specification

  • Spec changes are included in core-spec/ and follow the existing structure
  • Spec changes have been discussed on the mailing list or in a linked issue
  • Breaking changes to the spec are clearly called out in the summary

Ontology

  • Ontology changes in ontology/ are consistent with spec changes
  • New or modified terms are defined and documented

Converters

  • Converter logic in converters/ is updated to reflect spec or ontology changes
  • New converters include tests under the converter's test directory

Validation

  • Validation rules in validation/ are updated if the spec changed
  • New validation cases are covered by tests

Documentation

  • docs/ is updated to reflect any user-facing changes
  • New features or behaviors are documented with examples where appropriate
  • CONTRIBUTING.md is updated if the contribution process changed

Examples

  • examples/ are added or updated for any new spec constructs or converter support

Tests

  • All existing tests pass (pytest / CI green)
  • New functionality is covered by tests

Compliance

  • ASF license headers are present on all new source files
  • No third-party dependencies are added without PMC/IPMC approval

A dataset source that is a SQL query but does not start with the bare
SELECT/WITH keyword (leading -- or /* */ comment, CRLF after the keyword,
surrounding parentheses, or no whitespace as in SELECT*FROM) fell through
to the relation path, which split the text on dots and emitted an
uppercased bogus database/schema/table with no warning.

Detect queries after skipping leading whitespace, SQL comments and opening
parentheses, matching SELECT/WITH as a whole word so names such as
SELECT_RESULTS stay relations. Query text is emitted verbatim as
base_table.definition. The relation path now requires three valid quoted
or unquoted identifiers and raises otherwise.

Fixes apache#376
The keyword check used a \b word boundary, but Snowflake allows `$` in
unquoted identifiers, so a valid table reference such as
select$archive.public.orders was classified as a query. Match the keyword
only when the next character cannot continue an unquoted identifier.

Also skip Snowflake's `// ...` single-line comments before a query, and
cover both cases with tests.
Comment on lines +432 to +437
_LEADING_SQL_TRIVIA = re.compile(
r"^(?:\s+|--[^\n]*(?:\n|$)|//[^\n]*(?:\n|$)|/\*.*?\*/)+", re.DOTALL
)
_QUERY_KEYWORD = re.compile(r"^(?:SELECT|WITH)(?![A-Za-z0-9_$])", re.IGNORECASE)
_UNQUOTED_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_$]*$")
_QUOTED_IDENTIFIER = re.compile(r'^"(?:[^"]|"")+"$')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably a question for @khush-bhatia but I see sqlglot used in other parts of Ossie & it supports the Snowflake dialect. I wonder if directly parsing would be a bit easier for handling edge cases when compared to regex

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks, that could make this simpler. Happy to try SQLGlot here.
@khush-bhatia, what do you think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

tried sqlglot and pushed an update. lmk what you think.

Use Snowflake tokenization to recognize SELECT/WITH sources while preserving the original SQL. Retain relation identifier validation and handle tokenization failures as conversion errors.

Declare SQLGlot in the Snowflake package dependencies and lockfile. Add regression coverage for comments, line endings, quoted identifiers, query preservation, and malformed sources.

@christianeu-db christianeu-db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looking a lot simpler - thanks for taking the feedback! I left one other comment (with some pseudocode) about the remaining regex could be removed / the parsing logic could be consolidated.

once that's in, it's worth re-pinging one of the committers (esp. one with snowflake expertise). I like the encapsulation of _is_query_source that you added - this makes the query detection logic easier to extend in the futrue

Comment on lines +428 to +429
_UNQUOTED_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_$]*$")
_QUOTED_IDENTIFIER = re.compile(r'^"(?:[^"]|"")+"$')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You might be able to avoid these regex as well with an approach like the following:

def try_parse_source_relation(source_stripped):
try:
table = parse_one(source_stripped, read="snowflake", into=exp.Table)
except ParseError:
return None
if table is None or len(table.parts) != 3 or not all(isinstance(p, exp.Identifier) for p in table.parts):
return None

return {
"database": _render_identifier(table.parts[0]),
"schema": _render_identifier(table.parts[1]),
"table": _render_identifier(table.parts[2]),
}

There is one nuance that the parse_one might be permissive to trailing semi-colons (so that might be a case to check in try_parse_source_relation => return None).

This helps keep the control flow a little cleaner:

if is_source(source_stripped):
return ...

parsed_relation = try_parse_relation(source_stripped)
if parsed_relation is not None:
return parsed_relation

throw_error

(ideas from this blog on the parse/don't validate pattern)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

thanks. took the control-flow shape in the latest commit.

i also tried parse_one(..., into=exp.Table) for the relation path. it works with four guards: trailing ; (as you noted), comments, extra args like an alias or AT (OFFSET => ...) that would otherwise be dropped silently, and isinstance(exp.Table), since a.b.c; d.e.f parses to a Block. with those it passes every test except from.public.orders and db.public.qualify

those two are the real question. sqlglot decides which keywords can be identifiers from its own ID_VAR_TOKENS list (parser doc), so it rejects from and qualify but accepts order, table, group, which snowflake reserves too (reserved-keywords). the regexes are snowflake's identifier grammar verbatim (identifiers): they check shape only and leave reserved words to snowflake

so it's partial enforcement via sqlglot, or none. i lean none because it's consistent but i'm fine either way and have the parse_one version ready (with a bump to sqlglot 30.13.0, since 30.12.0 misparses "@".public.orders).

which do we prefer?

cc: @khush-bhatia since you know the snowflake side best

Extract the existing three-part relation handling into a helper returning the table mapping or None. Preserve identifier validation, normalization, query passthrough, error messages, and dependency requirements.

Add compatibility coverage for quoted and keyword-shaped names, whitespace, and rejected table suffixes. Verify 174 tests on Python 3.11-3.14 and SQLGlot 30.12.0 compatibility; compare source parsing and CLI output with the published implementation.
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.

Snowflake converter turns a query source with a leading comment into a fake table

2 participants