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


##########
superset/mcp_service/chart/preview_utils.py:
##########
@@ -469,11 +457,424 @@ def _is_nan(value: Any) -> bool:
         return False
 
 
+_GAUGE_COLORS = (
+    "#1FA8C9",
+    "#454E7C",
+    "#5AC189",
+    "#FF7F44",
+    "#666666",
+    "#E04355",
+    "#FCC700",
+    "#A868B7",
+)
+
+
+def _gauge_column_label(column: Any) -> str | None:
+    """Resolve the result label for a native QueryFormColumn."""
+    if isinstance(column, str) and column:
+        return column
+    if not isinstance(column, dict) or not 0 < len(column) <= 20:
+        return None
+    label = (
+        column.get("label")
+        or column.get("sqlExpression")
+        or column.get("column_name")
+        or column.get("columnName")
+    )
+    return label if isinstance(label, str) and label else None
+
+
+def _parse_gauge_number_list(value: Any, field_name: str) -> list[float]:
+    """Parse a bounded comma-separated Gauge control."""
+    if value in (None, ""):
+        return []
+    if not isinstance(value, str) or len(value) > 1000:
+        raise ValueError(f"Gauge {field_name} must be a bounded string")
+    try:
+        parsed = [float(part.strip()) for part in value.split(",")]
+    except ValueError as ex:
+        raise ValueError(
+            f"Gauge {field_name} must be a comma-separated list of numbers"
+        ) from ex
+    if not all(math.isfinite(number) for number in parsed):
+        raise ValueError(f"Gauge {field_name} values must be finite")
+    return parsed
+
+
+def _prepare_gauge_preview(  # noqa: C901
+    data: Any, form_data: Dict[str, Any]
+) -> tuple[list[dict[str, Any]], dict[str, Any]] | ChartError:
+    """Validate Gauge rows and derive display values used by both previews."""
+    failure = validate_gauge_query_result({"queries": [{"data": data}]}, 
form_data)
+    if failure is not None:
+        return failure
+    assert isinstance(data, list)
+    metric_label = metric_result_label(form_data.get("metric"))
+    assert metric_label is not None
+
+    raw_groupby = form_data.get("groupby") or []
+    if isinstance(raw_groupby, str):
+        raw_groupby = [raw_groupby]
+    if not isinstance(raw_groupby, list) or len(raw_groupby) > 10:
+        return ChartError(
+            error="Gauge groupby must contain at most 10 column references.",
+            error_type="InvalidGaugeFormData",
+        )
+    group_labels: list[str] = []
+    for index, column in enumerate(raw_groupby):
+        label = _gauge_column_label(column)
+        if label is None:
+            return ChartError(
+                error=f"Gauge groupby[{index}] has no resolvable result 
label.",
+                error_type="InvalidGaugeFormData",
+            )
+        group_labels.append(label)
+
+    values = [float(row[metric_label]) for row in data]
+
+    def numeric_bound(field_name: str) -> float | None:
+        value = form_data.get(field_name)
+        if value in (None, ""):
+            return None
+        if isinstance(value, bool) or not isinstance(value, (int, float, str)):
+            raise ValueError(f"Gauge {field_name} must be numeric")
+        try:
+            converted = float(value)
+        except ValueError as ex:
+            raise ValueError(f"Gauge {field_name} must be numeric") from ex
+        if not math.isfinite(converted):
+            raise ValueError(f"Gauge {field_name} must be finite")
+        return converted
+
+    try:
+        minimum = numeric_bound("min_val")
+        maximum = numeric_bound("max_val")
+        interval_bounds = _parse_gauge_number_list(
+            form_data.get("intervals", ""), "intervals"
+        )
+        color_indices = _parse_gauge_number_list(
+            form_data.get("interval_color_indices", ""),
+            "interval_color_indices",
+        )
+    except ValueError as ex:
+        return ChartError(error=str(ex), error_type="InvalidGaugeFormData")
+
+    # Match transformProps auto-range semantics: twice the extrema including 0.
+    if minimum is None:
+        minimum = 2 * min([*values, 0]) if values else 0
+    if maximum is None:
+        maximum = 2 * max([*values, 0]) if values else 1
+    if minimum >= maximum:
+        return ChartError(
+            error=(
+                f"Gauge preview range is invalid: min_val {minimum:g} must be "
+                f"less than max_val {maximum:g}."
+            ),
+            error_type="InvalidGaugeRange",
+        )
+    if any(
+        left >= right
+        for left, right in zip(interval_bounds, interval_bounds[1:], 
strict=False)
+    ):
+        return ChartError(
+            error="Gauge intervals must be strictly increasing.",
+            error_type="InvalidGaugeFormData",
+        )
+    if any(bound <= minimum or bound > maximum for bound in interval_bounds):
+        return ChartError(
+            error="Gauge intervals must fall within the resolved min/max 
range.",
+            error_type="InvalidGaugeFormData",
+        )

Review Comment:
   Confirmed and fixed in 14c1d525d98dbad8df6f2de98685a5c93f5f9610. Interval 
validation now checks only explicitly configured bounds, preserving the native 
data-derived automatic range; off-dial bands are clipped instead of rejecting 
stable thresholds. Regressions cover low positive/negative values, 
explicit-bound rejection, and the unsaved product preview path. Validation: 
1,558 chart/Explore/common tests passed; branch-wide pre-commit passed.



##########
superset/mcp_service/chart/preview_utils.py:
##########
@@ -469,11 +457,424 @@ def _is_nan(value: Any) -> bool:
         return False
 
 
+_GAUGE_COLORS = (
+    "#1FA8C9",
+    "#454E7C",
+    "#5AC189",
+    "#FF7F44",
+    "#666666",
+    "#E04355",
+    "#FCC700",
+    "#A868B7",
+)
+
+
+def _gauge_column_label(column: Any) -> str | None:
+    """Resolve the result label for a native QueryFormColumn."""
+    if isinstance(column, str) and column:
+        return column
+    if not isinstance(column, dict) or not 0 < len(column) <= 20:
+        return None
+    label = (
+        column.get("label")
+        or column.get("sqlExpression")
+        or column.get("column_name")
+        or column.get("columnName")
+    )
+    return label if isinstance(label, str) and label else None
+
+
+def _parse_gauge_number_list(value: Any, field_name: str) -> list[float]:
+    """Parse a bounded comma-separated Gauge control."""
+    if value in (None, ""):
+        return []
+    if not isinstance(value, str) or len(value) > 1000:
+        raise ValueError(f"Gauge {field_name} must be a bounded string")
+    try:
+        parsed = [float(part.strip()) for part in value.split(",")]
+    except ValueError as ex:
+        raise ValueError(
+            f"Gauge {field_name} must be a comma-separated list of numbers"
+        ) from ex
+    if not all(math.isfinite(number) for number in parsed):
+        raise ValueError(f"Gauge {field_name} values must be finite")
+    return parsed
+
+
+def _prepare_gauge_preview(  # noqa: C901
+    data: Any, form_data: Dict[str, Any]
+) -> tuple[list[dict[str, Any]], dict[str, Any]] | ChartError:
+    """Validate Gauge rows and derive display values used by both previews."""
+    failure = validate_gauge_query_result({"queries": [{"data": data}]}, 
form_data)
+    if failure is not None:
+        return failure
+    assert isinstance(data, list)
+    metric_label = metric_result_label(form_data.get("metric"))
+    assert metric_label is not None
+
+    raw_groupby = form_data.get("groupby") or []
+    if isinstance(raw_groupby, str):
+        raw_groupby = [raw_groupby]
+    if not isinstance(raw_groupby, list) or len(raw_groupby) > 10:
+        return ChartError(
+            error="Gauge groupby must contain at most 10 column references.",
+            error_type="InvalidGaugeFormData",
+        )
+    group_labels: list[str] = []
+    for index, column in enumerate(raw_groupby):
+        label = _gauge_column_label(column)
+        if label is None:
+            return ChartError(
+                error=f"Gauge groupby[{index}] has no resolvable result 
label.",
+                error_type="InvalidGaugeFormData",
+            )
+        group_labels.append(label)
+
+    values = [float(row[metric_label]) for row in data]
+
+    def numeric_bound(field_name: str) -> float | None:
+        value = form_data.get(field_name)
+        if value in (None, ""):
+            return None
+        if isinstance(value, bool) or not isinstance(value, (int, float, str)):
+            raise ValueError(f"Gauge {field_name} must be numeric")
+        try:
+            converted = float(value)
+        except ValueError as ex:
+            raise ValueError(f"Gauge {field_name} must be numeric") from ex
+        if not math.isfinite(converted):
+            raise ValueError(f"Gauge {field_name} must be finite")
+        return converted
+
+    try:
+        minimum = numeric_bound("min_val")
+        maximum = numeric_bound("max_val")
+        interval_bounds = _parse_gauge_number_list(
+            form_data.get("intervals", ""), "intervals"
+        )
+        color_indices = _parse_gauge_number_list(
+            form_data.get("interval_color_indices", ""),
+            "interval_color_indices",
+        )
+    except ValueError as ex:
+        return ChartError(error=str(ex), error_type="InvalidGaugeFormData")
+
+    # Match transformProps auto-range semantics: twice the extrema including 0.
+    if minimum is None:
+        minimum = 2 * min([*values, 0]) if values else 0
+    if maximum is None:
+        maximum = 2 * max([*values, 0]) if values else 1
+    if minimum >= maximum:
+        return ChartError(
+            error=(
+                f"Gauge preview range is invalid: min_val {minimum:g} must be "
+                f"less than max_val {maximum:g}."
+            ),
+            error_type="InvalidGaugeRange",
+        )
+    if any(
+        left >= right
+        for left, right in zip(interval_bounds, interval_bounds[1:], 
strict=False)
+    ):
+        return ChartError(
+            error="Gauge intervals must be strictly increasing.",
+            error_type="InvalidGaugeFormData",
+        )
+    if any(bound <= minimum or bound > maximum for bound in interval_bounds):
+        return ChartError(
+            error="Gauge intervals must fall within the resolved min/max 
range.",
+            error_type="InvalidGaugeFormData",
+        )
+    if color_indices and len(color_indices) != len(interval_bounds):
+        return ChartError(
+            error="Gauge interval colors must match the number of interval 
bounds.",
+            error_type="InvalidGaugeFormData",
+        )
+    if any(index < 1 or not index.is_integer() for index in color_indices):
+        return ChartError(
+            error="Gauge interval color indices must be positive integers.",
+            error_type="InvalidGaugeFormData",
+        )
+
+    currency = form_data.get("currency_format")
+    if currency is not None and not isinstance(currency, dict):
+        return ChartError(
+            error="Gauge currency_format must be an object.",
+            error_type="InvalidGaugeFormData",
+        )
+    number_format = form_data.get("number_format", "SMART_NUMBER")
+    value_formatter = form_data.get("value_formatter", "{value}")
+    if not isinstance(number_format, str) or not isinstance(value_formatter, 
str):
+        return ChartError(
+            error="Gauge number_format and value_formatter must be strings.",
+            error_type="InvalidGaugeFormData",
+        )
+    from superset.utils.number_format import format_number_with_config
+
+    decorated: list[dict[str, Any]] = []
+    for row_index, row in enumerate(data):
+        value = float(row[metric_label])
+        group = (
+            ", ".join(f"{label}: {row.get(label)}" for label in group_labels) 
or "Value"
+        )
+        formatted = format_number_with_config(number_format, currency, value)
+        decorated.append(
+            {
+                **row,
+                "__mcp_gauge_group": group,
+                "__mcp_gauge_ratio": (value - minimum) / (maximum - minimum),
+                "__mcp_gauge_display": value_formatter.replace(
+                    "{value}", str(formatted), 1
+                ),
+                "__mcp_gauge_row": row_index,
+            }
+        )
+
+    colors = [
+        _GAUGE_COLORS[(int(index) - 1) % len(_GAUGE_COLORS)] for index in 
color_indices
+    ] or list(_GAUGE_COLORS[: max(1, len(interval_bounds))])
+    return decorated, {
+        "metric_label": metric_label,
+        "group_labels": group_labels,
+        "minimum": minimum,
+        "maximum": maximum,
+        "interval_bounds": interval_bounds,
+        "interval_colors": colors,
+    }
+
+
+def generate_gauge_ascii_preview(
+    data: Any, form_data: Dict[str, Any], width: int = 80
+) -> str | ChartError:
+    """Render a bounded dial-like ASCII Gauge using the configured value 
format."""
+    prepared = _prepare_gauge_preview(data, form_data)
+    if isinstance(prepared, ChartError):
+        return prepared
+    decorated, metadata = prepared
+    minimum = metadata["minimum"]
+    maximum = metadata["maximum"]
+    lines = [
+        "Gauge Chart",
+        f"Range: {minimum:g} to {maximum:g}",
+    ]
+    if metadata["interval_bounds"]:
+        lines.append(
+            "Intervals: "
+            + ", ".join(f"{value:g}" for value in metadata["interval_bounds"])
+        )
+    if not decorated:
+        lines.append("No data available")
+        return "\n".join(lines)
+
+    width = min(max(width, 40), 200)
+    bar_width = max(10, min(40, width - 38))
+    for row in decorated[:10]:
+        ratio = min(1.0, max(0.0, row["__mcp_gauge_ratio"]))
+        filled = round(ratio * bar_width)
+        bar = "█" * filled + "░" * (bar_width - filled)
+        label = str(row["__mcp_gauge_group"])
+        lines.append(f"{label[:20]:>20} [{bar}] {row['__mcp_gauge_display']}")
+    return "\n".join(lines)
+
+
+def generate_gauge_vega_lite_preview(  # noqa: C901
+    data: Any, form_data: Dict[str, Any]
+) -> VegaLitePreview | ChartError:
+    """Build a Gauge-specific layered radial Vega-Lite preview."""
+    prepared = _prepare_gauge_preview(data, form_data)
+    if isinstance(prepared, ChartError):
+        return prepared
+    decorated, metadata = prepared
+    start_angle = form_data.get("start_angle", 225)
+    end_angle = form_data.get("end_angle", -45)
+
+    def finite_angle(value: Any) -> float | None:
+        if isinstance(value, bool) or not isinstance(value, (int, float, str)):
+            return None
+        try:
+            converted = float(value)
+        except ValueError:
+            return None
+        return converted if math.isfinite(converted) else None
+
+    numeric_start_angle = finite_angle(start_angle)
+    numeric_end_angle = finite_angle(end_angle)
+    if numeric_start_angle is None or numeric_end_angle is None:
+        return ChartError(
+            error="Gauge start_angle and end_angle must be finite numbers.",
+            error_type="InvalidGaugeFormData",
+        )
+
+    angle_range = [math.radians(numeric_start_angle), 
math.radians(numeric_end_angle)]
+    theta_scale = {"domain": [0, 1], "range": angle_range}
+    metric_label = metadata["metric_label"]
+    tooltip = [
+        {"field": field, "type": "nominal"} for field in 
metadata["group_labels"]
+    ]
+    tooltip.extend(
+        [
+            {"field": metric_label, "type": "quantitative"},
+            {"field": "__mcp_gauge_display", "type": "nominal", "title": 
"Value"},
+        ]
+    )
+    progress_color: dict[str, Any]
+    interval_bounds = metadata["interval_bounds"]
+    if interval_bounds:
+        progress_color = {
+            "field": metric_label,
+            "type": "quantitative",
+            "scale": {
+                "type": "threshold",
+                "domain": interval_bounds[:-1],
+                "range": metadata["interval_colors"],
+            },
+            "legend": None,
+        }
+    elif metadata["group_labels"]:
+        progress_color = {
+            "field": "__mcp_gauge_group",
+            "type": "nominal",
+            "legend": None,
+        }
+    else:
+        progress_color = {"value": _GAUGE_COLORS[0]}
+
+    background_layers: list[dict[str, Any]] = []
+    for bound, color in reversed(
+        list(zip(interval_bounds, metadata["interval_colors"], strict=False))
+    ):
+        background_layers.append(
+            {
+                "mark": {
+                    "type": "arc",
+                    "innerRadius": 55,
+                    "outerRadius": 82,
+                    "color": color,
+                    "opacity": 0.35,
+                },
+                "encoding": {
+                    "theta": {
+                        "datum": (bound - metadata["minimum"])
+                        / (metadata["maximum"] - metadata["minimum"]),
+                        "type": "quantitative",
+                        "scale": theta_scale,
+                    }
+                },
+            }

Review Comment:
   Confirmed and fixed in 14c1d525d98dbad8df6f2de98685a5c93f5f9610. Each 
interval arc now has adjacent theta/theta2 endpoints with stacking disabled, 
and clipping keeps bands within the visible automatic range. Regressions assert 
all band endpoints for three angle configurations and clipped automatic bounds. 
Nine product-generated Vega-Lite 5 specs also compiled/rendered with finite, 
adjacent scenegraph endpoints. Validation: 1,558 tests and branch-wide 
pre-commit passed.



##########
superset/mcp_service/chart/chart_utils.py:
##########
@@ -599,6 +599,145 @@ def merge_interactive_pivot_ui_config(
         new_form_data["pivot_table_state"] = {**existing_state, **new_state}
 
 
+_GAUGE_FORM_DATA_FIELD_MAP: dict[str, str] = {
+    "groupby": "groupby",
+    "sort_by_metric": "sort_by_metric",
+    "row_limit": "row_limit",
+    "min_val": "min_val",
+    "max_val": "max_val",
+    "color_scheme": "color_scheme",
+    "font_size": "font_size",
+    "number_format": "number_format",
+    "currency_format": "currency_format",
+    "value_formatter": "value_formatter",
+    "start_angle": "start_angle",
+    "end_angle": "end_angle",
+    "show_pointer": "show_pointer",
+    "animation": "animation",
+    "show_axis_tick": "show_axis_tick",
+    "show_split_line": "show_split_line",
+    "split_number": "split_number",
+    "show_progress": "show_progress",
+    "overlap": "overlap",
+    "round_cap": "round_cap",
+    "intervals": "intervals",
+    "interval_color_indices": "interval_color_indices",
+    "time_range": "time_range",
+    "granularity_sqla": "granularity_sqla",
+}
+
+_GAUGE_PRESENTATION_FORM_DATA_KEYS = frozenset(
+    {
+        "min_val",
+        "max_val",
+        "color_scheme",
+        "font_size",
+        "number_format",
+        "currency_format",
+        "value_formatter",
+        "start_angle",
+        "end_angle",
+        "show_pointer",
+        "animation",
+        "show_axis_tick",
+        "show_split_line",
+        "split_number",
+        "show_progress",
+        "overlap",
+        "round_cap",
+        "intervals",
+        "interval_color_indices",
+    }
+)
+
+
+def _without_generated_gauge_time_filter(
+    form_data: dict[str, Any],
+) -> list[Any]:
+    """Return cached filters without the mapper-owned temporal binding."""
+    generated_subject = form_data.get(MCP_DASHBOARD_TIME_FILTER_SUBJECT)
+    return [
+        filter_
+        for filter_ in form_data.get("adhoc_filters", [])
+        if not (
+            generated_subject
+            and isinstance(filter_, dict)
+            and filter_.get("operator") == FilterOperator.TEMPORAL_RANGE.value
+            and filter_.get("subject") == generated_subject

Review Comment:
   Confirmed and fixed in 14c1d525d98dbad8df6f2de98685a5c93f5f9610. Cleanup 
requires the recorded subject plus the neutral No filter comparator and 
SIMPLE/WHERE shape; a user-authored or edited temporal range on that subject 
survives. Regressions exercise saved update, saved-chart preview, cached 
update-preview, and clear/rebind merges, including stale provenance after 
comparator edits. Validation: 1,558 chart/Explore/common tests and branch-wide 
pre-commit passed.



##########
superset/mcp_service/chart/schemas.py:
##########
@@ -1093,10 +1097,168 @@ class GaugeChartConfig(BaseChartConfig):
         ),
         max_length=100,
     )
+    font_size: int = Field(15, description="Gauge text size", ge=10, le=20)
+    number_format: str = Field(
+        "SMART_NUMBER", description="D3 number format", max_length=50
+    )
+    currency_format: CurrencyFormat | None = Field(
+        None, description="Currency symbol applied to the gauge value"
+    )
+    value_formatter: str = Field(
+        "{value}",
+        description="Value template; {value} is replaced with the formatted 
metric",
+        max_length=200,
+    )
+    start_angle: float = Field(225, description="Gauge start angle in degrees")
+    end_angle: float = Field(-45, description="Gauge end angle in degrees")
+    show_pointer: bool = Field(True, description="Show the gauge pointer")
+    animation: bool = Field(True, description="Animate gauge value changes")
+    show_axis_tick: bool = Field(False, description="Show minor axis ticks")
+    show_split_line: bool = Field(False, description="Show axis split lines")
+    split_number: int = Field(10, description="Number of axis segments", ge=3, 
le=30)
+    show_progress: bool = Field(True, description="Show the progress arc")
+    overlap: bool = Field(
+        True, description="Overlap progress arcs when multiple groups are 
present"
+    )
+    round_cap: bool = Field(False, description="Use rounded progress-arc caps")
+    intervals: str = Field(
+        "",
+        description="Comma-separated interval upper bounds",
+        max_length=1000,
+    )
+    interval_color_indices: str = Field(
+        "",
+        description="Comma-separated 1-based color indices for intervals",
+        max_length=1000,
+    )
+    time_range: str | None = Field(
+        None,
+        description="Optional Superset time range applied to the gauge query",
+        max_length=1000,
+    )
+    granularity_sqla: str | None = Field(
+        None,
+        description="Temporal column associated with time_range in native 
form_data",
+        min_length=1,
+        max_length=255,
+    )
+
+    @model_validator(mode="before")
+    @classmethod
+    def adapt_native_form_data(cls, data: Any) -> Any:  # noqa: C901
+        """Accept the Gauge plugin's native form_data without weakening 
typing."""
+        if not isinstance(data, dict):
+            return data
+        data = dict(data)
+
+        # ``gauge`` is the public MCP discriminator; ``gauge_chart`` remains
+        # the native frontend viz_type and is accepted only as an input alias.
+        if data.get("chart_type") == "gauge_chart" or (
+            "chart_type" not in data and data.get("viz_type") == "gauge_chart"
+        ):
+            data["chart_type"] = "gauge"
+        data.pop("viz_type", None)
+
+        # These identify the Explore/chart envelope, not Gauge controls.
+        for key in (
+            "datasource",
+            "datasource_id",
+            "datasource_name",
+            "datasource_type",
+            "form_data_key",
+            "slice_id",
+            "slice_name",
+            "url",
+        ):
+            data.pop(key, None)
+        data.pop("_mcp_dashboard_time_filter_subject", None)
+
+        metric = data.get("metric")
+        if isinstance(metric, str):
+            data["metric"] = {"name": metric, "saved_metric": True}
+        elif isinstance(metric, dict) and metric.get("expressionType") in {
+            "SIMPLE",
+            "SQL",
+        }:
+            expression_type = metric.get("expressionType")
+            if expression_type == "SQL":
+                data["metric"] = {
+                    "sql_expression": metric.get("sqlExpression"),
+                    "label": metric.get("label"),
+                }
+            else:
+                column = metric.get("column")
+                column_name = (
+                    column.get("column_name") or column.get("columnName")
+                    if isinstance(column, dict)
+                    else None
+                )
+                data["metric"] = {
+                    "name": column_name,
+                    "aggregate": metric.get("aggregate"),
+                    "label": metric.get("label"),
+                }
+
+        groupby = data.get("groupby")
+        if isinstance(groupby, str):
+            groupby = [groupby]
+        if isinstance(groupby, list):
+            data["groupby"] = [
+                {"name": value} if isinstance(value, str) else value
+                for value in groupby
+            ]
+
+        # Native SIMPLE filters are losslessly representable by FilterConfig.
+        # SQL adhoc filters remain intentionally unsupported on the typed MCP
+        # surface. TEMPORAL_RANGE is represented by time_range/granularity.
+        if "adhoc_filters" in data:
+            if "filters" in data:
+                raise ValueError("Use either filters or adhoc_filters, not 
both")
+            native_filters = data.pop("adhoc_filters")
+            if not isinstance(native_filters, list):
+                raise ValueError("adhoc_filters must be a list")
+            filters: list[dict[str, Any]] = []
+            for index, filter_ in enumerate(native_filters):
+                if not isinstance(filter_, dict):
+                    raise ValueError(f"adhoc_filters[{index}] must be an 
object")
+                if filter_.get("expressionType") not in (None, "SIMPLE"):
+                    raise ValueError(
+                        f"adhoc_filters[{index}] must use 
expressionType='SIMPLE'"
+                    )
+                if str(filter_.get("clause", "WHERE")).upper() != "WHERE":
+                    raise ValueError(f"adhoc_filters[{index}] must use 
clause='WHERE'")
+                operator = filter_.get("operator") or filter_.get("op")
+                subject = filter_.get("subject") or filter_.get("col")
+                comparator = filter_.get("comparator", filter_.get("val"))
+                if operator == "TEMPORAL_RANGE":
+                    if not isinstance(subject, str) or not subject:
+                        raise ValueError(
+                            f"adhoc_filters[{index}] has no temporal subject"
+                        )
+                    data.setdefault("granularity_sqla", subject)
+                    data.setdefault("time_range", comparator)
+                    continue

Review Comment:
   Confirmed and fixed in 14c1d525d98dbad8df6f2de98685a5c93f5f9610. Native 
TEMPORAL_RANGE filters require a nonblank string comparator and validate it 
before normalization, even if a top-level time_range is present. Schema 
regressions cover null/empty/whitespace/non-string values; real FastMCP client 
calls verify missing/null/empty comparators reject before compilation. 
Validation: 1,558 chart/Explore/common tests and branch-wide pre-commit passed.



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