diff --git a/superset/sql/parse.py b/superset/sql/parse.py index 2026b0409b0..1d0bfe8b4e6 100644 --- a/superset/sql/parse.py +++ b/superset/sql/parse.py @@ -1856,13 +1856,15 @@ def sanitize_clause(clause: str, engine: str) -> str: Comments are the one exception: a trailing line comment can comment out surrounding SQL once the clause is embedded into a larger query (e.g. wrapped in parentheses), so any clause that contains comments is re-rendered - to normalize them into a safe form. + to normalize them into a safe form. A trailing statement terminator is + likewise stripped, since callers embed the clause inside a larger fragment + (``WHERE (...)``) where a stray ``;`` would produce invalid SQL. """ try: statement = SQLStatement(clause, engine) parsed = statement._parsed # pylint: disable=protected-access if not any(node.comments for node in parsed.walk()): - return clause + return clause.rstrip().rstrip(";").rstrip() return _normalized_generator( SQLGLOT_DIALECTS.get(engine), diff --git a/tests/unit_tests/sql/parse_tests.py b/tests/unit_tests/sql/parse_tests.py index 0f2d7439dda..8342a5ef264 100644 --- a/tests/unit_tests/sql/parse_tests.py +++ b/tests/unit_tests/sql/parse_tests.py @@ -3148,6 +3148,10 @@ def test_is_valid_cvas(sql: str, engine: str, expected: bool) -> None: ), # Block comments preserved ("col = 'col1 = 1) AND (col2 = 2'", "col = 'col1 = 1) AND (col2 = 2'", "base"), ("col = 'select 1; select 2'", "col = 'select 1; select 2'", "base"), + # Trailing statement terminators are stripped so the clause stays valid + # once embedded inside a larger fragment (e.g. ``WHERE (...)``). + ("col = 1;", "col = 1", "base"), + ("col = 1 ; ", "col = 1", "base"), ("col = 'abc -- comment'", "col = 'abc -- comment'", "base"), ("col1 = 1) AND (col2 = 2)", QueryClauseValidationException, "base"), ("(col1 = 1) AND (col2 = 2", QueryClauseValidationException, "base"),