From b8fd9d05eaf2f7b47bb12429d8eba51e16f1ca59 Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Wed, 12 Aug 2026 08:23:14 +0530 Subject: [PATCH] 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. --- sql_metadata/dialect_parser.py | 12 ++++++++++++ test/test_malformed_input.py | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 test/test_malformed_input.py 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 == {}