|
24 | 24 | "E223": "B005", |
25 | 25 | } |
26 | 26 | _ERROR_SEVERITY: str = "error" |
| 27 | +_NATIVE_UNKNOWN_COLUMN_CODE: str = "E201" |
| 28 | +_NATIVE_AMBIGUOUS_COLUMN_CODE: str = "E221" |
27 | 29 | _ORDER_BY_CONTEXT: str = "ORDER BY" |
28 | 30 | _WINDOW_ORDER_BY_CONTEXT: str = "WINDOW ORDER BY" |
29 | 31 | _SNOWFLAKE_DIALECT: str = "snowflake" |
|
32 | 34 | r"^Unknown column '(column([1-9][0-9]*))'$", |
33 | 35 | flags=re.IGNORECASE, |
34 | 36 | ) |
| 37 | +_UNKNOWN_COLUMN_PATTERN: re.Pattern[str] = re.compile( |
| 38 | + r"^Unknown column '([^']+)'(?: in table '[^']+'| \(not found in any referenced table\))$" |
| 39 | +) |
| 40 | +_AMBIGUOUS_UNQUALIFIED_COLUMN_PATTERN: re.Pattern[str] = re.compile( |
| 41 | + r"^Ambiguous unqualified column '([^']+)' found in [0-9]+ referenced tables$" |
| 42 | +) |
35 | 43 |
|
36 | 44 |
|
37 | 45 | def get_schema_validations( |
@@ -112,6 +120,11 @@ def _binding_result(*, sql: str, dialect: str | None, response: object) -> SqlBi |
112 | 120 | message=raw_message, |
113 | 121 | line=line, |
114 | 122 | column=column, |
| 123 | + ) or _is_proven_snowflake_output_alias( |
| 124 | + sql=sql, |
| 125 | + dialect=dialect, |
| 126 | + native_code=native_code, |
| 127 | + message=raw_message, |
115 | 128 | ): |
116 | 129 | continue |
117 | 130 | diagnostics.append( |
@@ -259,6 +272,127 @@ def _projection_is_implicit_values_column( |
259 | 272 | return (candidate_line, candidate_column) == (line, column) |
260 | 273 |
|
261 | 274 |
|
| 275 | +def _is_proven_snowflake_output_alias( |
| 276 | + *, sql: str, dialect: str | None, native_code: str, message: str |
| 277 | +) -> bool: |
| 278 | + if (dialect or "").lower() != _SNOWFLAKE_DIALECT: |
| 279 | + return False |
| 280 | + column_name: str | None = _output_alias_diagnostic_column( |
| 281 | + native_code=native_code, |
| 282 | + message=message, |
| 283 | + ) |
| 284 | + if column_name is None: |
| 285 | + return False |
| 286 | + polyglot_module: Any = import_polyglot_sql() |
| 287 | + try: |
| 288 | + parsed: Any = polyglot_module.parse_one(sql, dialect=_SNOWFLAKE_DIALECT) |
| 289 | + except polyglot_module.PolyglotError: |
| 290 | + return False |
| 291 | + selects: list[Any] = [] |
| 292 | + if str(getattr(parsed, "kind", "")) == _SELECT_KIND: |
| 293 | + selects.append(parsed) |
| 294 | + selects.extend(parsed.find_all(_SELECT_KIND)) |
| 295 | + return any( |
| 296 | + _select_proves_output_alias( |
| 297 | + select=select, |
| 298 | + column_name=column_name, |
| 299 | + native_code=native_code, |
| 300 | + ) |
| 301 | + for select in selects |
| 302 | + ) |
| 303 | + |
| 304 | + |
| 305 | +def _output_alias_diagnostic_column(*, native_code: str, message: str) -> str | None: |
| 306 | + pattern: re.Pattern[str] |
| 307 | + if native_code == _NATIVE_UNKNOWN_COLUMN_CODE: |
| 308 | + pattern = _UNKNOWN_COLUMN_PATTERN |
| 309 | + elif native_code == _NATIVE_AMBIGUOUS_COLUMN_CODE: |
| 310 | + pattern = _AMBIGUOUS_UNQUALIFIED_COLUMN_PATTERN |
| 311 | + else: |
| 312 | + return None |
| 313 | + match: re.Match[str] | None = pattern.fullmatch(message) |
| 314 | + return match.group(1) if match is not None else None |
| 315 | + |
| 316 | + |
| 317 | +def _select_proves_output_alias(*, select: Any, column_name: str, native_code: str) -> bool: |
| 318 | + payload: object = select.to_dict().get(_SELECT_KIND) |
| 319 | + if not isinstance(payload, dict): |
| 320 | + return False |
| 321 | + select_payload: dict[str, object] = cast(dict[str, object], payload) |
| 322 | + projections: object = select_payload.get("expressions") |
| 323 | + if not isinstance(projections, list): |
| 324 | + return False |
| 325 | + aliases: set[str] = set() |
| 326 | + for projection in projections: |
| 327 | + expression, alias = _projection_expression_and_alias(projection) |
| 328 | + if native_code == _NATIVE_UNKNOWN_COLUMN_CODE and column_name.lower() in aliases: |
| 329 | + if _payload_contains_unqualified_column( |
| 330 | + payload=expression, |
| 331 | + column_name=column_name, |
| 332 | + ): |
| 333 | + return True |
| 334 | + if alias is not None: |
| 335 | + aliases.add(alias.lower()) |
| 336 | + if column_name.lower() not in aliases: |
| 337 | + return False |
| 338 | + if native_code == _NATIVE_UNKNOWN_COLUMN_CODE: |
| 339 | + return _payload_contains_unqualified_column( |
| 340 | + payload=select_payload.get("where_clause"), |
| 341 | + column_name=column_name, |
| 342 | + ) |
| 343 | + return _payload_contains_unqualified_column( |
| 344 | + payload=select_payload.get("order_by"), |
| 345 | + column_name=column_name, |
| 346 | + ) |
| 347 | + |
| 348 | + |
| 349 | +def _projection_expression_and_alias(projection: object) -> tuple[object, str | None]: |
| 350 | + if not isinstance(projection, dict): |
| 351 | + return projection, None |
| 352 | + alias_payload: object = cast(dict[str, object], projection).get("alias") |
| 353 | + if not isinstance(alias_payload, dict): |
| 354 | + return projection, None |
| 355 | + alias_dict: dict[str, object] = cast(dict[str, object], alias_payload) |
| 356 | + name_payload: object = alias_dict.get("alias") |
| 357 | + if not isinstance(name_payload, dict): |
| 358 | + return alias_dict.get("this"), None |
| 359 | + name_dict: dict[str, object] = cast(dict[str, object], name_payload) |
| 360 | + alias: str | None = ( |
| 361 | + str(name_dict.get("name")) |
| 362 | + if name_dict.get("quoted") is not True and name_dict.get("name") is not None |
| 363 | + else None |
| 364 | + ) |
| 365 | + return alias_dict.get("this"), alias |
| 366 | + |
| 367 | + |
| 368 | +def _payload_contains_unqualified_column(*, payload: object, column_name: str) -> bool: |
| 369 | + if isinstance(payload, list | tuple): |
| 370 | + return any( |
| 371 | + _payload_contains_unqualified_column(payload=value, column_name=column_name) |
| 372 | + for value in payload |
| 373 | + ) |
| 374 | + if not isinstance(payload, dict): |
| 375 | + return False |
| 376 | + payload_dict: dict[str, object] = cast(dict[str, object], payload) |
| 377 | + column_payload: object = payload_dict.get("column") |
| 378 | + if isinstance(column_payload, dict): |
| 379 | + column_dict: dict[str, object] = cast(dict[str, object], column_payload) |
| 380 | + name_payload: object = column_dict.get("name") |
| 381 | + if isinstance(name_payload, dict): |
| 382 | + name_dict: dict[str, object] = cast(dict[str, object], name_payload) |
| 383 | + if ( |
| 384 | + column_dict.get("table") is None |
| 385 | + and name_dict.get("quoted") is not True |
| 386 | + and str(name_dict.get("name") or "").lower() == column_name.lower() |
| 387 | + ): |
| 388 | + return True |
| 389 | + return any( |
| 390 | + key != _SELECT_KIND |
| 391 | + and _payload_contains_unqualified_column(payload=value, column_name=column_name) |
| 392 | + for key, value in payload_dict.items() |
| 393 | + ) |
| 394 | + |
| 395 | + |
262 | 396 | def _optional_int(value: object) -> int | None: |
263 | 397 | return value if isinstance(value, int) else None |
264 | 398 |
|
|
0 commit comments