gabotorresruiz commented on code in PR #45294:
URL: https://github.com/apache/superset/pull/45294#discussion_r4288779453


##########
superset/common/query_context_factory.py:
##########
@@ -434,8 +434,19 @@ def _apply_granularity(  # noqa: C901
                     if filter["col"] != filter_to_remove
                 ]
 
-    def _apply_filters(self, query_object: QueryObject) -> None:
+    def _apply_filters(
+        self, query_object: QueryObject, form_data: dict[str, Any] | None = 
None
+    ) -> None:
+        """Apply the time range, retaining the source of dashboard 
overrides."""
+        extra_form_data: object = (form_data or {}).get("extra_form_data")
+        dashboard_time_override: bool = (
+            isinstance(extra_form_data, dict)
+            and "time_range" in extra_form_data
+            and extra_form_data["time_range"] == query_object.time_range
+        )
         if query_object.time_range:
             for filter_object in query_object.filter:
                 if filter_object["op"] == "TEMPORAL_RANGE":
                     filter_object["val"] = query_object.time_range
+                    if dashboard_time_override:
+                        filter_object["isExtra"] = True

Review Comment:
   This block worries me a bit. The stamp is keyed on the op, so it marks every 
`TEMPORAL_RANGE` predicate on the query rather than the one the dashboard 
override targets, and `validate_filter_columns` short-circuits on `isExtra` 
before the metric branch. A chart-owned temporal filter therefore gets the 
dashboard pass as well.
   
   With a saved real range rather than a no-op, on `/api/v1/chart/data`:
   
   | chart-owned filter | `0ce9b6ca46` | `9ae3a13aa5` | `224a5fbd` |
   | --- | --- | --- | --- |
   | `gone_col` `TEMPORAL_RANGE` `2024-01-01 : 2024-06-01`, no dashboard range 
| 200 | 400 | 400 |
   | the same, with a dashboard range | 200 | 400 | **200**, `gone_col` 
rejected, provider queried |
   | `total_amount` same shape, with a dashboard range | **400** | **400** | 
**200**, `total_amount` rejected |
   
   The third row is the one I would not want to ship: the metric refusal that 
is already on `master` goes `400` to `200`, which also makes `metric filters 
remain unsupported` in the `UPDATING.md` bullet untrue in that state. It is the 
sibling of the ordering point you just fixed, reached through the other door. 
The second row means the headline guarantee holds only on a dashboard with no 
time-range filter.
   
   @aminghadersohi's case is a saved `No filter` predicate, so scoping the 
stamp to a value that was already unbounded closes it without handing out the 
pass (needs `is_no_time_range` imported from `superset.utils.date_parser`):
   
   ```suggestion
                   if filter_object["op"] == "TEMPORAL_RANGE":
                       saved = filter_object.get("val")
                       was_unbounded = saved is None or (
                           isinstance(saved, str) and is_no_time_range(saved)
                       )
                       filter_object["val"] = query_object.time_range
                       if dashboard_time_override and was_unbounded:
                           filter_object["isExtra"] = True
   ```
   
   I tried that plus the same guard in `dashboard_filter_context.py`: all three 
rows go back to the `9ae3a13aa5` column, and `metric_filter_test.py`, 
`tests/unit_tests/charts/test_chart_data_api.py` and `tests/unit_tests/common` 
all stay green. Could a real-range case join 
`test_dashboard_time_override_preserves_foreign_filter_provenance`? All four of 
its params save `No filter`, which is why this shape slipped through. And since 
the rule now lives in shared machinery, 
`tests/unit_tests/common/test_query_context_factory.py` feels like the right 
home for the stamping rule itself. Or am I misreading what the stamp is meant 
to cover?



##########
superset/charts/data/dashboard_filter_context.py:
##########
@@ -382,5 +383,11 @@ def apply_dashboard_filter_context(  # noqa: C901
         for src_key, target_key in 
EXTRA_FORM_DATA_OVERRIDE_REGULAR_MAPPINGS.items():
             if src_key in extra_form_data:
                 query[target_key] = extra_form_data[src_key]
+                if src_key == "time_range" and extra_form_data[src_key]:
+                    # The query factory rewrites temporal predicates with this 
range.
+                    filter_: QueryObjectFilterClause
+                    for filter_ in query.get("filters") or []:
+                        if filter_.get("op") == "TEMPORAL_RANGE":
+                            filter_["isExtra"] = True

Review Comment:
   Same scoping question here, and this path has a second caller: 
`superset/dashboards/excel_export/workbook.py:223` also goes through 
`apply_dashboard_filter_context`, so the stamp reaches dashboard Excel export 
and not just the `filters_dashboard_id` route. Worth noting that guarding only 
`_apply_filters` is not enough: with that one fixed, the `server` shape still 
returned `200` for both regressed rows until the same value check landed here.



##########
superset/semantic_layers/mapper.py:
##########
@@ -1392,22 +1393,46 @@ def validate_filter_columns(
     dimension_names: Collection[str],
     metric_names: Collection[str],
 ) -> None:
-    """Refuse chart-defined metric predicates; report incompatible extras 
later."""
+    """Refuse chart-defined predicates on metrics or on columns the view lacks.
+
+    Dashboard (``isExtra``) filters are not refused: incompatible ones are
+    skipped and reported, so a dashboard filter cannot break the chart. A
+    "No filter" time range constrains nothing, so its column is not checked.
+    """
     for filter_ in filters:
         column: Column = filter_["col"]
         if (
-            isinstance(column, str)
-            and column not in dimension_names
-            and column in metric_names
-            and not filter_.get("isExtra")
+            not isinstance(column, str)
+            or column in dimension_names
+            or filter_.get("isExtra")
+            or _is_no_op_time_range(filter_)
         ):
+            continue
+        if column in metric_names:
             raise QueryObjectValidationError(
                 _(
                     "Filter column '%(column)s' is a metric; semantic views 
only "
-                    "filter on dimensions",
+                    "filter on dimensions. Select a dimension or remove the 
filter.",
                     column=column,
                 )
             )
+        raise QueryObjectValidationError(

Review Comment:
   Confirmed closed, thanks. The metric no-op is back to `400` on `224a5fbd`: 
`{"col": "total_amount", "op": "TEMPORAL_RANGE", "val": "No filter"}` and `val: 
null` both return the metric error, matching `0ce9b6ca46`, and reverting the 
two line move turns all three `test_metric_no_op_time_range_is_still_refused` 
params red. One sibling of this same ordering shows up in the new 
`_apply_filters` stamp, which I left on its own line.



-- 
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