Skip to content

Commit d0a9296

Browse files
committed
fix: use upstream Polyglot parser
1 parent f094f04 commit d0a9296

18 files changed

Lines changed: 429 additions & 44 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ dependencies = [
1515
"duckdb>=1.3.2",
1616
"filelock>=3.20.3",
1717
"orjson>=3.10.0",
18-
"polyglot-sql-chio==0.9.3",
18+
"polyglot-sql==0.11.0",
1919
"pydantic-settings>=2,<3",
2020
"PyYAML>=6.0.2",
2121
"rich>=15.0.0",

‎scripts/rules_benchmark/_helpers/workflow.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,7 @@ def _hardware_payload() -> dict[str, object]:
10991099
def _version_payload() -> dict[str, str]:
11001100
return {
11011101
"sqlbuild": importlib.metadata.version("sqlbuild"),
1102-
"polyglot_sql": importlib.metadata.version("polyglot-sql-chio"),
1102+
"polyglot_sql": importlib.metadata.version("polyglot-sql"),
11031103
}
11041104

11051105

‎src/sqlbuild/cli/commands/_helpers/compile/sql_test_artifact_cache.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def build_sql_test_artifact_identity_context(
4242
{
4343
"algorithm": _ALGORITHM_FINGERPRINT,
4444
"sqlbuild_version": _package_version("sqlbuild"),
45-
"polyglot_version": _package_version("polyglot-sql-chio"),
45+
"polyglot_version": _package_version("polyglot-sql"),
4646
"python_version": platform.python_version_tuple()[:2],
4747
"adapter_class": f"{type(adapter).__module__}.{type(adapter).__qualname__}",
4848
"set_difference_operator": adapter.render_set_difference_operator(),

‎src/sqlbuild/compiler/compile/_helpers/analysis/cache.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def build_analysis_cache_context(
8585
"algorithm": _ANALYSIS_ALGORITHM_FINGERPRINT,
8686
"cache_version": _ANALYSIS_CACHE_VERSION,
8787
"sqlbuild_version": _package_version("sqlbuild"),
88-
"polyglot_version": _package_version("polyglot-sql-chio"),
88+
"polyglot_version": _package_version("polyglot-sql"),
8989
"python_version": platform.python_version_tuple()[:2],
9090
"allow_compact_analysis": allow_compact_analysis,
9191
"inference_profile": profile_payload,

‎src/sqlbuild/compiler/compile/_helpers/analysis/columns.py‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@
233233
TIMESTAMP_WITH_TIME_ZONE_SQL_TYPE_NAME as _TIMESTAMP_WITH_TIME_ZONE_SQL_TYPE_NAME,
234234
)
235235
from sqlbuild.compiler.sql_analysis.main._find_matching_paren import find_matching_paren
236+
from sqlbuild.compiler.sql_analysis.main._normalize_for_polyglot import (
237+
normalize_sql_for_polyglot,
238+
)
236239
from sqlbuild.compiler.sql_analysis.main._schema_validation import get_schema_validations
237240
from sqlbuild.compiler.sql_analysis.main.import_polyglot_sql import import_polyglot_sql
238241
from sqlbuild.compiler.sql_analysis.models import (
@@ -268,7 +271,10 @@ def infer_columns_with_sql_analysis(
268271

269272
profile: ExpressionInferenceProfile = inference_profile or ExpressionInferenceProfile()
270273

271-
cleaned_sql: str = _replace_refs_with_stubs(query_sql)
274+
cleaned_sql: str = _replace_refs_with_stubs(
275+
query_sql=query_sql,
276+
dialect=profile.sql_analysis_dialect,
277+
)
272278
if placeholders:
273279
cleaned_sql = substitute_placeholder_defaults(
274280
query_sql=cleaned_sql, placeholders=placeholders
@@ -295,7 +301,10 @@ def analyze_columns_with_polyglot(
295301
"""Infer columns with one Polyglot parse, returning False when unavailable."""
296302

297303
profile: ExpressionInferenceProfile = inference_profile or ExpressionInferenceProfile()
298-
cleaned_sql: str = _replace_refs_with_stubs(query_sql)
304+
cleaned_sql: str = _replace_refs_with_stubs(
305+
query_sql=query_sql,
306+
dialect=profile.sql_analysis_dialect,
307+
)
299308
if placeholders:
300309
cleaned_sql = substitute_placeholder_defaults(
301310
query_sql=cleaned_sql, placeholders=placeholders
@@ -324,7 +333,10 @@ def analyze_columns_and_lineage_with_polyglot(
324333

325334
polyglot_module: Any = import_polyglot_sql()
326335
profile: ExpressionInferenceProfile = inference_profile or ExpressionInferenceProfile()
327-
cleaned_sql: str = _replace_refs_with_stubs(query_sql)
336+
cleaned_sql: str = _replace_refs_with_stubs(
337+
query_sql=query_sql,
338+
dialect=profile.sql_analysis_dialect,
339+
)
328340
if placeholders:
329341
cleaned_sql = substitute_placeholder_defaults(
330342
query_sql=cleaned_sql, placeholders=placeholders
@@ -398,7 +410,7 @@ def get_complete_schema_binding_request(
398410
) -> SqlSchemaValidationRequest:
399411
"""Build one stable native schema-validation request."""
400412

401-
cleaned_sql: str = _replace_refs_with_stubs(query_sql)
413+
cleaned_sql: str = _replace_refs_with_stubs(query_sql=query_sql, dialect=dialect)
402414
if placeholders:
403415
cleaned_sql = substitute_placeholder_defaults(
404416
query_sql=cleaned_sql,
@@ -1578,7 +1590,7 @@ def _replacer(match: re.Match[str]) -> str:
15781590
return _PLACEHOLDER_PATTERN.sub(_replacer, query_sql)
15791591

15801592

1581-
def _replace_refs_with_stubs(query_sql: str) -> str:
1593+
def _replace_refs_with_stubs(*, query_sql: str, dialect: str | None = None) -> str:
15821594
"""Replace SQLBuild marker calls with parseable SQL stubs."""
15831595

15841596
result: str = _REF_PATTERN.sub(r"\1", query_sql)
@@ -1587,7 +1599,7 @@ def _replace_refs_with_stubs(query_sql: str) -> str:
15871599
result = _DBT_REF_PATTERN.sub(r"\1", result)
15881600
result = _UDF_PATTERN.sub(r"__sqlbuild_udf_\1", result)
15891601
result = _replace_table_function_calls_with_stubs(result)
1590-
return result
1602+
return normalize_sql_for_polyglot(sql=result, dialect=dialect)
15911603

15921604

15931605
def _replace_table_function_calls_with_stubs(query_sql: str) -> str:

‎src/sqlbuild/compiler/compile/_helpers/analysis/required_columns.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def resolve_required_external_columns(
7171
polyglot_module: Any = import_polyglot_sql()
7272
try:
7373
parsed: Any = polyglot_module.parse_one(
74-
_replace_refs_with_stubs(query_sql),
74+
_replace_refs_with_stubs(query_sql=query_sql, dialect=dialect),
7575
dialect=dialect or "generic",
7676
)
7777
except polyglot_module.PolyglotError:

‎src/sqlbuild/compiler/compile/_helpers/analysis/validation.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def validate_sql_syntax(
2424
) -> None:
2525
"""Validate that the model query SQL is parseable by Polyglot."""
2626

27-
cleaned_sql: str = _replace_refs_with_stubs(query_sql)
27+
cleaned_sql: str = _replace_refs_with_stubs(query_sql=query_sql, dialect=dialect)
2828
if placeholders:
2929
cleaned_sql = substitute_placeholder_defaults(
3030
query_sql=cleaned_sql, placeholders=placeholders
@@ -171,6 +171,7 @@ def _validate_sql_syntax_with_message(
171171
cleaned_sql: str = _clean_sql_for_validation(
172172
query_sql=query_sql,
173173
placeholders=placeholders,
174+
dialect=dialect,
174175
)
175176
polyglot_module: Any = import_polyglot_sql()
176177
try:
@@ -194,14 +195,20 @@ def _validate_hook_sql_with_message(
194195
cleaned_sql: str = _clean_sql_for_validation(
195196
query_sql=query_sql,
196197
placeholders=placeholders,
198+
dialect=dialect,
197199
)
198200
error_message: str | None = _validate_sql_with_polyglot(sql=cleaned_sql, dialect=dialect)
199201
if error_message is not None:
200202
_raise_sql_validation_error(error_prefix=error_prefix, error_message=error_message)
201203

202204

203-
def _clean_sql_for_validation(*, query_sql: str, placeholders: dict[str, str] | None) -> str:
204-
cleaned_sql: str = _replace_refs_with_stubs(query_sql)
205+
def _clean_sql_for_validation(
206+
*,
207+
query_sql: str,
208+
placeholders: dict[str, str] | None,
209+
dialect: str | None,
210+
) -> str:
211+
cleaned_sql: str = _replace_refs_with_stubs(query_sql=query_sql, dialect=dialect)
205212
if placeholders:
206213
return substitute_placeholder_defaults(
207214
query_sql=cleaned_sql,
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Dialect compatibility normalization for Polyglot analysis input."""
2+
3+
from __future__ import annotations
4+
5+
import re
6+
7+
from sqlbuild.compiler.sql_analysis._helpers.scanning import (
8+
skip_block_comment_impl,
9+
skip_line_comment_impl,
10+
skip_quoted_text_impl,
11+
)
12+
from sqlbuild.compiler.sql_analysis.constants import (
13+
SNOWFLAKE_DIALECT_NAME,
14+
SQL_CLOSE_BRACKET,
15+
SQL_DOLLAR_QUOTE_DELIMITER,
16+
SQL_ESCAPE_CHARACTER,
17+
SQL_OPEN_BRACKET,
18+
SQL_QUOTE_CHARACTERS,
19+
SQL_STRING_QUOTE_CHARACTER,
20+
)
21+
22+
_IDENTIFIER: str = r'(?:[A-Za-z_][A-Za-z0-9_$]*|"(?:""|[^"])+")'
23+
_VARIANT_PATH_SUFFIX: re.Pattern[str] = re.compile(
24+
rf"(?P<base>{_IDENTIFIER}(?:\.{_IDENTIFIER})*:{_IDENTIFIER}"
25+
rf"(?:(?::|\.){_IDENTIFIER})*)$"
26+
)
27+
_SIMPLE_BRACKET_KEY: re.Pattern[str] = re.compile(rf"\s*(?:{_IDENTIFIER}|[0-9]+|'(?:''|[^'])*')\s*")
28+
_SCAN_SPECIAL: re.Pattern[str] = re.compile(r"[-/$'\"`\[]")
29+
30+
31+
def normalize_sql_for_polyglot_impl(*, sql: str, dialect: str | None) -> str:
32+
"""Return analysis-equivalent SQL for Polyglot's supported dialect surface."""
33+
34+
if dialect is None or dialect.casefold() != SNOWFLAKE_DIALECT_NAME:
35+
return sql
36+
replacements: list[tuple[int, int, str]] = []
37+
index: int = 0
38+
while index < len(sql):
39+
special: re.Match[str] | None = _SCAN_SPECIAL.search(sql, index)
40+
if special is None:
41+
break
42+
index = special.start()
43+
if sql.startswith("--", index):
44+
index = skip_line_comment_impl(sql=sql, start=index)
45+
continue
46+
if sql.startswith("/*", index):
47+
index = skip_block_comment_impl(
48+
sql=sql,
49+
start=index,
50+
context="Snowflake SQL analysis",
51+
)
52+
continue
53+
if sql.startswith(SQL_DOLLAR_QUOTE_DELIMITER, index):
54+
index = _skip_dollar_quoted_text(sql=sql, start=index)
55+
continue
56+
if sql[index] in SQL_QUOTE_CHARACTERS:
57+
index = _skip_snowflake_quoted_text(sql=sql, start=index)
58+
continue
59+
close_bracket: int | None = _matching_bracket(sql=sql, open_bracket=index)
60+
if close_bracket is None:
61+
index += 1
62+
continue
63+
key_sql: str = sql[index + 1 : close_bracket]
64+
normalized_key: str = normalize_sql_for_polyglot_impl(sql=key_sql, dialect=dialect)
65+
base_end: int = _trailing_trivia_start(sql=sql, end=index)
66+
base_match: re.Match[str] | None = _VARIANT_PATH_SUFFIX.search(sql[:base_end])
67+
if base_match is not None and _SIMPLE_BRACKET_KEY.fullmatch(key_sql) is None:
68+
base_sql: str = base_match.group("base")
69+
trivia: str = sql[base_end:index]
70+
replacements.append(
71+
(
72+
base_match.start("base"),
73+
close_bracket + 1,
74+
f"GET({base_sql}{trivia}, {normalized_key})",
75+
)
76+
)
77+
elif normalized_key != key_sql:
78+
replacements.append((index + 1, close_bracket, normalized_key))
79+
index = close_bracket + 1
80+
normalized: str = sql
81+
for start, end, replacement in reversed(replacements):
82+
normalized = f"{normalized[:start]}{replacement}{normalized[end:]}"
83+
return normalized
84+
85+
86+
def _matching_bracket(*, sql: str, open_bracket: int) -> int | None:
87+
depth: int = 1
88+
index: int = open_bracket + 1
89+
while index < len(sql):
90+
if sql.startswith("--", index):
91+
index = skip_line_comment_impl(sql=sql, start=index)
92+
continue
93+
if sql.startswith("/*", index):
94+
index = skip_block_comment_impl(
95+
sql=sql,
96+
start=index,
97+
context="Snowflake SQL analysis",
98+
)
99+
continue
100+
if sql.startswith(SQL_DOLLAR_QUOTE_DELIMITER, index):
101+
index = _skip_dollar_quoted_text(sql=sql, start=index)
102+
continue
103+
if sql[index] in SQL_QUOTE_CHARACTERS:
104+
index = _skip_snowflake_quoted_text(sql=sql, start=index)
105+
continue
106+
if sql[index] == SQL_OPEN_BRACKET:
107+
depth += 1
108+
elif sql[index] == SQL_CLOSE_BRACKET:
109+
depth -= 1
110+
if depth == 0:
111+
return index
112+
index += 1
113+
return None
114+
115+
116+
def _skip_snowflake_quoted_text(*, sql: str, start: int) -> int:
117+
if sql[start] != SQL_STRING_QUOTE_CHARACTER:
118+
return skip_quoted_text_impl(sql=sql, start=start, context="Snowflake SQL analysis")
119+
index: int = start + 1
120+
while index < len(sql):
121+
if sql[index] == SQL_ESCAPE_CHARACTER:
122+
index += 2
123+
continue
124+
if sql[index] == SQL_STRING_QUOTE_CHARACTER:
125+
if index + 1 < len(sql) and sql[index + 1] == SQL_STRING_QUOTE_CHARACTER:
126+
index += 2
127+
continue
128+
return index + 1
129+
index += 1
130+
return len(sql)
131+
132+
133+
def _skip_dollar_quoted_text(*, sql: str, start: int) -> int:
134+
closing: int = sql.find(SQL_DOLLAR_QUOTE_DELIMITER, start + len(SQL_DOLLAR_QUOTE_DELIMITER))
135+
return len(sql) if closing == -1 else closing + len(SQL_DOLLAR_QUOTE_DELIMITER)
136+
137+
138+
def _trailing_trivia_start(*, sql: str, end: int) -> int:
139+
current: int = end
140+
while True:
141+
trimmed: int = current
142+
while trimmed > 0 and sql[trimmed - 1].isspace():
143+
trimmed -= 1
144+
if not sql[:trimmed].endswith("*/"):
145+
return trimmed
146+
opening: int = sql.rfind("/*", 0, trimmed - 2)
147+
if opening == -1:
148+
return trimmed
149+
current = opening

‎src/sqlbuild/compiler/sql_analysis/constants.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
SQL_QUOTE_CHARACTERS: frozenset[str] = frozenset({*SQL_ESCAPABLE_QUOTE_CHARACTERS, "`"})
77
SQL_OPEN_PARENTHESIS: str = "("
88
SQL_CLOSE_PARENTHESIS: str = ")"
9+
SQL_OPEN_BRACKET: str = "["
10+
SQL_CLOSE_BRACKET: str = "]"
11+
SQL_STRING_QUOTE_CHARACTER: str = "'"
12+
SQL_ESCAPE_CHARACTER: str = "\\"
13+
SQL_DOLLAR_QUOTE_DELIMITER: str = "$$"
914
SQL_IDENTIFIER_PREFIX: str = "_"
15+
SNOWFLAKE_DIALECT_NAME: str = "snowflake"
1016
BINDING_UNKNOWN_TABLE_INTERNAL_CODE: str = "B000"
1117

1218
POLYGLOT_KIND_ALIAS: str = "alias"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Public dialect compatibility normalization for Polyglot analysis input."""
2+
3+
from __future__ import annotations
4+
5+
from sqlbuild.compiler.sql_analysis._helpers.polyglot_normalization import (
6+
normalize_sql_for_polyglot_impl,
7+
)
8+
9+
10+
def normalize_sql_for_polyglot(*, sql: str, dialect: str | None) -> str:
11+
"""Return analysis-equivalent SQL for Polyglot's supported dialect surface."""
12+
13+
return normalize_sql_for_polyglot_impl(sql=sql, dialect=dialect)

0 commit comments

Comments
 (0)