diff --git a/sql_metadata/dialect_parser.py b/sql_metadata/dialect_parser.py index 87807458..10eeecd0 100644 --- a/sql_metadata/dialect_parser.py +++ b/sql_metadata/dialect_parser.py @@ -249,6 +249,18 @@ def _parse_with_dialect(clean_sql: str, dialect: Any) -> exp.Expression | None: dialect=dialect, error_level=sqlglot.ErrorLevel.WARN, ) + except (ParseError, TokenError): + # Re-raise so _try_dialects can report a real syntax error on the + # last dialect (see its except clause). + raise + except Exception: + # WARN mode is supposed to return a best-effort AST instead of + # raising, but sqlglot can still blow up while assembling that tree, + # e.g. an AttributeError on a node whose key is None for input like + # "{ =". Treat any such failure as "this dialect produced nothing" + # so the query is reported as invalid rather than crashing the + # public accessors with a raw sqlglot exception. + return None finally: logger.setLevel(old_level) diff --git a/test/test_malformed_input.py b/test/test_malformed_input.py new file mode 100644 index 00000000..34a68fd1 --- /dev/null +++ b/test/test_malformed_input.py @@ -0,0 +1,27 @@ +"""Regression tests for malformed input that used to crash the parser. + +sqlglot in best-effort (WARN) mode can build a partial AST and then raise while +assembling it, e.g. an ``AttributeError`` on a node whose key is ``None`` for a +few-character string like ``"{ ="``. That escaped DialectParser and every public +accessor crashed with a raw ``AttributeError`` instead of reporting an invalid +query. +""" + +import pytest + +from sql_metadata import InvalidQueryDefinition, Parser + + +@pytest.mark.parametrize("query", ["{ =", "SELECT { =", "x { ="]) +def test_bracket_equals_does_not_crash(query): + # query_type / tables validate the AST, so they surface the invalid query + # as InvalidQueryDefinition rather than an AttributeError. + with pytest.raises(InvalidQueryDefinition): + Parser(query).query_type + + with pytest.raises(InvalidQueryDefinition): + Parser(query).tables + + # The best-effort accessors must simply come back empty, not crash. + assert Parser(query).columns == [] + assert Parser(query).columns_dict == {}