aminghadersohi commented on code in PR #45681:
URL: https://github.com/apache/superset/pull/45681#discussion_r4396198533


##########
superset/models/helpers.py:
##########
@@ -4104,6 +4110,286 @@ def get_rendered_sql(
     def text(self, clause: str) -> TextClause:
         return self.db_engine_spec.get_text_clause(clause)
 
+    def _get_rls_schema(self) -> str:
+        """Resolve RLS tables with the same schema policy as SQL 
expressions."""
+        return get_rls_schema(
+            self.database,
+            self.catalog,
+            self.schema,
+            self.sql or "",
+            self.template_params_dict,
+        )
+
+    def _get_filter_column(
+        self,
+        name: str,
+        columns_by_name: dict[str, "TableColumn"],
+    ) -> Optional["TableColumn"]:
+        """Resolve real names before the first matching verbose filter 
label."""
+        col = columns_by_name.get(name)
+        if col is not None:
+            return col
+        return next((col for col in self.columns if col.verbose_name == name), 
None)
+
+    def _get_adhoc_column_rls_expression(
+        self,
+        col: AdhocColumn,
+        rendered_sql: str,
+        template_processor: BaseTemplateProcessor,
+    ) -> str | None:
+        """Query-backed adhoc columns execute their rendered SQL literally."""
+        return rendered_sql
+
+    def _get_query_sql_expressions(  # noqa: C901
+        self,
+        query_obj: QueryObjectDict,
+        template_processor: BaseTemplateProcessor,
+        granularity: str | None,
+        removed_filters: list[str],
+    ) -> list[str]:
+        """Render SQL-bearing chart fields without building a chart or its 
prequeries.
+
+        Render each source expression once, matching execution. An adhoc column
+        that resolves to saved metadata also renders that column's expression.
+        """
+        columns_by_name = {col.column_name: col for col in self.columns}
+        columns = {name: col.expression for name, col in 
columns_by_name.items()}
+        metrics = {metric.metric_name: metric.expression for metric in 
self.metrics}
+        selected_columns = query_obj.get("columns") or []
+        selected_metrics = query_obj.get("metrics") or []
+        adhoc_columns_by_label = {
+            col.get("label"): col
+            for col in selected_columns
+            if utils.is_adhoc_column(col)
+        }
+        metric_labels = {utils.get_metric_name(metric) for metric in 
selected_metrics}
+        expressions: list[str] = []
+
+        def add_expression(expression: str) -> None:
+            """Render source SQL, preserving literal Jinja in its output."""
+            expressions.append(template_processor.process_template(expression))
+
+        def add(value: Any, metric: bool = False) -> None:
+            """Resolve stored references in the namespace used by the query 
sink."""
+            if isinstance(value, str):
+                references = metrics if metric else columns
+                # Saved columns without expressions are quoted identifiers.
+                # Unknown selection strings have a separate SQL fallback below.
+                if expression := references.get(value):
+                    add_expression(expression)
+            elif isinstance(value, dict):
+                if sql := value.get("sqlExpression"):
+                    sql = template_processor.process_template(sql)
+                    if not metric:
+                        sql = self._get_adhoc_column_rls_expression(
+                            cast(AdhocColumn, value), sql, template_processor
+                        )
+                    if sql:
+                        expressions.append(sql)
+                add(value.get("column"))
+                add(value.get("column_name"))
+
+        def add_orderby(value: Any) -> None:
+            """ORDER BY prefers selected labels before stored metric/column 
names."""
+            if isinstance(value, str):
+                if value in metric_labels:
+                    # Selected metrics have already been collected.
+                    return
+                if value in adhoc_columns_by_label:
+                    add(adhoc_columns_by_label[value])
+                    return
+            add(
+                value,
+                metric=utils.is_adhoc_metric(value)
+                or (isinstance(value, str) and value in metrics),
+            )
+
+        def add_filter(value: Any) -> None:
+            """Filters accept verbose column names before stored metric 
names."""
+            # Execution renders adhoc/metric SQL before checking removal, but
+            # delays saved-column SQL until after that check.
+            if not isinstance(value, str):
+                add(value)
+            elif value == utils.DTTM_ALIAS and query_obj.get("is_timeseries", 
True):
+                if value not in removed_filters and granularity not in 
removed_filters:
+                    add(granularity)
+            elif (col := self._get_filter_column(value, columns_by_name)) is 
not None:
+                if value not in removed_filters and col.expression:
+                    add_expression(col.expression)
+            elif value in metrics:
+                add(value, metric=True)
+            elif value in adhoc_columns_by_label:
+                add(adhoc_columns_by_label[value])
+
+        effective_columns = query_obj.get("groupby") or [
+            col for col in selected_columns if col != utils.DTTM_ALIAS
+        ]
+        # ORDER BY metrics can enable grouping even without selected metrics.
+        need_groupby = bool(
+            query_obj.get("metrics") is not None
+            or query_obj.get("groupby")
+            or any(
+                utils.is_adhoc_metric(cast(Metric, value))
+                or (
+                    isinstance(value, str)
+                    and (
+                        value in metric_labels
+                        or (value in metrics and value not in 
adhoc_columns_by_label)
+                    )
+                )
+                for value, _ascending in query_obj.get("orderby") or []
+            )
+        )
+        for value in effective_columns:
+            if isinstance(value, str) and value not in columns:
+                # GROUP BY executes unknown strings as SQL. Detail selections
+                # quote them before rendering, unlike saved bare column names.
+                add_expression(
+                    value if need_groupby else 
self.database.quote_identifier(value)
+                )
+            else:
+                add(value)
+        for metric_value in selected_metrics:
+            add(metric_value, metric=True)
+        for orderby_value, _ascending in query_obj.get("orderby") or []:
+            add_orderby(orderby_value)
+        # FROM templates remove filters before execution renders time/filter 
columns.
+        # Render with the same processor so its removal state reaches this 
collector.
+        if self.sql:
+            self.get_rendered_sql(template_processor)

Review Comment:
   Correction to my previous reply at 
https://github.com/apache/superset/pull/45681#discussion_r4396010177 : two 
paths remained. Calculated filter/time expressions without subqueries still 
caused FROM rendering, and expression templates were rendered before checking 
for subqueries. The SqlaTable saved-column resolution I labeled “not affected” 
was affected too.
   
   Fixed in 1a121cb71cf0dc0c7154dbc96b8220f78e38a9f9. The collector inspects 
SQL and literal template inputs without executing them, and renders only for 
discovered table-reading subqueries. Quoted `SELECT` text and tableless 
subqueries do not require FROM removal state. Opaque or ambiguous outputs use 
the existing per-user/guest fallback key without running their macros, 
preserving cache isolation. Execution-time rendering and the existing 
ExtraCache gate are unchanged.
   
   `test_cache_key_skips_partition_templates_without_subqueries` covers all 
requested paths: calculated-column filters, non-timeseries calculated time 
columns, extras.where/having, calculated columns, saved metrics, adhoc SQL, 
fetch_values_predicate and adhoc saved-column resolution. Its 80 cases cover 
SqlaTable and Query, successful/failing partition discovery, plus time aliases, 
primary time filters, ordering, grouping, series metrics, quoted SELECT 
literals and tableless subqueries. It checks zero partition calls and includes 
a live-macro control.
   
   Additional tests: 
`test_opaque_expression_cache_key_is_viewer_scoped_without_rendering` checks 
distinct viewer keys and executed RLS results; 
`test_cache_key_inspection_does_not_execute_jinja_filters` and 
`test_unused_template_parameter_subquery_does_not_trigger_macro` guard the 
inspection itself. `test_nested_template_parameter_subquery_cache_keys` retains 
exact RLS keys for literal/parameter-generated SQL.
   
   With both production helpers restored to 
90d1e8a992f2dc98d2652cf9ad53b4d757fbe5f3, the changed-test selection gives **94 
failed, 6 passed**. The six passing cases are compatibility controls, not 
failure reproducers. After restoring this implementation, all **100** selected 
cases pass; the full touched file has **506 passed**. Requested suites plus 
schema and RLS helper tests: **5,786 passed**; SQL coverage gate: **3,126 
passed, 100.00%**. Existing swapped-RLS and removed-filter/series-limit 
regressions pass. Branch-scoped pre-commit and mypy pass.
   
   Siblings checked:
   - superset/models/helpers.py:4144 — fixed in 
1a121cb71cf0dc0c7154dbc96b8220f78e38a9f9 (side-effect-free template-input and 
SQL inspection)
   - superset/models/helpers.py:4231 — fixed in 
1a121cb71cf0dc0c7154dbc96b8220f78e38a9f9 (columns, filters, temporal axes, 
metrics, ordering, series limits, WHERE/HAVING and fetch predicates)
   - superset/connectors/sqla/models.py:2572 — fixed in 
1a121cb71cf0dc0c7154dbc96b8220f78e38a9f9 (resolved saved-column expressions are 
checked too)
   - superset/connectors/sqla/models.py:3196 — fixed in 
1a121cb71cf0dc0c7154dbc96b8220f78e38a9f9 (SqlaTable uses the shared collector)
   - superset/models/sql_lab.py:395 — fixed in 
1a121cb71cf0dc0c7154dbc96b8220f78e38a9f9 (Query uses the shared collector; it 
does not execute saved-column metadata)
   - superset/connectors/sqla/models.py:3034 — not affected: the existing 
ExtraCache-gated SQL build is unchanged.
   - superset/models/helpers.py:4598 — not affected: execution-time FROM 
rendering remains necessary and unchanged.
   - superset/utils/rls.py:38 — not affected: the existing identity helper 
distinguishes users and guest RLS scopes; the new opaque-template fallback 
reuses it.
   - superset/utils/rls.py:309 — not affected: underlying-table collection 
parses SQL rather than executing expression templates.
   - superset/sql/parse.py:3883 — not affected: schema-validation rendering 
already neutralizes partition macros.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to