Skip to content

Commit c41a012

Browse files
committed
fix: resolve Snowflake values columns
1 parent f5c66cd commit c41a012

2 files changed

Lines changed: 173 additions & 2 deletions

File tree

‎src/sqlbuild/compiler/sql_analysis/_helpers/schema_validation.py‎

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import sqlbuild._native as _native
1010
from sqlbuild.compiler.sql_analysis.exceptions import SqlAnalysisBoundaryError
11+
from sqlbuild.compiler.sql_analysis.main.import_polyglot_sql import import_polyglot_sql
1112
from sqlbuild.compiler.sql_analysis.models import (
1213
SqlBindingDiagnostic,
1314
SqlBindingResult,
@@ -25,6 +26,12 @@
2526
_ERROR_SEVERITY: str = "error"
2627
_ORDER_BY_CONTEXT: str = "ORDER BY"
2728
_WINDOW_ORDER_BY_CONTEXT: str = "WINDOW ORDER BY"
29+
_SNOWFLAKE_DIALECT: str = "snowflake"
30+
_SELECT_KIND: str = "select"
31+
_IMPLICIT_VALUES_COLUMN_PATTERN: re.Pattern[str] = re.compile(
32+
r"^Unknown column '(column([1-9][0-9]*))'$",
33+
flags=re.IGNORECASE,
34+
)
2835

2936

3037
def get_schema_validations(
@@ -43,7 +50,7 @@ def get_schema_validations(
4350
"native SQL schema validation returned an invalid batch response"
4451
)
4552
return tuple(
46-
_binding_result(sql=request.sql, response=response)
53+
_binding_result(sql=request.sql, dialect=request.dialect, response=response)
4754
for request, response in zip(requests, responses, strict=True)
4855
)
4956

@@ -73,7 +80,7 @@ def _request_payload(*, request: SqlSchemaValidationRequest) -> dict[str, object
7380
}
7481

7582

76-
def _binding_result(*, sql: str, response: object) -> SqlBindingResult:
83+
def _binding_result(*, sql: str, dialect: str | None, response: object) -> SqlBindingResult:
7784
response_dict: dict[str, object] | None = (
7885
cast(dict[str, object], response) if isinstance(response, dict) else None
7986
)
@@ -99,6 +106,14 @@ def _binding_result(*, sql: str, response: object) -> SqlBindingResult:
99106
line=_optional_int(value_dict.get("line")),
100107
column=_optional_int(value_dict.get("column")),
101108
)
109+
if _is_proven_snowflake_values_column(
110+
sql=sql,
111+
dialect=dialect,
112+
message=raw_message,
113+
line=line,
114+
column=column,
115+
):
116+
continue
102117
diagnostics.append(
103118
SqlBindingDiagnostic(
104119
code=_SQLBUILD_CODE_BY_NATIVE_CODE[native_code],
@@ -112,6 +127,138 @@ def _binding_result(*, sql: str, response: object) -> SqlBindingResult:
112127
return SqlBindingResult(diagnostics=tuple(diagnostics))
113128

114129

130+
def _is_proven_snowflake_values_column(
131+
*,
132+
sql: str,
133+
dialect: str | None,
134+
message: str,
135+
line: int | None,
136+
column: int | None,
137+
) -> bool:
138+
if (dialect or "").lower() != _SNOWFLAKE_DIALECT or line is None or column is None:
139+
return False
140+
match: re.Match[str] | None = _IMPLICIT_VALUES_COLUMN_PATTERN.fullmatch(message)
141+
if match is None:
142+
return False
143+
column_name: str = match.group(1)
144+
ordinal: int = int(match.group(2))
145+
polyglot_module: Any = import_polyglot_sql()
146+
try:
147+
parsed: Any = polyglot_module.parse_one(sql, dialect=_SNOWFLAKE_DIALECT)
148+
except polyglot_module.PolyglotError:
149+
return False
150+
selects: list[Any] = []
151+
if str(getattr(parsed, "kind", "")) == _SELECT_KIND:
152+
selects.append(parsed)
153+
selects.extend(parsed.find_all(_SELECT_KIND))
154+
return any(
155+
_select_proves_implicit_values_column(
156+
select=select,
157+
sql=sql,
158+
column_name=column_name,
159+
ordinal=ordinal,
160+
line=line,
161+
column=column,
162+
)
163+
for select in selects
164+
)
165+
166+
167+
def _select_proves_implicit_values_column(
168+
*,
169+
select: Any,
170+
sql: str,
171+
column_name: str,
172+
ordinal: int,
173+
line: int,
174+
column: int,
175+
) -> bool:
176+
payload: object = select.to_dict().get(_SELECT_KIND)
177+
if not isinstance(payload, dict):
178+
return False
179+
select_payload: dict[str, object] = cast(dict[str, object], payload)
180+
if select_payload.get("joins"):
181+
return False
182+
from_payload: object = select_payload.get("from")
183+
if not isinstance(from_payload, dict):
184+
return False
185+
relations: object = cast(dict[str, object], from_payload).get("expressions")
186+
if not isinstance(relations, list) or len(relations) != 1:
187+
return False
188+
relation: object = relations[0]
189+
if not isinstance(relation, dict):
190+
return False
191+
values: object = cast(dict[str, object], relation).get("values")
192+
if not isinstance(values, dict):
193+
return False
194+
values_payload: dict[str, object] = cast(dict[str, object], values)
195+
if values_payload.get("alias") is not None or values_payload.get("column_aliases"):
196+
return False
197+
rows: object = values_payload.get("expressions")
198+
if not isinstance(rows, list) or not rows:
199+
return False
200+
row_widths: list[int] = []
201+
for row in rows:
202+
if not isinstance(row, dict):
203+
return False
204+
expressions: object = cast(dict[str, object], row).get("expressions")
205+
if not isinstance(expressions, list):
206+
return False
207+
row_widths.append(len(expressions))
208+
if any(width < ordinal for width in row_widths):
209+
return False
210+
projections: object = select_payload.get("expressions")
211+
if not isinstance(projections, list):
212+
return False
213+
return any(
214+
_projection_is_implicit_values_column(
215+
projection=projection,
216+
sql=sql,
217+
column_name=column_name,
218+
line=line,
219+
column=column,
220+
)
221+
for projection in projections
222+
)
223+
224+
225+
def _projection_is_implicit_values_column(
226+
*, projection: object, sql: str, column_name: str, line: int, column: int
227+
) -> bool:
228+
if not isinstance(projection, dict):
229+
return False
230+
expression: object = projection
231+
alias_payload: object = cast(dict[str, object], projection).get("alias")
232+
if isinstance(alias_payload, dict):
233+
expression = cast(dict[str, object], alias_payload).get("this")
234+
if not isinstance(expression, dict):
235+
return False
236+
column_payload: object = cast(dict[str, object], expression).get("column")
237+
if not isinstance(column_payload, dict):
238+
return False
239+
column_dict: dict[str, object] = cast(dict[str, object], column_payload)
240+
if column_dict.get("table") is not None:
241+
return False
242+
name_payload: object = column_dict.get("name")
243+
if not isinstance(name_payload, dict):
244+
return False
245+
name_dict: dict[str, object] = cast(dict[str, object], name_payload)
246+
if (
247+
name_dict.get("quoted") is True
248+
or str(name_dict.get("name") or "").lower() != column_name.lower()
249+
):
250+
return False
251+
span: object = column_dict.get("span")
252+
if not isinstance(span, dict):
253+
return False
254+
start: object = cast(dict[str, object], span).get("start")
255+
if not isinstance(start, int):
256+
return False
257+
candidate_line: int = sql.count("\n", 0, start) + 1
258+
candidate_column: int = start - sql.rfind("\n", 0, start)
259+
return (candidate_line, candidate_column) == (line, column)
260+
261+
115262
def _optional_int(value: object) -> int | None:
116263
return value if isinstance(value, int) else None
117264

‎tests/unit/src/sqlbuild/compiler/sql_analysis/main/test_schema_validation_scopes.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,30 @@
109109
dialects=_SUPPORTED_SCHEMA_DIALECTS,
110110
expected_diagnostic_count=0,
111111
),
112+
SchemaValidationScopeTestCase(
113+
description="snowflake implicit values column resolves in direct projection",
114+
query_sql=(
115+
"WITH offsets AS (SELECT column1 AS offset_seconds "
116+
"FROM VALUES (0), (10)) SELECT offset_seconds FROM offsets"
117+
),
118+
schema={"orders": {"order_id": "integer"}},
119+
dialects=("snowflake",),
120+
expected_diagnostic_count=0,
121+
),
122+
SchemaValidationScopeTestCase(
123+
description="snowflake values column beyond row width remains unknown",
124+
query_sql="SELECT column2 FROM VALUES (0), (10)",
125+
schema={"orders": {"order_id": "integer"}},
126+
dialects=("snowflake",),
127+
expected_diagnostic_count=1,
128+
),
129+
SchemaValidationScopeTestCase(
130+
description="unknown column outside snowflake values remains unknown",
131+
query_sql="SELECT column1 FROM orders",
132+
schema={"orders": {"order_id": "integer"}},
133+
dialects=("snowflake",),
134+
expected_diagnostic_count=1,
135+
),
112136
],
113137
ids=lambda case: case.description,
114138
)

0 commit comments

Comments
 (0)