Conversation
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.
| _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'^"(?:[^"]|"")+"$') |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Thanks, that could make this simpler. Happy to try SQLGlot here.
@khush-bhatia, what do you think?
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
| _UNQUOTED_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_$]*$") | ||
| _QUOTED_IDENTIFIER = re.compile(r'^"(?:[^"]|"")+"$') |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
Summary
_parse_sourcein 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\nafter the keyword, is wrapped in parentheses, or has no whitespace afterSELECTfell through to the relation path, which split the text on dots and emitted an uppercaseddatabase/schema/tablewith no warning:became
base_table: {database: "-- REVENUE SOURCE\nSELECT AMOUNT FROM DB", schema: SCHEMA, table: ORDERS}.This PR:
_is_query_source, which skips leading whitespace, SQL comments and opening parentheses, then matchesSELECT/WITHonly when the next character cannot continue an unquoted identifier.SELECT_RESULTSandSELECT$ARCHIVEstay relations;SELECT*FROM,SELECT/*c*/and CRLF after the keyword are queries. Query text is emitted verbatim asbase_table.definition, which Snowflake documents for query-backed tables.OssieConversionErrorotherwise, 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
core-spec/and follow the existing structureOntology
ontology/are consistent with spec changesConverters
converters/is updated to reflect spec or ontology changesValidation
validation/are updated if the spec changedDocumentation
docs/is updated to reflect any user-facing changesCONTRIBUTING.mdis updated if the contribution process changedExamples
examples/are added or updated for any new spec constructs or converter supportTests
pytest/ CI green)Compliance