fix: cover MUTATE_AFTER_SPLIT branches, guard empty/mismatched mutated statements

Add missing coverage for the MUTATE_AFTER_SPLIT=True branch, add type
hints to satisfy dev-standard lint, and guard against a SQL_QUERY_MUTATOR
that strips all statements or changes statement count when mutating
before the split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Evan
2026-07-12 09:21:59 -07:00
parent c6bf4e65b6
commit ef1ec529bc
6 changed files with 201 additions and 8 deletions

View File

@@ -151,11 +151,26 @@ def _prepare_statement_blocks(
# `execute_sql_with_cursor`, which is a no-op here since its
# `is_split=True` no longer matches the config) operate on the
# already-mutated SQL.
mutated_sql = database.mutate_sql_based_on_config(
mutated_sql: str = database.mutate_sql_based_on_config(
parsed_script.format(comments=db_engine_spec.allows_sql_comments),
is_split=False,
)
parsed_script = SQLScript(mutated_sql, engine=db_engine_spec.engine)
if not parsed_script.statements:
# A `SQL_QUERY_MUTATOR` that strips a query down to nothing
# (e.g. only comments/whitespace) would otherwise leave us with
# an empty `blocks` list, skipping the execution loop below and
# surfacing a confusing error instead of a clean one.
raise SupersetErrorException(
SupersetError(
message=__(
"The SQL query mutator removed all executable "
"statements from this query."
),
error_type=SupersetErrorType.INVALID_SQL_ERROR,
level=ErrorLevel.ERROR,
)
)
blocks = [
statement.format(comments=db_engine_spec.allows_sql_comments)
for statement in parsed_script.statements
@@ -179,6 +194,14 @@ def _finalize_successful_query(
# Get original statement strings
original_sqls = [stmt.format() for stmt in original_script.statements]
if len(original_sqls) != len(execution_results):
# A `SQL_QUERY_MUTATOR` that changes the number of statements (e.g. by
# prepending a `SET ROLE` statement when run on the whole, un-split
# query) can leave the un-mutated `original_script` no longer aligned
# 1:1 with `execution_results`. Fall back to labeling each result with
# its own executed SQL rather than crash a query that ran successfully.
original_sqls = [exec_sql for exec_sql, *_ in execution_results]
for orig_sql, (exec_sql, result_set, exec_time, rowcount) in zip(
original_sqls, execution_results, strict=True
):