Skip to content

fix(mcp): complete Gauge chart support follow-up - #43943

Merged
aminghadersohi merged 7 commits into
masterfrom
sc-119163-mcp-gauge-gap-fixes
Sep 9, 2026
Merged

aminghadersohi merged 7 commits into
masterfrom
sc-119163-mcp-gauge-gap-fixes

Conversation

@aminghadersohi

@aminghadersohi aminghadersohi commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

SUMMARY

Completes Gauge MCP support introduced by #43568, retaining the public gauge identity and native gauge_chart compatibility, complete controls, shared query construction, and saved/unsaved update and preview paths.

The latest follow-up also addresses the optional XLSX note in approving review 5146726270: openpyxl exports preserve non-finite floats as nan/inf/-inf text, distinct from blank NULLs and consistent with CSV. Master e6d088d was merged normally to resolve the shared rejected-filter helper conflict while preserving both query-error and rejected-filter checks.

This finite follow-up addresses review 5140601116:

  • raw get_chart_data preserves every source group, including NULL/NaN/non-finite aggregates, without Gauge-specific filtering, count rewriting, or all-invalid errors
  • saved and form-data-key JSON/CSV/XLSX inspection follows the ordinary chart-data contract; unsaved exports honor the requested format
  • only preview/compile renderability paths skip invalid dials and reject nonempty all-invalid output; finite dials, configured compile limits, and structured query errors remain covered
  • normalize empty/whitespace temporal sentinels on both operands before conflict checks, retaining strict missing/non-string comparator validation
  • enforce facet-center geometry in default Python CI, in addition to actual Vega scenegraph rendering
  • pin automatic all-zero bounds to 0–1 and derive needle length from the midpoint of the dial band

Earlier fixes remain: datasource overrides, dataset-rebind filter isolation, metric-label resolution, Gauge dispatch, ambiguity errors, config-less same-dataset updates, duplicate neutral-binding self-healing, and dead-helper removal.

Original merge: #43568
Completeness request: #43568 (comment)
Previous 22-suggestion dispositions: #43943 (comment)
Previous human-review dispositions: #43943 (comment)

Coordination: One finite pass over all live reviews, issue comments, and inline threads. This correction supersedes the earlier get-data normalization disposition: raw inspection is not a renderability check. All five preceding items and the optional item in the approving review are addressed; four earlier resolved inline threads remain covered. The new review is body-only, with no new inline thread to resolve. Ready for review; no additional review request, draft conversion, history rewrite, or PR merge.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

No live UI screenshot: the local application health endpoint is unavailable.

  • Raw inspection previously silently removed invalid Gauge groups or rejected all-invalid data; it returns all source groups and preserves the query's count/completeness semantics.
  • Preview/compile still keeps finite dials and rejects nonempty all-invalid results.
  • Each faceted needle starts at its own arc center and reaches the colored band midpoint (68.5px for the 55–82px band).

TESTING INSTRUCTIONS

Validation for head dfeb3922517376e51e7ccacecfefab6965beda48:

  • Six XLSX fail-before cases cover saved, saved-with-form-data-key, and unsaved public FastMCP paths with mixed/all-non-finite groups. Loading the exported workbooks asserts NULL remains blank, NaN/Infinity remain distinguishable text, and finite values remain numeric. All six pass after the fix.

  • Earlier fail-before evidence against f17d7f9e81: 19 raw-data/export cases and 19 sentinel/needle cases failed before the fixes. Reverting facet centers to outer signals separately makes the default-CI geometry test fail without NODE_PATH.

  • 1,816 affected Python tests passed, covering chart/explore/common MCP paths, query-context JSON/CSV/XLSX serialization, and SQLAlchemy label resolution:
    NODE_PATH=/path/to/vega-node-modules PYTHONPATH=$PWD:$PWD/superset-core/src pytest -q tests/unit_tests/mcp_service/chart tests/unit_tests/mcp_service/explore tests/unit_tests/mcp_service/common tests/unit_tests/common/test_query_context_processor.py tests/unit_tests/connectors/sqla/models_test.py

  • Public FastMCP regressions cover saved, saved-with-form-data-key, and unsaved requests; mixed and all-non-finite groups; JSON/CSV/XLSX rows; source rowcount distinct from returned row_count; completeness; and both XLSX engines. Preview and real generate/update compile regressions remain in the broad suite.

  • 21 frontend Gauge tests passed: buildQuery and transformProps.

  • Actual Vega-Lite 5/Vega 5 compilation, SVG rendering, and scenegraph assertions passed across 12 product-generated specs, checking per-cell centers, endpoints, band-derived length, angles, and visibility. The optional renderer requires vega@5/vega-lite@5 through NODE_PATH; facet-center and exact 0–1 range assertions also run in default Python CI without those dependencies.

  • Full branch-applicable and staged pre-commit passed, including mypy, Ruff/format, Pylint, and repository checks. Compileall, optimized-mode renderability checks, and diff whitespace checks passed.

  • All 13 required checks passed on this exact head, including unit tests, PostgreSQL integration, Playwright, pre-commit.

  • The exact-head dependency check passed. No checks, coverage thresholds, or timeouts were weakened.

Manual verification:

  1. Export XLSX and verify NULL is blank while NaN/Infinity are nan/inf/-inf text. Query saved and unsaved grouped Gauges containing finite, NULL, NaN, and infinite aggregates in JSON/CSV/XLSX: all groups remain. All-invalid raw data is not a renderability error.
  2. Preview/compile the same data: finite dials survive; nonempty all-invalid data errors.
  3. Preview grouped dials with custom angles and toggle pointers: each visible needle starts at its own dial center and reaches its band.
  4. Combine blank/whitespace or No filter temporal sentinels with a real native range: no false conflict. Actual conflicting restrictions still reject.
  5. Preview all-zero data with omitted bounds: range is exactly 0–1; explicit degenerate bounds still reject.

No live UI smoke test is claimed.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

@github-actions github-actions Bot added the doc Namespace | Anything related to documentation label Sep 6, 2026
@netlify

netlify Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit dfeb392
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6aa08d513da07a000890ac82
😎 Deploy Preview https://deploy-preview-43943--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov

codecov Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.60485% with 164 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.90%. Comparing base (e6d088d) to head (dfeb392).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/chart/preview_utils.py 74.37% 32 Missing and 19 partials ⚠️
superset/mcp_service/chart/query_result.py 66.38% 23 Missing and 17 partials ⚠️
superset/mcp_service/chart/schemas.py 72.51% 20 Missing and 16 partials ⚠️
superset/mcp_service/chart/plugins/gauge.py 56.52% 5 Missing and 5 partials ⚠️
...perset/mcp_service/chart/tool/get_chart_preview.py 72.72% 4 Missing and 5 partials ⚠️
.../mcp_service/chart/validation/dataset_validator.py 88.88% 6 Missing and 1 partial ⚠️
superset/mcp_service/chart/tool/get_chart_data.py 73.91% 4 Missing and 2 partials ⚠️
superset/mcp_service/chart/chart_utils.py 96.66% 1 Missing and 1 partial ⚠️
...set/mcp_service/chart/tool/update_chart_preview.py 75.00% 2 Missing ⚠️
...t/mcp_service/chart/validation/schema_validator.py 75.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #43943      +/-   ##
==========================================
+ Coverage   79.85%   79.90%   +0.04%     
==========================================
  Files        2916     2916              
  Lines      170703   171199     +496     
  Branches    39553    39672     +119     
==========================================
+ Hits       136319   136797     +478     
+ Misses      31867    31819      -48     
- Partials     2517     2583      +66     
Flag Coverage Δ
hive 37.41% <10.55%> (-0.19%) ⬇️
mysql 57.04% <10.55%> (-0.33%) ⬇️
postgres 57.06% <10.55%> (-0.33%) ⬇️
presto 39.27% <10.55%> (-0.20%) ⬇️
python 84.36% <76.60%> (+0.09%) ⬆️
sqlite 56.76% <10.55%> (-0.33%) ⬇️
unit 75.48% <76.60%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aminghadersohi
aminghadersohi marked this pull request as ready for review September 6, 2026 18:25
Comment thread superset/mcp_service/chart/preview_utils.py
Comment thread superset/mcp_service/chart/preview_utils.py Outdated
Comment thread superset/mcp_service/chart/chart_utils.py
Comment thread superset/mcp_service/chart/schemas.py Outdated
@bito-code-review

bito-code-review Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #2e1438

Actionable Suggestions - 0
Additional Suggestions - 22
  • superset/mcp_service/chart/preview_utils.py - 5
    • Datasource override ignored · Line 88-89
      `resolve_form_data_datasource` reads `form_data.get("datasource_id")` first and only falls back to the combined `datasource` string when `datasource_id` is absent. Setting only `query_form_data["datasource"]` won't override a pre-existing `datasource_id` (e.g. from stored chart params in `update_chart_preview.py`). The old `factory.create(datasource={"id": dataset_id, ...})` forced the id. Set `datasource_id`/`datasource_type` too.
    • All-zero gauge range rejected · Line 565-576
      When all metric values are 0 and `min_val`/`max_val` are unset, the auto-range yields `minimum == maximum == 0`, so the `minimum >= maximum` guard returns `InvalidGaugeRange`. The frontend `transformProps` (`calculateMin`/`calculateMax`) renders this case as a valid dial, so the preview diverges and rejects valid all-zero data. Widen the auto-computed range (e.g. `maximum = minimum + 1`) only when both bounds were auto-derived.
    • Pointer rule mark not rendered · Line 824-836
      `pointer_layer` uses a `rule` mark with `theta`/`radius`/`radius2` encodings, but Vega-Lite `rule` marks only support `x`/`y`/`x2`/`y2`/`latitude`/`longitude`. The rule compiler returns `{}` when none of those are present, so the pointer renders nothing. Since `show_pointer` defaults to `True`, the needle is silently missing. Use a polar-capable mark or compute x/y from the angle.
    • Function complexity exceeds limits · Line 504-504
      `_prepare_gauge_preview` has too many return statements, branches, and statements (PLR0911, PLR0912, PLR0915). Consider refactoring into smaller functions. Similar complexity issues exist in `generate_gauge_vega_lite_preview` (line 686) and `_generate_vega_lite_preview_from_data` (line 893).
    • Use of assert for validation · Line 511-511
      `assert` statements are used for validation, which can be disabled in optimized mode. Replace with explicit error handling. This occurs at lines 511 and 513.
  • superset/mcp_service/chart/chart_utils.py - 1
    • Rebind leaks old filters · Line 714-720
      On dataset rebind, `merged` is built from presentation keys only (dropping old filters per the docstring), yet this `temporal_explicit` branch prepends `_without_generated_gauge_time_filter(existing_form_data)`, leaking old-dataset user filters into the new chart. Guard with `if not dataset_rebind:` so rebind keeps only the freshly generated temporal binding from `new_form_data`.
  • superset/mcp_service/chart/tool/get_chart_data.py - 1
    • Gauge metric label mismatch · Line 769-769
      `validate_gauge_query_result(result, form_data)` compares `metric_result_label` (query_result.py:97) against result columns. For saved (non-adhoc) metrics, `form_data` holds the raw metric key, but result columns are labeled via `get_metric_name`/`verbose_map` (utils/core.py:1394). If a gauge metric has a verbose name, the label won't match and a valid chart may be rejected. Consider resolving labels before comparison.
  • superset/mcp_service/chart/tool/get_chart_preview.py - 1
    • Gauge validation gate mismatch · Line 456-456
      `validate_gauge_query_result` gates on `form_data.get("viz_type")` (query_result.py:129), but the dispatch at line 467 uses `self.chart.viz_type` first. When `chart.viz_type == "gauge_chart"` while `form_data` lacks/differs on `viz_type`, validation is skipped yet the gauge path runs; inside `_prepare_gauge_preview` the re-validation is likewise skipped and `assert metric_label is not None` (preview_utils.py:513) can raise an `AssertionError` not caught by the `except` at lines 484-491. Resolve `viz_type` once and gate both on it.
  • superset/mcp_service/chart/query_result.py - 2
    • SQL metric label mismatch · Line 118-121
      For `expressionType == "SQL"`, `metric_result_label` returns the raw `sqlExpression`, but the actual query-result key is produced by `get_metric_name` → `make_label_compatible` (superset/db_engine_specs/base.py:2431), which truncates labels exceeding `max_column_name_length` (e.g. Oracle/MSSQL). A long expression would then fail the `metric_label not in row` check at line 171 with a false "missing metric output" error. Consider resolving the key through the same label pipeline.
    • Magic length thresholds · Line 101-121
      The hardcoded caps `0 < len(metric) <= 20` (line 101) and `len(sql_expression) <= 2000` (line 120) cause `metric_result_label` to return `None` for valid metrics that exceed them, which `validate_gauge_query_result` then reports as "no resolvable metric result label" even though the query succeeded. These thresholds are undocumented magic numbers with behavioral impact.
  • superset/mcp_service/chart/schemas.py - 5
    • Silent temporal-filter drop · Line 1243-1244
      `data.setdefault("granularity_sqla", subject)` / `setdefault("time_range", comparator)` silently drop any second TEMPORAL_RANGE filter, so a native form_data with two temporal filters loses one without error, producing wrong query results. Consider raising a ValueError when a temporal range is already set.
    • Lossless-filter claim mismatch · Line 1211-1213
      The comment claims native SIMPLE filters are "losslessly representable by FilterConfig", but `FilterConfig.op` (lines 880-891) omits native operators `NOT ILIKE`, `IS TRUE`, `IS FALSE`, `CONTAINS_ANY/ALL`, `IS_EMPTY`, `IS_NOT_EMPTY`, `LENGTH_*` (see `FilterOperator` in `superset/utils/core.py`). Such filters pass through line 1248 and then fail Pydantic Literal validation with a confusing error. Consider rejecting them here with a clear message.
    • Assertion guard fragile · Line 1275-1275
      `assert col.name is not None` is fragile here. Pydantic v2 converts it to a generic "Assertion failed" ValidationError, and under `python -O` the assert is stripped entirely, so a nameless `groupby` entry reaches `col.name.casefold()` and raises `AttributeError` instead of a clean validation error. Use an explicit `if col.name is None: raise ValueError(...)` like the sibling validators.
    • Too many branches in adapt_native_form_data · Line 1148-1148
      `adapt_native_form_data` has 20 branches (limit 12) per PLR0912. Consider extracting helper functions for native filter conversion and metric normalization to reduce branching complexity.
    • Dynamically typed expressions disallowed · Line 1148-1148
      `data: Any` parameter and `-> Any` return type in `adapt_native_form_data` trigger ANN401. Use a more specific type such as `dict[str, Any]` for the parameter and return type.
  • superset/mcp_service/chart/validation/dataset_validator.py - 3
    • Uncaught ambiguous ref error · Line 486-493
      `_column_exists` now calls `resolve_dataset_reference`, which raises `AmbiguousDatasetReferenceError` on case-colliding names. Its only caller `_adhoc_filter_column_valid` (compile.py:226) doesn't catch it, so an ambiguous HAVING filter column propagates a raw `ValueError` out of `validate_and_compile` and is swallowed by the generic top-level `except ValueError` — losing the actionable message that `_validate_saved_metric_refs` produces for the same condition. Handle the ambiguity here for consistent error quality.
    • Uncaught ambiguity exception · Line 780-784
      `resolve_dataset_reference` raises `AmbiguousDatasetReferenceError` when column names differ only by case (line 77), but this new call in `_validate_aggregations` doesn't catch it, unlike the sibling `_validate_columns_exist` which wraps the same call in try/except. On the normal `validate_against_dataset` path the ambiguity is caught earlier, but `_validate_aggregations` is a public static method callable directly (see `test_dataset_validator.py`), where the exception would propagate uncaught. Consider handling it defensively for consistency.
    • First ambiguous ref only · Line 350-353
      `ambiguous_references` collects every ambiguous ref, but only `ambiguous_references[0]` is returned. If a chart references several case-ambiguous columns, the user must fix and re-run one at a time. Consider aggregating all matches into the error message (e.g., extend `_build_ambiguous_reference_error` to accept the full list), or track only the first to avoid building a list that is never fully consumed.
  • superset/mcp_service/chart/tool/get_chart_type_schema.py - 1
    • Duplicated alias mapping · Line 225-226
      The `gauge_chart` → `gauge` alias is now hardcoded here, duplicating the same mapping already in `schemas.py:2620` (`_VIZ_TYPE_TO_CHART_TYPE`) and `get_chart_data.py:160`. If the alias ever changes, all three must be updated in lockstep. Consider centralizing this alias lookup to avoid divergence.
  • superset/mcp_service/chart/plugins/gauge.py - 1
    • Private API coupling · Line 118-118
      `post_map_validate` calls the private `DatasetValidator._get_dataset_context`. Coupling a plugin to a private method makes the validation fragile if the validator's internals change. Consider exposing a public accessor or documenting the dependency.
  • tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py - 2
    • Unused module import · Line 26-26
      `patch` is imported at module scope but never used there — all usages (e.g. lines 1189, 1432, 2372) are local re-imports inside functions. Remove it from the module-level import to avoid dead code and keep the import list accurate.
    • Unused module import · Line 29-29
      `Client` is imported at module scope but never used there — all usages (e.g. lines 1434, 1893, 2374) are local re-imports inside functions. Remove the module-level import to avoid dead code.
Review Details
  • Files reviewed - 28 · Commit Range: 3d76aa8..14c1d52
    • docs/admin_docs/configuration/mcp-server.mdx
    • superset/mcp_service/app.py
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/compile.py
    • superset/mcp_service/chart/plugins/gauge.py
    • superset/mcp_service/chart/preview_utils.py
    • superset/mcp_service/chart/query_result.py
    • superset/mcp_service/chart/resources/chart_configs.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/generate_chart.py
    • superset/mcp_service/chart/tool/get_chart_data.py
    • superset/mcp_service/chart/tool/get_chart_preview.py
    • superset/mcp_service/chart/tool/get_chart_type_schema.py
    • superset/mcp_service/chart/tool/update_chart.py
    • superset/mcp_service/chart/tool/update_chart_preview.py
    • superset/mcp_service/chart/validation/dataset_validator.py
    • superset/mcp_service/chart/validation/schema_validator.py
    • superset/mcp_service/explore/tool/generate_explore_link.py
    • tests/unit_tests/mcp_service/chart/test_compile.py
    • tests/unit_tests/mcp_service/chart/test_gauge_chart.py
    • tests/unit_tests/mcp_service/chart/test_preview_utils.py
    • tests/unit_tests/mcp_service/chart/tool/test_generate_chart.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_type_schema.py
    • tests/unit_tests/mcp_service/chart/tool/test_update_chart.py
    • tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py
    • tests/unit_tests/mcp_service/explore/tool/test_generate_explore_link.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@aminghadersohi

Copy link
Copy Markdown
Contributor Author

Finite maintainer pass on the 22 additional suggestions in Bito's comment, starting from 14c1d52. Read all 6 reviews, 4 resolved threads (8 inline comments), and 3 issue comments. No new review is requested.

# Suggestion Disposition
1 Datasource override ignored Fixed: override combined and separate datasource id/type together, without mutating stored params; regression checks the shared resolver.
2 All-zero auto-range Fixed: widen only a fully automatic degenerate range to 0–1; explicit invalid bounds still reject.
3 Pointer rule not rendered Fixed: Cartesian rule endpoints derived from the dial angle. Also give faceted units valid dimensions. Rendered 37 product-generated specs and checked finite 52px needles, visibility, angles, and facets.
4 Preview function complexity Style-only: existing complexity exemptions are intentional; configured branch-wide static checks pass. No behavior defect identified.
5 Preview validation assertions Fixed: direct Gauge renderers always validate as Gauge and return structured errors, including under Python optimization.
6 Dataset rebind leaks filters Fixed: old-dataset filters are never prepended on rebind; same-dataset temporal changes still preserve user ranges. Saved-update and preview payload regressions cover explicit clear/new binding.
7 Saved metric verbose label mismatch Stale for this path: QueryContextProcessor.get_data applies verbose names only to table-like exports, not JSON. Added real JSON serialization/validation regression. Mapping to a verbose name here would break valid results.
8 Gauge dispatch/validation mismatch Fixed: saved chart identity is resolved before query construction and reused for ASCII/table/Vega validation and dispatch; public saved get-data also validates with that identity. Missing/stale params discriminators are covered.
9 Engine-truncated SQL result label Stale: make_sqla_column_compatible preserves the expected key; query restores labels_expected on returned DataFrames. Added query-path regression with a truncated driver alias and long SQL metric label.
10 Arbitrary label length caps Fixed: remove the unrelated 20-key/2,000-character caps; retain shape checks. Long expressions and metadata-rich metrics are covered.
11 Silent temporal-filter drop Fixed: reject distinct/conflicting native temporal restrictions, including conflicts with top-level controls, instead of silently discarding one. Public FastMCP rejection happens before compilation.
12 Lossless-filter claim/operators Fixed: narrow the claim and reject unsupported native operators with an indexed, explicit error derived from FilterConfig's supported operators.
13 Groupby assertion guard Hardened with explicit ValueError; normal ColumnRef validation already rejects nameless dimensions.
14 Native adapter branch count Style-only: no incorrect behavior follows from the branch count; configured checks pass.
15 Any in before-validator Rejected: a Pydantic before-validator must accept arbitrary raw input; the existing non-dict passthrough is deliberate. A dict-only annotation would misstate its contract.
16 Preserved HAVING ambiguity escapes Fixed: return the structured ambiguity error; apply the same exact-first/unique-case-insensitive contract to preserved WHERE filters. Compiler regressions cover both clauses and exact matches.
17 Aggregation ambiguity escapes Fixed: append structured errors to the aggregation validator's existing error-list return contract; direct-call regression added.
18 Only first ambiguous reference reported Optional UX/style: the tier-1 API deliberately returns one actionable error, consistently with sibling validators. No acceptance or ambiguity-resolution inconsistency.
19 Duplicate Gauge alias mapping Style-only: all three mappings agree; existing discovery/dispatch regressions pass. No alias behavior defect.
20 Private dataset-context accessor Style-only: this internal plugin uses the existing validator helper with its supported signature and handles unavailable context. No incorrect behavior demonstrated.
21 Unused module-level patch import Stale: the public saved-Gauge FastMCP test uses the module-level import, without a local re-import.
22 Unused module-level Client import Stale: the same public FastMCP test uses the module-level Client.

The four already-resolved inline fixes are retained and covered by the broad regression run. Validation and exact-head CI are recorded in the PR body.

@rebenitez1802 rebenitez1802 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two correctness findings worth addressing on the Gauge follow-up. Flagging the mediums here; I have a separate batch of low/nit items (native "No filter" sentinel treated as a temporal conflict, the config-less rebind guard, a self-healing duplicate NO_TIME_RANGE filter, dead _preserve_previous_adhoc_filters, and a couple of test-assertion gaps) I can share if useful.

🟡 A single NULL/NaN dial rejects the entire Gauge result — superset/mcp_service/chart/query_result.py:180,189

validate_gauge_query_result returns on the first row whose metric value isn't finite, so one empty-aggregate group discards all dials of a multi-dial gauge — the "up to 10 dials" feature this series adds. A SQL NULL aggregate reaches the validator as None/NaN (QueryContextProcessor.get_data returns df.to_dict(orient="records") with NaN verbatim on the JSON path, no verbose_map), and the ECharts gauge (transformProps.ts maps data_point[metricLabel] with no null filtering) renders such rows fine — so MCP is strictly stricter than the viz it fronts. It gates every path (get_chart_data both query paths, all three get_chart_preview strategies, generate_preview_from_form_data, and the generate/update_chart compile check), so it also regresses reads of already-saved gauges whose current data has a null group. Fix: skip non-finite dial values (still render the valid dials) and only return an error when no finite value exists, matching the frontend.

🟡 Grouped (faceted) Gauge preview draws the needle at the cell corner — superset/mcp_service/chart/preview_utils.py:845

The pointer rule encodes x/y as value-exprs "width / 2" / "height / 2". The single-dial path is correct, but when groupby wraps the spec in facet/spec, the facet child scope has no width/height signals (only child_width/child_height), so both resolve to 0 — the needle is drawn from (0, 0) while the arc auto-centers at (child_width/2, child_height/2), detaching the pointer from the dial in every grouped preview. Reproduced by compiling the generated spec through vega-lite@5 + vega@5 (scenegraph: rule at x=0,y=0; arc centered at 100,150). Preview-only — the saved chart, its form_data, and query results are unaffected. Fix: reference child_width/child_height in the pointer value-exprs, or move the pointer into the unit spec so width/height resolve to the cell.

@aminghadersohi

Copy link
Copy Markdown
Contributor Author

Addressed this review in f17d7f9e81e5638946a7c9dbb74680d4011e54d5.

  • Mixed NULL/NaN/non-finite dials: query normalization preserves finite groups and skips invalid values without mutating cached results. Nonempty all-invalid output still errors; malformed envelopes/aliases and query failures remain errors. Genuinely empty results retain the existing no-data behavior. Applied to both get-data query paths, all preview formats, and generate/update compile checks. Gauge compilation samples its configured dial limit (up to 10), so the first two empty groups cannot hide a later valid dial.
  • Faceted pointer: endpoint origins use the same child dimensions as the facet unit. The prior regression checked length but missed origin; the new actual Vega-Lite 5/Vega 5 scenegraph test compares each needle with its own arc center, and verifies visibility, length, angles, and multiple cells across 12 specs.
  • Native "No filter": treated as a neutral dashboard binding, not a conflicting restriction. Real conflicting ranges still reject.
  • Config-less rebind guard: restating the same dataset is accepted; a different Gauge dataset still requires a complete config. Public FastMCP tests cover both.
  • Duplicate NO_TIME_RANGE: generated neutral bindings self-heal on updates, including omitted temporal controls; user-authored ranges survive.
  • Dead helper: removed _preserve_previous_adhoc_filters and its unreachable helper-only tests. Coverage targets the shared merge and actual cached-update path instead.
  • Assertion gaps: added returned-data/row-count assertions, saved-versus-preview role assertions, and real generate/update compile regressions rather than mocking successful compilation.

The offered low/nit batch was not posted separately; all named areas above were inspected and addressed directly. No additional review is requested. The four existing resolved threads remain resolved; this review has no new inline thread to resolve.

Evidence: 20 fail-before cases against starting head 2e014b6; final 1,772 affected Python tests, 21 frontend Gauge tests, actual rendered-spec checks, branch-wide/staged pre-commit (including mypy), compileall, and optimized-mode validation passed. All 13 required GitHub checks passed on this exact head, including unit tests, PostgreSQL integration, Playwright, and pre-commit.

@bito-code-review

Copy link
Copy Markdown
Contributor

AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

@rebenitez1802 rebenitez1802 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed f17d7f9e81 — the two rendering issues from the last pass are genuinely fixed (faceted needle centering and finite-dial preservation), and I found no regressions. Requesting changes on one correctness item; the rest are non-blocking follow-ups.

🔴 Blocking — get_chart_data silently drops null gauge groups and misreports counts — superset/mcp_service/chart/tool/get_chart_data.py:772,1140
normalize_gauge_query_result is the right behavior for the preview/compile renderability checks, but it's also wired into get_chart_data, a raw data-inspection tool. For a saved gauge with groupby where one group's aggregate is NULL/NaN, get_chart_data now returns fewer rows than the query produced and rewrites rowcount / row_count / data_completeness to match, so the dropped group vanishes with no signal; if all groups are non-finite it returns a hard NonFiniteGaugeMetric error. Meanwhile the ECharts gauge (Gauge/transformProps.ts maps data_point[metricLabel] with no null filtering) still renders a dial per group, and every non-gauge chart type returns all rows — so the agent's view of the data disagrees with the chart it's inspecting, which for an agent-facing tool is a silent-correctness failure. Fix: restrict the drop/hard-error normalization to the preview/compile paths; in get_chart_data, return all rows (at most annotate non-finite groups) so the data tool stays faithful to the query and consistent with other chart types.

The following are non-blocking — fine as follow-ups:

🟢 Low — Fix for native "No filter" misses the empty/whitespace time_range sentinel — superset/mcp_service/chart/schemas.py:1254
adapt_native_form_data now treats None and NO_TIME_RANGE ("No filter") as neutral, but not "" / " " — which time_range_validation.py:155-162 documents as the same "no filter applied" sentinel. So time_range="" plus a real adhoc TEMPORAL_RANGE is still rejected as a bogus conflict. Fix: normalize empty/whitespace to unset alongside NO_TIME_RANGE on both operands before the conflict check.

🟢 Low — Facet-centering fix has no enforced test in default CI — tests/unit_tests/mcp_service/chart/test_gauge_render.py:31
The only assertion that the needle origin equals its per-cell arc center (rule.x==arc.x, rule.y==arc.y) is guarded by @pytest.mark.skipif(not os.environ.get("NODE_PATH")), and NODE_PATH is set nowhere in CI/conftest/pyproject — so it's skipped in every default run. The one non-skipped pointer test asserts only the encoding key set and sin/cos substrings, never the x/y origin exprs, so reverting the faceted center_x/center_y to width/2 would reintroduce the off-center needle with the suite still green. Fix: set NODE_PATH in the unit-test job, or add a non-skipped assertion that encoding['x']/['y'] resolve to the cell center for the faceted spec.

🟢 Low — test_gauge_all_zero_auto_range doesn't pin the 0–1 range — tests/unit_tests/mcp_service/chart/test_gauge_chart.py:879
It asserts only "no ChartError" (renderable) and that explicit degenerate bounds reject; it never checks the derived range is [0, 1]. A regression changing the omitted-bounds fallback to any other nondegenerate range (e.g. 0–100) would pass. The ascii preview already emits Range: 0 to 1, so it's trivially assertable.

🟢 Low — Needle length (52px) still stops short of the dial band (innerRadius 55px) — superset/mcp_service/chart/preview_utils.py:854
Unchanged: the pointer reaches radius 52 while the arc band starts at 55, so the tip stops ~3px inside the hollow center and never overlaps the colored dial (unlike the ECharts pointer). Cosmetic, preview-only. Fix: derive the needle length from the arc innerRadius/outerRadius.

@aminghadersohi

Copy link
Copy Markdown
Contributor Author

Addressed all five items in review 5140601116 in 307f84d5bc234f62acdfcc02b049f34bd6891097.

  1. Raw Gauge data: removed renderability normalization from both get-data query paths. Saved, saved-with-form-data-key, and unsaved FastMCP requests retain mixed/all-non-finite groups in JSON/CSV/XLSX. Regressions assert returned rows, source rowcount distinct from row_count, and ordinary completeness semantics. Unsaved exports honor their format; the XLSX fallback also preserves non-finite groups. Preview/compile-only filtering and all-invalid errors remain covered by real generate/update compilation tests. This supersedes the previous comment's get-data normalization disposition.
  2. Temporal sentinels: normalize empty/whitespace and No filter before comparing both operands. Schema and public FastMCP cases pass; missing/non-string comparators and genuinely conflicting restrictions still reject.
  3. Default-CI facet regression: non-skipped tests assert x/y origins equal child-unit centers. Deliberately restoring outer width/height signals fails that test without NODE_PATH. Actual Vega scenegraph checks also pass.
  4. All-zero bounds: pin ASCII Range: 0 to 1 and Vega metadata min/max to 0/1; explicit degenerate bounds still reject.
  5. Needle length: derive 68.5px from the 55–82px band midpoint; rendered endpoints must reach within that band.

No new items were rejected. Earlier 22-suggestion dispositions and human-review follow-ups remain applicable except the explicitly superseded raw-data behavior and needle length. All four earlier inline threads remain resolved; this review has no new inline thread to resolve.

Validation: 1,810 affected Python tests, 21 frontend Gauge tests, 12 rendered specs, full branch-applicable/staged pre-commit (including mypy), compileall, and optimized-mode renderability checks passed. Recorded fail-before cases cover raw-data/export and sentinel/needle fixes. All 13 required checks passed on this exact head, including unit tests, PostgreSQL integration, Playwright, pre-commit. The unit job initially exceeded its existing 30-minute limit after all 14,776 main-suite tests passed; one rerun completed successfully, including the remaining coverage batches. No tests, coverage thresholds, or timeouts were changed.

The prior non-required dependency-check failure occurred while setup-uv fetched Astral's version manifest, before dependency checks. One failed-job rerun succeeded; the new head's check-python-deps also passed. No code failure was masked.

PR testing/coordination sections are updated. No additional review is requested.

@rebenitez1802 rebenitez1802 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 307f84d5bc — the blocking get_chart_data raw-rows issue is resolved (renderability normalization removed from both data query paths, so raw null/NaN/inf groups are now preserved in JSON/CSV/XLSX, and row_count is correctly distinct from source rowcount), and I confirmed it did not regress the preview/compile filtering — the gauge renderability normalizer and all-invalid error are still enforced in compile.py, preview_utils.py, and get_chart_preview.py (all three strategies).

All four follow-ups from the last pass are addressed and now have non-skipped test coverage:

  • Native ""/whitespace/"No filter" temporal sentinels normalized on both operands; genuine conflicts and missing/non-string comparators still reject.
  • Faceted needle centering pinned by a default-CI test (x/y origin == cell center), not just the NODE_PATH-gated render test.
  • All-zero auto-range pins Range: 0 to 1 (ASCII + Vega usermeta); explicit degenerate bounds still reject.
  • Needle length derived from the dial band midpoint (68.5 of the 55–82 band) so the tip reaches the band.

Approving.

One optional, non-blocking follow-up: on the default openpyxl Excel path, non-finite metric cells serialize as blank, so a NULL group and a NaN/inf group are indistinguishable in .xlsx (CSV renders nan/inf and the xlsxwriter fallback renders #NUM!/#DIV/0!). Consider stringifying non-finite floats in _write_excel_data for cross-format consistency — largely an inherent xlsx limitation, fine to defer.

@aminghadersohi

Copy link
Copy Markdown
Contributor Author

Addressed the optional XLSX item in the approving review in dfeb3922517376e51e7ccacecfefab6965beda48.

  • The default openpyxl exporter writes non-finite floats as nan, inf, and -inf text, matching CSV. NULL remains blank and finite values remain numeric; no rows or raw query values are changed.
  • Six public FastMCP XLSX cases failed before the fix and pass afterward: saved, saved-with-form-data-key, and unsaved requests, each with mixed/all-non-finite groups. Tests load the actual exported workbook and assert cell values, not just row presence.
  • Resolved the master conflict with a normal merge (f8962a5), retaining the upstream shared rejected-filter helper and this PR's query-error checks. No rebase or force-push.
  • All prior review dispositions remain covered, including the preceding five-item pass. The four existing inline threads remain resolved; the approving review has no new inline thread to resolve.

Validation: 1,816 affected Python tests, 21 frontend Gauge tests, 12 rendered specs, full branch-applicable/staged pre-commit including mypy, compileall, and optimized-mode renderability checks passed. All 13 required checks passed on this exact head, including unit tests, PostgreSQL integration, Playwright, pre-commit.

PR body testing/coordination sections are updated. No additional review is requested.

@bito-code-review

bito-code-review Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #8b4b0a

Actionable Suggestions - 0
Additional Suggestions - 7
  • superset/mcp_service/chart/chart_utils.py - 1
    • Duplicated binding predicate · Line 749-756
      The `is_binding` predicate (749-756) duplicates the generated-binding signature already defined in `_without_generated_gauge_time_filter` (662-670). These two copies can diverge, causing the dedup to strip user filters or miss duplicates. Extract a shared predicate helper (e.g. `_is_generated_gauge_time_filter(filter_, subject)`) and use it in BOTH `_without_generated_gauge_time_filter` and this dedup loop so the signature is defined once.
  • tests/unit_tests/mcp_service/explore/tool/test_generate_explore_link.py - 1
    • Test asserts removed adhoc filter · Line 91-96
      For `comparator` in ("", " "), the `GaugeChartConfig` validator (schemas.py ~1245) runs `validate_time_range(comparator) or NO_TIME_RANGE`, then sets `data["temporal_column"] = subject` and `continue`s — the adhoc filter is consumed, not preserved. So `form_data["adhoc_filters"][0]` raises KeyError and the else-branch assertions fail. Assert `temporal_column`/`time_range` instead.
  • superset/mcp_service/chart/tool/get_chart_data.py - 1
    • Cross-path export divergence · Line 1319-1321
      This non-finite handling is only added to the openpyxl path `_write_excel_data`. The parallel fallback `_write_xlsxwriter_data` still relies on `nan_inf_to_errors=True`, which writes `#NUM!` error cells. The same chart data therefore yields different cell content depending on which library is installed. Consider mirroring this branch in `_write_xlsxwriter_data` for consistent output.
  • superset/mcp_service/chart/compile.py - 1
    • Negative row_limit edge case · Line 138-138
      For gauge charts, `int(form_data.get("row_limit") or 10)` only guards `None`/`0`; a negative `row_limit` (e.g. -5) is truthy and passes through, so `min(10, -5)` yields -5. That invalid limit is forwarded to `build_query_context_from_form_data`, which can make the compile check fail with a spurious database error. Clamp to a positive floor.
  • tests/unit_tests/mcp_service/chart/test_gauge_render.py - 1
    • Tautological test assertion · Line 89-89
      This assertion can never fail: `radius` is the arithmetic midpoint of `innerRadius` and `outerRadius`, so `radius > innerRadius && radius < outerRadius` is always true. It gives false confidence that the needle geometry is validated. Consider asserting `radius` equals the production `pointer_radius` (68.5) instead.
  • superset/mcp_service/chart/query_result.py - 1
    • Fragile first-error retention · Line 189-196
      `value_error = value_error or ChartError(...)` relies on `ChartError` (a pydantic `BaseModel`) being truthy to keep the first error. It works today, but if `MCPBaseError` ever gains `__bool__`/`__len__`, the first error would be silently dropped. An explicit `if value_error is None:` guard is clearer and robust.
  • tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py - 1
    • Missing type annotation · Line 139-139
      The new local variable `cached_filters` (line 139) lacks an explicit type annotation, which the repo's test-file typing standard requires for all local variables (BITO.md rules 13153/13158/13347/13598). Annotate it, e.g. `cached_filters: list[dict[str, Any]] = mock_get_previous_form_data.return_value["adhoc_filters"]`, and re-add `from typing import Any` (which was removed in this PR) — or use `list[dict]` to avoid needing the import.
Review Details
  • Files reviewed - 23 · Commit Range: 14c1d52..dfeb392
    • docs/admin_docs/configuration/mcp-server.mdx
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/compile.py
    • superset/mcp_service/chart/preview_utils.py
    • superset/mcp_service/chart/query_result.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/get_chart_data.py
    • superset/mcp_service/chart/tool/get_chart_preview.py
    • superset/mcp_service/chart/validation/dataset_validator.py
    • tests/unit_tests/common/test_query_context_processor.py
    • tests/unit_tests/connectors/sqla/models_test.py
    • tests/unit_tests/mcp_service/chart/test_compile.py
    • tests/unit_tests/mcp_service/chart/test_gauge_chart.py
    • tests/unit_tests/mcp_service/chart/test_preview_utils.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py
    • tests/unit_tests/mcp_service/chart/tool/test_update_chart.py
    • tests/unit_tests/mcp_service/explore/tool/test_generate_explore_link.py
    • superset/mcp_service/chart/tool/update_chart.py
    • superset/mcp_service/chart/tool/update_chart_preview.py
    • tests/unit_tests/mcp_service/chart/test_gauge_render.py
    • tests/unit_tests/mcp_service/chart/tool/test_generate_chart.py
    • tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@aminghadersohi
aminghadersohi merged commit 08e686b into master Sep 9, 2026
77 checks passed
@aminghadersohi
aminghadersohi deleted the sc-119163-mcp-gauge-gap-fixes branch September 9, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Namespace | Anything related to documentation size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants