This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 4ee500e47bf fix(explore): render Jinja before validating legacy chart
filters (#41996)
4ee500e47bf is described below
commit 4ee500e47bf064f098f359201994279b4e3ffaeb
Author: Jean Massucatto <[email protected]>
AuthorDate: Fri Jul 24 02:05:14 2026 -0300
fix(explore): render Jinja before validating legacy chart filters (#41996)
---
superset/utils/core.py | 10 ++++--
superset/viz.py | 10 ------
tests/unit_tests/core_tests.py | 63 ++++++++++++++++++++++++++++++++++
tests/unit_tests/test_viz_query_obj.py | 57 ++++++++++++++++++++++++++++++
4 files changed, 127 insertions(+), 13 deletions(-)
diff --git a/superset/utils/core.py b/superset/utils/core.py
index cdeba702802..5810beba34a 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -96,7 +96,6 @@ from superset.exceptions import (
SupersetException,
SupersetTimeoutException,
)
-from superset.sql.parse import sanitize_clause
from superset.superset_typing import (
AdhocColumn,
AdhocMetric,
@@ -1434,12 +1433,15 @@ def convert_legacy_filters_into_adhoc( # pylint:
disable=invalid-name
def split_adhoc_filters_into_base_filters( # pylint: disable=invalid-name
form_data: FormData,
- engine: str,
+ engine: str | None = None, # pylint: disable=unused-argument
) -> None:
"""
Mutates form data to restructure the adhoc filters in the form of the
three base
filters, `where`, `having`, and `filters` which represent free form where
sql,
free form having sql, and structured where clauses.
+
+ ``engine`` is retained for backwards compatibility and is unused: clauses
are
+ validated downstream, after Jinja templates are rendered.
"""
adhoc_filters = form_data.get("adhoc_filters")
if isinstance(adhoc_filters, list):
@@ -1460,7 +1462,9 @@ def split_adhoc_filters_into_base_filters( # pylint:
disable=invalid-name
)
elif expression_type == "SQL":
sql_expression = adhoc_filter.get("sqlExpression")
- sql_expression = sanitize_clause(sql_expression, engine)
+ # keep a trailing line comment from swallowing the " AND " join
+ if sql_expression and "--" in sql_expression:
+ sql_expression = f"{sql_expression}\n"
if clause == "WHERE":
sql_where_filters.append(sql_expression)
elif clause == "HAVING":
diff --git a/superset/viz.py b/superset/viz.py
index 412436854f7..8ee02d84240 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -56,7 +56,6 @@ from superset.exceptions import (
)
from superset.extensions import cache_manager, security_manager
from superset.models.helpers import QueryResult
-from superset.sql.parse import sanitize_clause
from superset.superset_typing import (
Column,
Metric,
@@ -414,15 +413,6 @@ class BaseViz: # pylint: disable=too-many-public-methods
self.from_dttm = from_dttm
self.to_dttm = to_dttm
- # validate sql filters
- for param in ("where", "having"):
- clause = self.form_data.get(param)
- if clause:
- engine = self.datasource.database.db_engine_spec.engine
- sanitized_clause = sanitize_clause(clause, engine)
- if sanitized_clause != clause:
- self.form_data[param] = sanitized_clause
-
# extras are used to query elements specific to a datasource type
# for instance the extra where clause that applies only to Tables
extras = {
diff --git a/tests/unit_tests/core_tests.py b/tests/unit_tests/core_tests.py
index acd0501d82f..3acad5fb744 100644
--- a/tests/unit_tests/core_tests.py
+++ b/tests/unit_tests/core_tests.py
@@ -30,6 +30,7 @@ from superset.utils.core import (
get_metric_names,
get_time_filter_status,
is_adhoc_metric,
+ split_adhoc_filters_into_base_filters,
)
from tests.unit_tests.fixtures.datasets import get_dataset_mock
@@ -55,6 +56,10 @@ SQL_ADHOC_COLUMN: AdhocColumn = {
"label": "My Adhoc Column",
"sqlExpression": "case when foo = 1 then 'foo' else 'bar' end",
}
+JINJA_HAVING = (
+ "sum(price_each) > {% if filter_values('threshold')|length %} "
+ "{{ filter_values('threshold')[0] }} {% else %} 0 {% endif %}"
+)
def test_get_metric_name_saved_metric():
@@ -233,3 +238,61 @@ def test_get_time_filter_status_no_temporal_col():
}
],
)
+
+
+def test_split_adhoc_filters_joins_sql_expressions():
+ form_data = {
+ "adhoc_filters": [
+ {
+ "expressionType": "SQL",
+ "clause": "WHERE",
+ "sqlExpression": "a = 1",
+ },
+ {
+ "expressionType": "SQL",
+ "clause": "WHERE",
+ "sqlExpression": "b = 2",
+ },
+ ]
+ }
+
+ split_adhoc_filters_into_base_filters(form_data)
+
+ assert form_data["where"] == "(a = 1) AND (b = 2)"
+
+
+def test_split_adhoc_filters_preserves_jinja_templates():
+ form_data = {
+ "adhoc_filters": [
+ {
+ "expressionType": "SQL",
+ "clause": "HAVING",
+ "sqlExpression": JINJA_HAVING,
+ }
+ ]
+ }
+
+ split_adhoc_filters_into_base_filters(form_data)
+
+ assert form_data["having"] == f"({JINJA_HAVING})"
+
+
+def test_split_adhoc_filters_terminates_inline_comments():
+ form_data = {
+ "adhoc_filters": [
+ {
+ "expressionType": "SQL",
+ "clause": "WHERE",
+ "sqlExpression": "a = 1 -- comment",
+ },
+ {
+ "expressionType": "SQL",
+ "clause": "WHERE",
+ "sqlExpression": "b = 2",
+ },
+ ]
+ }
+
+ split_adhoc_filters_into_base_filters(form_data)
+
+ assert form_data["where"] == "(a = 1 -- comment\n) AND (b = 2)"
diff --git a/tests/unit_tests/test_viz_query_obj.py
b/tests/unit_tests/test_viz_query_obj.py
new file mode 100644
index 00000000000..b5c6c04f627
--- /dev/null
+++ b/tests/unit_tests/test_viz_query_obj.py
@@ -0,0 +1,57 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Behavioral tests for ``viz.BaseViz.query_obj`` covering free-form filter
+clause handling.
+"""
+
+from typing import Any
+
+from superset import viz
+from superset.connectors.sqla.models import SqlaTable
+from superset.models.core import Database
+
+JINJA_HAVING = (
+ "sum(price_each) > {% if filter_values('threshold')|length %} "
+ "{{ filter_values('threshold')[0] }} {% else %} 0 {% endif %}"
+)
+
+
+def _viz(form_data: dict[str, Any]) -> viz.BaseViz:
+ database = Database(database_name="d", sqlalchemy_uri="sqlite://")
+ datasource = SqlaTable(
+ table_name="t",
+ columns=[],
+ metrics=[],
+ main_dttm_col=None,
+ database=database,
+ )
+ return viz.BaseViz(datasource=datasource, form_data=form_data)
+
+
+def test_query_obj_preserves_jinja_in_freeform_having():
+ """
+ A free-form HAVING clause containing Jinja must reach the query object
+ extras untouched: templates are only rendered (and the resulting SQL
+ validated) downstream, so validating the raw clause here would reject
+ valid templates (regression guard for premature clause validation).
+ """
+ obj = _viz({"viz_type": "table", "having": JINJA_HAVING})
+
+ query_obj = obj.query_obj()
+
+ assert query_obj["extras"]["having"] == f"({JINJA_HAVING})"