Skip to content

Commit cedc35e

Browse files
authored
fix(SQLLab): remove error icon displayed when writing Jinja SQL even when the script is correct (#36422)
1 parent 12a266f commit cedc35e

File tree

3 files changed

+432
-0
lines changed

3 files changed

+432
-0
lines changed

superset/commands/database/validate_sql.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
)
3333
from superset.daos.database import DatabaseDAO
3434
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
35+
from superset.exceptions import (
36+
SupersetSyntaxErrorException,
37+
SupersetTemplateException,
38+
)
39+
from superset.jinja_context import get_template_processor
3540
from superset.models.core import Database
3641
from superset.sql_validators import get_validator_by_name
3742
from superset.sql_validators.base import BaseSQLValidator
@@ -62,12 +67,51 @@ def run(self) -> list[dict[str, Any]]:
6267
sql = self._properties["sql"]
6368
catalog = self._properties.get("catalog")
6469
schema = self._properties.get("schema")
70+
template_params = self._properties.get("template_params") or {}
71+
6572
try:
73+
# Render Jinja templates to handle template syntax before
74+
# validation. Note: The ENABLE_TEMPLATE_PROCESSING feature flag is
75+
# checked within get_template_processor(), which returns
76+
# NoOpTemplateProcessor when disabled. Template processing errors
77+
# (e.g., undefined filters, syntax errors) are caught by this
78+
# exception handler and surfaced to the client as
79+
# ValidatorSQLError or ValidatorSQL400Error with appropriate error
80+
# messages.
81+
template_processor = get_template_processor(self._model)
82+
# process_template() renders Jinja2 templates and always returns a
83+
# new string (does not mutate the input SQL). May raise
84+
# SupersetSyntaxErrorException for template syntax errors or
85+
# SupersetTemplateException for internal errors.
86+
sql = template_processor.process_template(sql, **template_params)
87+
6688
timeout = app.config["SQLLAB_VALIDATION_TIMEOUT"]
6789
timeout_msg = f"The query exceeded the {timeout} seconds timeout."
6890
with utils.timeout(seconds=timeout, error_message=timeout_msg):
6991
errors = self._validator.validate(sql, catalog, schema, self._model)
7092
return [err.to_dict() for err in errors]
93+
except SupersetSyntaxErrorException as ex:
94+
# Template syntax errors (e.g., invalid Jinja2 syntax, undefined variables)
95+
# These contain detailed error information including line numbers
96+
logger.warning(
97+
"Template syntax error during SQL validation",
98+
extra={"errors": [err.message for err in ex.errors]},
99+
)
100+
raise ValidatorSQL400Error(ex.errors[0]) from ex
101+
except SupersetTemplateException as ex:
102+
# Internal template processing errors (e.g., recursion, unexpected failures)
103+
logger.error(
104+
"Template processing error during SQL validation", exc_info=True
105+
)
106+
superset_error = SupersetError(
107+
message=__(
108+
"Template processing failed: %(ex)s",
109+
ex=str(ex),
110+
),
111+
error_type=SupersetErrorType.GENERIC_COMMAND_ERROR,
112+
level=ErrorLevel.ERROR,
113+
)
114+
raise ValidatorSQL400Error(superset_error) from ex
71115
except Exception as ex:
72116
logger.exception(ex)
73117
superset_error = SupersetError(

tests/integration_tests/databases/api_tests.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4104,6 +4104,115 @@ def test_validate_sql_endpoint_failure(self, get_validator_by_name):
41044104
assert rv.status_code == 422
41054105
assert "Kaboom!" in response["errors"][0]["message"]
41064106

4107+
@mock.patch.dict(
4108+
"superset.config.SQL_VALIDATORS_BY_ENGINE",
4109+
SQL_VALIDATORS_BY_ENGINE,
4110+
clear=True,
4111+
)
4112+
def test_validate_sql_with_jinja_templates(self):
4113+
"""
4114+
Database API: validate SQL with Jinja templates
4115+
"""
4116+
request_payload = {
4117+
"sql": (
4118+
"SELECT *\nFROM birth_names\nWHERE 1=1\n"
4119+
"{% if city_filter is defined %}\n"
4120+
" AND city = '{{ city_filter }}'\n{% endif %}\n"
4121+
"LIMIT {{ limit | default(100) }}"
4122+
),
4123+
"schema": None,
4124+
"template_params": {},
4125+
}
4126+
4127+
example_db = get_example_database()
4128+
if example_db.backend not in ("presto", "postgresql"):
4129+
pytest.skip("Only presto and PG are implemented")
4130+
4131+
self.login(ADMIN_USERNAME)
4132+
uri = f"api/v1/database/{example_db.id}/validate_sql/"
4133+
rv = self.client.post(uri, json=request_payload)
4134+
response = json.loads(rv.data.decode("utf-8"))
4135+
assert rv.status_code == 200
4136+
# Template was successfully rendered and validated
4137+
# so a valid query returns an empty result list
4138+
result = response["result"]
4139+
assert isinstance(result, list)
4140+
assert len(result) == 0
4141+
4142+
@mock.patch.dict(
4143+
"superset.config.SQL_VALIDATORS_BY_ENGINE",
4144+
SQL_VALIDATORS_BY_ENGINE,
4145+
clear=True,
4146+
)
4147+
def test_validate_sql_with_jinja_templates_and_params(self):
4148+
"""
4149+
Database API: validate SQL with Jinja templates and parameters
4150+
"""
4151+
request_payload = {
4152+
"sql": (
4153+
"SELECT *\nFROM birth_names\nWHERE 1=1\n"
4154+
"{% if city_filter is defined %}\n"
4155+
" AND city = '{{ city_filter }}'\n"
4156+
"{% endif %}\nLIMIT {{ limit }}"
4157+
),
4158+
"schema": None,
4159+
"template_params": {"city_filter": "New York", "limit": 50},
4160+
}
4161+
4162+
example_db = get_example_database()
4163+
if example_db.backend not in ("presto", "postgresql"):
4164+
pytest.skip("Only presto and PG are implemented")
4165+
4166+
self.login(ADMIN_USERNAME)
4167+
uri = f"api/v1/database/{example_db.id}/validate_sql/"
4168+
rv = self.client.post(uri, json=request_payload)
4169+
response = json.loads(rv.data.decode("utf-8"))
4170+
assert rv.status_code == 200
4171+
# Template was successfully rendered with parameters and validated
4172+
# so a valid query returns an empty result list
4173+
result = response["result"]
4174+
assert isinstance(result, list)
4175+
assert len(result) == 0
4176+
4177+
@mock.patch.dict(
4178+
"superset.config.SQL_VALIDATORS_BY_ENGINE",
4179+
SQL_VALIDATORS_BY_ENGINE,
4180+
clear=True,
4181+
)
4182+
def test_validate_sql_with_jinja_invalid_sql_after_render(self):
4183+
"""
4184+
Database API: validate SQL with Jinja templates that renders to invalid SQL
4185+
4186+
This test ensures that SQL validation errors are not hidden by template
4187+
processing. The template should render successfully, but the resulting SQL
4188+
should fail syntax validation.
4189+
"""
4190+
request_payload = {
4191+
"sql": (
4192+
"SELECT *\nFROM birth_names\n"
4193+
"{% if add_invalid_clause %}\n"
4194+
"WHERE\n"
4195+
"{% endif %}"
4196+
),
4197+
"schema": None,
4198+
"template_params": {"add_invalid_clause": True},
4199+
}
4200+
4201+
example_db = get_example_database()
4202+
if example_db.backend not in ("presto", "postgresql"):
4203+
pytest.skip("Only presto and PG are implemented")
4204+
4205+
self.login(ADMIN_USERNAME)
4206+
uri = f"api/v1/database/{example_db.id}/validate_sql/"
4207+
rv = self.client.post(uri, json=request_payload)
4208+
response = json.loads(rv.data.decode("utf-8"))
4209+
assert rv.status_code == 200
4210+
# The template should render successfully, but SQL validation
4211+
# should catch the syntax error (WHERE clause with no condition)
4212+
result = response["result"]
4213+
assert isinstance(result, list)
4214+
assert len(result) > 0
4215+
41074216
def test_get_databases_with_extra_filters(self):
41084217
"""
41094218
API: Test get database with extra query filter.

0 commit comments

Comments
 (0)