Skip to content

Commit b8fd9d0

Browse files
committed
Don't crash on malformed SQL that makes sqlglot raise mid-parse
DialectParser runs sqlglot in WARN mode so it returns a best-effort AST instead of raising. For a handful of short inputs like "{ =" sqlglot instead raises an AttributeError while assembling that partial tree (a node whose key is None). That was neither ParseError nor TokenError, so it escaped _try_dialects and every public accessor (tables, columns, query_type, ...) crashed with a raw AttributeError. Catch any such sqlglot-side failure in _parse_with_dialect and treat it as 'this dialect produced no result', so the query is reported through the normal InvalidQueryDefinition path. ParseError/TokenError are still re-raised so the existing syntax-error handling is unchanged.
1 parent 65c18c1 commit b8fd9d0

2 files changed

Lines changed: 39 additions & 0 deletions

File tree

sql_metadata/dialect_parser.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,18 @@ def _parse_with_dialect(clean_sql: str, dialect: Any) -> exp.Expression | None:
249249
dialect=dialect,
250250
error_level=sqlglot.ErrorLevel.WARN,
251251
)
252+
except (ParseError, TokenError):
253+
# Re-raise so _try_dialects can report a real syntax error on the
254+
# last dialect (see its except clause).
255+
raise
256+
except Exception:
257+
# WARN mode is supposed to return a best-effort AST instead of
258+
# raising, but sqlglot can still blow up while assembling that tree,
259+
# e.g. an AttributeError on a node whose key is None for input like
260+
# "{ =". Treat any such failure as "this dialect produced nothing"
261+
# so the query is reported as invalid rather than crashing the
262+
# public accessors with a raw sqlglot exception.
263+
return None
252264
finally:
253265
logger.setLevel(old_level)
254266

test/test_malformed_input.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Regression tests for malformed input that used to crash the parser.
2+
3+
sqlglot in best-effort (WARN) mode can build a partial AST and then raise while
4+
assembling it, e.g. an ``AttributeError`` on a node whose key is ``None`` for a
5+
few-character string like ``"{ ="``. That escaped DialectParser and every public
6+
accessor crashed with a raw ``AttributeError`` instead of reporting an invalid
7+
query.
8+
"""
9+
10+
import pytest
11+
12+
from sql_metadata import InvalidQueryDefinition, Parser
13+
14+
15+
@pytest.mark.parametrize("query", ["{ =", "SELECT { =", "x { ="])
16+
def test_bracket_equals_does_not_crash(query):
17+
# query_type / tables validate the AST, so they surface the invalid query
18+
# as InvalidQueryDefinition rather than an AttributeError.
19+
with pytest.raises(InvalidQueryDefinition):
20+
Parser(query).query_type
21+
22+
with pytest.raises(InvalidQueryDefinition):
23+
Parser(query).tables
24+
25+
# The best-effort accessors must simply come back empty, not crash.
26+
assert Parser(query).columns == []
27+
assert Parser(query).columns_dict == {}

0 commit comments

Comments
 (0)