Skip to content

feat(mcp): sankey chart type plugin - #43573

Open
gkneighb wants to merge 2 commits into
apache:masterfrom
gkneighb:feat/mcp-sankey-plugin
Open

gkneighb wants to merge 2 commits into
apache:masterfrom
gkneighb:feat/mcp-sankey-plugin

Conversation

@gkneighb

Copy link
Copy Markdown
Contributor

SUMMARY

Adds a generate_chart MCP plugin for the sankey chart type (viz_type: sankey_v2), so the MCP generate_chart tool can produce sankey flow diagrams. Sankey is a shipped Superset viz type that had no MCP plugin; this closes that gap.

The plugin mirrors the frontend Sankey buildQuery contract: a source and a target column define the edges of the flow diagram (the query groups by both) and one metric weights each edge. When sort_by_metric is set (the frontend default), edges are ordered by the metric descending.

Follows the established plugin pattern (pie/funnel/gauge/treemap/heatmap/radar/bubble/waterfall) — a config schema, a map_*_config mapper, a plugin class, and registration:

  • SankeyChartConfig — source, target, and metric required; optional sort_by_metric, row_limit, filters, color_scheme. Added to the ChartConfig discriminated union and the get_chart_type_schema adapters. source/target may not be saved_metric/sql_expression (node dimensions, not metrics).
  • map_sankey_config — maps the config to form_data; saved metrics pass through as a bare name string, ad-hoc metrics as SIMPLE/SQL adhoc objects.
  • SankeyChartPlugin — registered in the plugin registry. sankey_v2 was absent from the recommendation category map, so this adds it (its own sankey category).

The field set is intentionally minimal (core query contract).

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — backend/MCP only, no UI change.

TESTING INSTRUCTIONS

pytest tests/unit_tests/mcp_service/chart/test_sankey_chart.py — 18 unit tests cover schema validation (three required fields, source/target not-a-metric, extra-field rejection), ChartConfig union dispatch, form_data mapping (source/target/metric/sort/filters/saved-metric), and registry integration (registration, resolve_viz_type, display_name, pre_validate, recommendation category).

To exercise end-to-end: call the generate_chart MCP tool with
{"chart_type": "sankey_v2", "source": {"name": "from_stage"}, "target": {"name": "to_stage"}, "metric": {"name": "users", "aggregate": "SUM"}}.

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

🤖 Generated with Claude Code

@codecov

codecov Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 43.20988% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.12%. Comparing base (342a8f1) to head (6886650).
⚠️ Report is 180 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/chart/plugins/sankey.py 34.54% 36 Missing ⚠️
superset/mcp_service/chart/chart_utils.py 28.57% 5 Missing ⚠️
superset/mcp_service/chart/schemas.py 70.58% 5 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #43573       +/-   ##
===========================================
- Coverage   81.97%   66.12%   -15.86%     
===========================================
  Files        2988     2989        +1     
  Lines      184142   184223       +81     
  Branches    42565    42579       +14     
===========================================
- Hits       150952   121812    -29140     
- Misses      30427    59763    +29336     
+ Partials     2763     2648      -115     
Flag Coverage Δ
hive 36.38% <43.20%> (+<0.01%) ⬆️
mysql 55.20% <43.20%> (-0.01%) ⬇️
postgres 55.21% <43.20%> (?)
presto 38.26% <43.20%> (+<0.01%) ⬆️
python 55.52% <43.20%> (-30.48%) ⬇️
sqlite 54.93% <43.20%> (-0.01%) ⬇️
unit ?

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.

@gkneighb
gkneighb force-pushed the feat/mcp-sankey-plugin branch from 4a45a88 to 619081f Compare August 27, 2026 11:11
@gkneighb
gkneighb marked this pull request as ready for review August 27, 2026 12:05
@dosubot dosubot Bot added the viz:charts:sankey Related to the Sankey chart label Aug 27, 2026
Comment on lines +1095 to +1101
for col, name in ((self.source, "source"), (self.target, "target")):
_reject_sql_expression_on_dimension(col, name)
if col and col.saved_metric:
raise ValueError(
f"{name} cannot use saved_metric=True; "
"saved metrics belong in the 'metric' field"
)

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.

Suggestion: Dimension validation rejects sql_expression and saved_metric, but it does not reject aggregate on source or target. Such a configuration passes schema and dataset aggregation validation for compatible aggregates, then map_sankey_config discards the aggregate and emits only the raw column name, silently changing the requested Sankey semantics. Reject aggregated source/target references during validation. [type error]

Severity Level: Major ⚠️
- ❌ Sankey charts can show flows grouped by raw nodes despite requested source/target aggregation.
- ⚠️ `generate_chart` accepts invalid dimension configuration without feedback.
- ⚠️ Saved charts preserve misleading configuration semantics in generated form data.

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/chart/schemas.py
**Line:** 1095:1101
**Comment:**
	*Type Error: Dimension validation rejects `sql_expression` and `saved_metric`, but it does not reject `aggregate` on `source` or `target`. Such a configuration passes schema and dataset aggregation validation for compatible aggregates, then `map_sankey_config` discards the aggregate and emits only the raw column name, silently changing the requested Sankey semantics. Reject aggregated source/target references during validation.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 073e59e.

Source and target are now rejected when col.is_metric is true, covering aggregated or saved-metric references and requiring plain node columns.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of c47db17.

Source and target are now rejected when col.is_metric is true, covering aggregated or saved-metric references and requiring plain node columns.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 6886650.

The Sankey dimension validator now rejects any source or target where col.is_metric is true, covering aggregated and saved-metric references while allowing only plain node columns.

If that's not right, unresolve this thread and CodeAnt will leave it open.

@bito-code-review

Copy link
Copy Markdown
Contributor

The issue is correct. The SankeyChartConfig validator currently only checks for sql_expression and saved_metric on source and target dimensions, but it fails to reject the aggregate field, which leads to silent semantic changes in the generated Sankey configuration. To resolve this, update the reject_metric_style_nodes validator in superset/mcp_service/chart/schemas.py to also check for and reject the aggregate field on source and target columns.

superset/mcp_service/chart/schemas.py

@model_validator(mode="after")
    def reject_metric_style_nodes(self) -> "SankeyChartConfig":
        """source and target are node dimensions, not metrics."""
        for col, name in ((self.source, "source"), (self.target, "target")):
            _reject_sql_expression_on_dimension(col, name)
            if col and (col.saved_metric or col.aggregate):
                raise ValueError(
                    f"{name} cannot use saved_metric=True or aggregate; "
                    "saved metrics and aggregates belong in the 'metric' field"
                )
        return self

Comment on lines +1031 to +1033
"source": config.source.name,
"target": config.target.name,
"metric": create_metric_object(config.metric),

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.

Suggestion: The MCP query builder reads grouping columns from groupby, but this mapper only emits source and target; unlike the frontend buildQuery, the backend MCP path does not derive groupby from those fields. Generated Sankey queries therefore aggregate the metric over the entire dataset instead of producing one weighted edge per source/target pair. Include both node columns in the query grouping data or add equivalent Sankey-specific handling to the MCP query builder. [api mismatch]

Severity Level: Critical 🚨
- ❌ MCP Sankey previews aggregate all edges together.
- ❌ Generated Sankey flow relationships are lost.
- ⚠️ Compile validation checks the wrong query shape.

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/chart/chart_utils.py
**Line:** 1031:1033
**Comment:**
	*Api Mismatch: The MCP query builder reads grouping columns from `groupby`, but this mapper only emits `source` and `target`; unlike the frontend `buildQuery`, the backend MCP path does not derive `groupby` from those fields. Generated Sankey queries therefore aggregate the metric over the entire dataset instead of producing one weighted edge per source/target pair. Include both node columns in the query grouping data or add equivalent Sankey-specific handling to the MCP query builder.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 073e59e.

The Sankey form data now explicitly includes groupby containing both the source and target column names.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of c47db17.

The Sankey form data now explicitly includes groupby containing both the source and target column names.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 6886650.

map_sankey_config now emits groupby containing both config.source.name and config.target.name, ensuring MCP queries aggregate one weighted row per source/target pair.

If that's not right, unresolve this thread and CodeAnt will leave it open.

"source": config.source.name,
"target": config.target.name,
"metric": create_metric_object(config.metric),
"sort_by_metric": config.sort_by_metric,

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.

Suggestion: sort_by_metric is preserved in form data, but the MCP query builder does not translate it into an orderby clause; the frontend-only buildQuery implementation is not invoked by the MCP generation path. As a result, requests using the default sort_by_metric=True do not actually order edges by descending metric. Add the corresponding order-by information when constructing the MCP query. [logic error]

Severity Level: Major ⚠️
- ⚠️ MCP Sankey previews ignore default metric ordering.
- ⚠️ Largest flows may not appear first.
- ⚠️ Row-limited results can omit higher-weight edges.

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/chart/chart_utils.py
**Line:** 1034:1034
**Comment:**
	*Logic Error: `sort_by_metric` is preserved in form data, but the MCP query builder does not translate it into an `orderby` clause; the frontend-only `buildQuery` implementation is not invoked by the MCP generation path. As a result, requests using the default `sort_by_metric=True` do not actually order edges by descending metric. Add the corresponding order-by information when constructing the MCP query.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@bito-code-review

bito-code-review Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #8f7497

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset/mcp_service/chart/tool/get_chart_type_schema.py - 1
    • Docstring missing sankey_v2 · Line 39-52
      The diff adds `sankey_v2` to the adapter registry and examples, but the tool's docstring (lines 247-249) still lists only 10 chart types, omitting `sankey_v2`. Since this docstring is the tool's contract for LLM agents (per `superset/mcp_service/CLAUDE.md`), agents reading it won't discover `sankey_v2` as a valid value. Update the docstring to include `sankey_v2` in the valid chart_type list.
  • superset/mcp_service/chart/plugins/sankey.py - 1
    • Dead code pass branch · Line 113-115
      The `if config_dict["metric"].get("sql_expression"): pass` branch on lines 113-115 is dead code—the `pass` does nothing. Restructure to `if config_dict.get("metric") and not config_dict["metric"].get("sql_expression"):` and nest the saved_metric/else branches inside, eliminating the no-op branch.
Review Details
  • Files reviewed - 7 · Commit Range: 619081f..619081f
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/sankey.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/get_chart_data.py
    • superset/mcp_service/chart/tool/get_chart_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_sankey_chart.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 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.

Review of feat(mcp): sankey chart type plugin — first human review on this PR. This is the 6th of the MCP chart-plugin family, and it carries the most consequential defect we've seen in the set. Not requesting changes (COMMENT only), but flagging one Critical correctness bug plus the now-familiar aggregate validation gap. All claims below were executed against the PR head (619081fbad) unless marked INSPECTED.

🔴 Critical — the generated query has no GROUP BY (bot threads 2 & 3, one root cause)

map_sankey_config (chart_utils.py:1021) emits source and target as top-level form_data keys, but the MCP query builder never reads those keys. It derives grouping columns via columns_from_form_data (superset/common/form_data_query_context.py:133), which looks only at groupby / columns / x_axis. Since the mapper emits neither groupby nor orderby, the resulting query groups by nothing and orders by nothing.

Every sibling mapper emits groupby explicitly — e.g. map_pie_config does "groupby": [config.dimension.name], funnel does "groupby": [config.breakdown.name], box_plot/histogram likewise. Sankey is the only one that copied the frontend field names (source/target) verbatim instead of translating them to what the backend builder consumes. The frontend Sankey/buildQuery.ts bridges this gap (const groupby = [source, target], plus orderby from sort_by_metric), but the MCP path does not invoke buildQuery — so both transforms are silently lost.

Traced consequence (RAN):

map_sankey_config(cfg) keys = ['color_scheme','metric','row_limit','sort_by_metric','source','target','viz_type']
  form_data.get('groupby') = None
  form_data.get('orderby') = None
  columns_from_form_data(form_data) => []      # what the query actually GROUPs BY
  frontend contract groupby would be => ['from_stage','to_stage']

With columns=[] and metrics=[SUM(users)], _compile_chart builds SELECT SUM(users) FROM dataset [WHERE …] LIMIT 2 — a single aggregate row over the entire dataset, no GROUP BY. It is not a crash and nothing downstream backfills it (the only fallback in _compile_chart is granularity_sqla, which Sankey never sets). This is exactly codeant's thread-2 diagnosis, confirmed end to end.

Scope of impact (INSPECTED): generate_chart runs both _compile_chart (generate_chart.py:398,610) and generate_preview_from_form_data (:734) through this same path, and update_chart_preview too. So the compile check passes (a valid one-row query → false confidence) while the preview/data/SQL the tool returns to the agent show a single collapsed total rather than a flow diagram. A chart saved with save_chart=True still renders correctly when later opened in Explore (the frontend buildQuery runs there) — but the entire MCP-surfaced output for this tool is wrong. That is why Critical is the right severity here, distinct from the cosmetic gauge/radar issues.

Thread 3 (sort_by_metric) is the same bug: sort_by_metric is carried in form_data but never translated to orderby, because buildQuery (which would emit [metric, false]) is bypassed. form_data.get('orderby') is None above → no ordering, so row-limited results can drop the heaviest edges.

Fix — mirror the frontend contract inside map_sankey_config, matching how every other mapper already emits groupby:

form_data: Dict[str, Any] = {
    "viz_type": "sankey_v2",
    "groupby": [config.source.name, config.target.name],
    "metric": create_metric_object(config.metric),
    "sort_by_metric": config.sort_by_metric,
    "row_limit": config.row_limit,
    "color_scheme": config.color_scheme or "supersetColors",
}
if config.sort_by_metric:
    form_data["orderby"] = [[form_data["metric"], False]]

(Keeping source/target too is harmless if the frontend still expects them for rendering — but groupby is what makes the backend query correct.)

🟠 Major — reject_metric_style_nodes misses aggregate (bot thread 1)

reject_metric_style_nodes (schemas.py:1093) rejects sql_expression and saved_metric on source/target, but not aggregate. A metric-shaped node passes validation and the aggregate is then silently dropped by the mapper (RAN):

source={"name":"amount","aggregate":"SUM"} accepted; source.is_metric => True
map_sankey_config(...)['source'] => 'amount'   # aggregate SILENTLY DROPPED

This is byte-identical to the gap already confirmed in gauge (#43568), treemap (#43569), and radar (#43571). The canonical predicate already exists in this file: ColumnRef.is_metric at schemas.py:751 (bool(aggregate) or saved_metric or bool(sql_expression)). Gate on it:

if col.is_metric:
    raise ValueError(f"{name} must be a plain node column, not a metric …")

Sankey-specific severity: worse than gauge/radar (cosmetic) and on par with treemap's grain corruption. source/target are the GROUP BY dimensions of the flow. Combined with the Critical above (once groupby is fixed), an aggregated node would either be dropped to its raw name or, if it reached grouping, redefine the flow topology — so this should be closed together with the groupby fix.

Tests (Rule 26)

test_sankey_chart.py (+178, 16 tests, all RAN green). Reverting only the production files makes every test fail at import (they import map_sankey_config / SankeyChartConfig), so none is an independent regression guard. More importantly, none asserts the behavior that is actually broken:

  • No test asserts groupby/orderby in the emitted form_data. test_basic_sankey_form_data asserts form_data["source"] == "from_stage" / ["target"] == "to_stage" — it locks in the buggy source/target-only shape as if it were the contract (the same pattern radar's tests fell into).
  • No negative test for aggregate on source/target — only saved_metric is covered.

A single assertion — assert columns_from_form_data(map_sankey_config(cfg)) == ["from_stage","to_stage"] — would have caught the Critical bug at authoring time. Worth adding alongside the fix, and worth back-porting the columns_from_form_data assertion to the sibling mappers.

Family synthesis

This is the 7th confirmed instance of the same shape in mcp_service: the MCP path constructs QueryContext straight from form_data and re-implements, per chart, whatever the frontend buildQuery does — so any transform that lives only in buildQuery is lost unless the mapper re-derives it. Prior instances: #43432 granularity_sqla vs granularity, SC-117852 Bugs A/B, #43225 OpenAPI/TS divergence, gauge bounds, radar arity, treemap grain. The per-PR fix here is two lines in map_sankey_config; the durable fix is architectural — have the MCP path reuse the canonical buildQuery-equivalent grouping/order derivation rather than hand-copying it into each mapper. Not a blocker for this PR, but the recurrence is the real signal.

Gates / status (re-derived at post time)

  • CI: 61 unique checks (latest run per name) — 46 success, 12 skipped, 3 neutral, 0 pending, 0 failing. Green.
  • Mergeability: mergeable=MERGEABLE, mergeStateStatus=BLOCKED — blocked by branch protection (external contributor needs maintainer approval), not by CI.
  • External contributor; leaving this as a comment for maintainer sign-off.

Thanks for the thorough test file and the clear buildQuery-contract docstrings — the mapping bug is subtle precisely because the docstrings describe the right contract; it's just the two derived fields (groupby, orderby) that don't make it into form_data.

@netlify

netlify Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 6886650
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6abf25f0e6d0a90007d596f6
😎 Deploy Preview https://deploy-preview-43573--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.

@aminghadersohi aminghadersohi 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.

Round 2 — reviewed the new head cdcfaa65f0 (fix(mcp): sankey — emit orderby + reject aggregate on source/target). All three findings from my round-1 review are fixed, and each was verified by executing against the pinned head, not just reading the diff. Still a comment (external contributor) — but from a correctness standpoint this is now clean.

✅ Critical — missing GROUP BY — FIXED (verified)

map_sankey_config now emits "groupby": [config.source.name, config.target.name]. Traced end to end (RAN):

map_sankey_config(cfg)['groupby']        => ['from_stage', 'to_stage']
columns_from_form_data(map_sankey_config(cfg)) => ['from_stage', 'to_stage']   # was [] in round 1

The MCP query now groups by both node columns, so _compile_chart/preview produce one weighted edge per source→target pair instead of a single collapsed aggregate row. The added docstring accurately explains why the explicit groupby is required (backend builders resolve grouping from groupby alone and carry no source/target alias). Resolves codeant thread at chart_utils.py (grouping).

⚠️ Major — sort_by_metric → orderby — CORRECTION: this section was wrong

if config.sort_by_metric:
    form_data["orderby"] = [[form_data["metric"], False]]

RAN: with sort_by_metric=True, orderby == [[<metric>, False]] (descending, mirroring Sankey/buildQuery.ts); with sort_by_metric=False, the orderby key is correctly absent. Row-limited results now keep the heaviest edges. Resolves the codeant sort_by_metric thread.

Correction (2026-08-31). The claim above is inaccurate and I'm amending it rather than deleting it. Setting form_data["orderby"] is a no-op on the MCP data path: build_query_dicts_from_form_data → _build_single_query_dict never reads a top-level orderby (only the deck_* branch does). What I actually executed was the mapper populating the key — not the query dict consuming it — so "verified" overstated the evidence. The ordering was genuinely unfixed at this head. @gkneighb caught this independently and fixed it properly in b5682e87 by emitting the orderby in the shared query builder; see my round-3 review. Apologies for the noise.

✅ Major — aggregate on source/target — FIXED (verified)

reject_metric_style_nodes now gates on the canonical ColumnRef.is_metric (schemas.py:751) instead of saved_metric alone. RAN:

source={"name":"amount","aggregate":"SUM"}  -> REJECTED ("source must be a plain node column, not a metric …")
target={"name":"amount","aggregate":"SUM"}  -> REJECTED
source saved_metric=True                    -> still rejected
basic node config                           -> still accepted

Resolves the codeant/bito aggregate thread.

✅ Test coverage gap — CLOSED

The +62 test lines add exactly the regression guards that were missing in round 1, and they assert the real behavior (not the old buggy shape):

  • test_columns_from_form_data_returns_the_node_columns — end-to-end guard that the emitted form_data actually groups by [from_stage, to_stage] (the assertion that would have caught the Critical at authoring time);
  • test_resolve_groupby_returns_the_node_columns;
  • groupby / orderby assertions in the mapping tests, incl. the sort_by_metric=False → no orderby case;
  • test_sankey_source_rejects_aggregate / test_sankey_target_rejects_aggregate.

Full suite RAN green: 20 passed (was 16). Mentally reverting the two production edits now breaks these guards — they are genuine regression tests, no longer just import-coupled.

Regressions / new issues in the round-2 delta

None. The delta is +14 lines in chart_utils.py, −3/+4 in schemas.py, +62 tests. Keeping source/target in form_data alongside groupby is harmless (the frontend renderer reads source/target; the backend query reads groupby); no double-grouping since the frontend buildQuery constructs its own query object.

Status (re-derived at post time, head cdcfaa65f0)

  • CI: 60 unique checks (latest run per name) — 47 success, 11 skipped, 2 neutral, 0 pending, 0 failing; netlify docs-preview StatusContext SUCCESS. Green.
  • Mergeability: mergeable=MERGEABLE, mergeStateStatus=BLOCKED — branch protection (external contributor needs maintainer approval), not CI.
  • The three codeant threads still show unresolved on GitHub (auto-reanchored to the new head), but all three are addressed in code as shown above — they can be marked resolved.

Nice turnaround — the fixes match the frontend buildQuery contract precisely, and this was the deepest instance of the family's buildQuery-bypass defect. Thanks for adding the end-to-end columns_from_form_data guard; that's the durable protection against this regressing.

@bito-code-review

bito-code-review Bot commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #b8a9f1

Actionable Suggestions - 0
Review Details
  • Files reviewed - 3 · Commit Range: 619081f..cdcfaa6
    • superset/mcp_service/chart/chart_utils.py
    • tests/unit_tests/mcp_service/chart/test_sankey_chart.py
    • superset/mcp_service/chart/schemas.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

@gkneighb

Copy link
Copy Markdown
Contributor Author

@aminghadersohi — correction on my round-2 fix: you're right that the orderby I'd stashed in form_data was a no-op. _build_single_query_dict never reads a top-level orderby (only the deck_* path does), which your funnel review made clear. Fixed properly in b5682e877e:

  • Removed the no-op form_data["orderby"] emission from map_sankey_config.
  • Emit orderby in the shared _build_single_query_dict when sort_by_metric is set, so the metric ordering actually reaches the query (this covers pie/funnel/treemap by the same path).
  • Replaced the mapper-level assertion with a query-context test: build_query_dicts_from_form_data(...) now yields columns=['from_stage','to_stage'] and a descending metric orderby.

The GROUP BY fix from round 2 stands; this closes the ordering half.

@bito-code-review

bito-code-review Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #a9bf70

Actionable Suggestions - 0
Review Details
  • Files reviewed - 3 · Commit Range: cdcfaa6..b5682e8
    • superset/mcp_service/chart/chart_helpers.py
    • superset/mcp_service/chart/chart_utils.py
    • tests/unit_tests/mcp_service/chart/test_sankey_chart.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

@rusackas
rusackas requested review from rusackas and a lite review from Copilot August 30, 2026 20:20

Copilot AI 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.

Pull request overview

Adds MCP generate_chart support for Superset’s shipped Sankey visualization (viz_type: sankey_v2) by introducing a new chart config schema, a form-data mapper, a plugin implementation, and registry/schema/category wiring so Sankey charts can be generated via MCP with the same core query contract as the frontend.

Changes:

  • Introduces SankeyChartConfig (schema + validation) and adds it to the ChartConfig discriminated union and chart-type schema adapters.
  • Implements Sankey form-data mapping (map_sankey_config) and a new SankeyChartPlugin, then registers it in the MCP chart plugin registry.
  • Adds Sankey to the chart recommendation category map and adds query-dict ordering support for sort_by_metric.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/unit_tests/mcp_service/chart/test_sankey_chart.py Adds unit tests for Sankey schema validation, form_data mapping, query-dict behavior, and registry/schema/category integration.
superset/mcp_service/chart/tool/get_chart_type_schema.py Registers sankey_v2 for schema discovery and provides an example payload.
superset/mcp_service/chart/tool/get_chart_data.py Adds sankey_v2 to the viz-type category map used by the MCP chart tool.
superset/mcp_service/chart/schemas.py Defines SankeyChartConfig and adds it into the ChartConfig union.
superset/mcp_service/chart/plugins/sankey.py Adds the Sankey chart MCP plugin (validation hints, normalization, naming, form_data mapping hook).
superset/mcp_service/chart/plugins/init.py Registers the new Sankey plugin.
superset/mcp_service/chart/chart_utils.py Adds map_sankey_config and Sankey chart naming helper (_sankey_chart_what).
superset/mcp_service/chart/chart_helpers.py Ensures sort_by_metric charts emit query-dict orderby so row limits behave as “top-N by metric”.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +139 to +146
suggestions=[
"Ensure 'source' and 'target' each have a 'name'",
"Ensure 'metric' field has 'name' and 'aggregate'",
"Example: {'chart_type': 'sankey_v2', "
"'source': {'name': 'from_stage'}, "
"'target': {'name': 'to_stage'}, "
"'metric': {'name': 'users', 'aggregate': 'SUM'}}",
],
Comment on lines +492 to +496
# sort_by_metric charts (pie/funnel/treemap/sankey) order by the metric
# descending. buildQuery derives this on the frontend; the MCP path builds
# the query dict directly and never reads a top-level form_data['orderby'],
# so translate the flag here or a row_limit truncates an unordered result
# (dropping the heaviest rows rather than the top-N by the metric).
Comment on lines +142 to +154
def test_sankey_form_data_with_filters_and_no_sort(self) -> None:
config = SankeyChartConfig(
chart_type="sankey_v2",
source={"name": "from_stage"},
target={"name": "to_stage"},
metric={"name": "users", "aggregate": "SUM"},
sort_by_metric=False,
filters=[{"column": "year", "op": "=", "value": 2026}],
)
form_data = map_sankey_config(config)
assert form_data["sort_by_metric"] is False
assert "orderby" not in form_data # no metric ordering when unset
assert form_data["adhoc_filters"], "filters must map to adhoc_filters"

@aminghadersohi aminghadersohi 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.

Reviewed at b5682e877e. All prior findings fixed and verified here:

  • GROUP BY — map_sankey_config emits groupby=[source, target]; resolve_groupby/columns_from_form_data yield ['from_stage','to_stage'].
  • Metric ordering — correctly moved out of form_data (a top-level orderby is a no-op on the MCP data path — only the deck_* branch reads it) into _build_single_query_dict. The built query dict now carries a descending orderby on the metric when sort_by_metric=True, and none when False. This closes the ordering half that round-2's form_data["orderby"] never reached.
  • aggregate on source/target — still rejected via ColumnRef.is_metric.

Net-new since the last round:

  • Merge conflict — the branch is CONFLICTING against master in superset/mcp_service/chart/schemas.py after #43480 (interactive pivot) landed in the same ChartConfig union region. A rebase is required before this can merge.

The three open Copilot threads (schema_error_hint wording, the pie/funnel/treemap/sankey comment in _build_single_query_dict — only pie and sankey set sort_by_metric on the MCP path today, and the extra sort_by_metric=False builder assertion) are non-blocking nits; no need to duplicate them here.

@gkneighb
gkneighb force-pushed the feat/mcp-sankey-plugin branch from b5682e8 to 6a86a0a Compare August 31, 2026 15:20

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

Approve — clean, well-tested plugin that follows the established MCP pattern (config → mapper → plugin → registration) and the security model is intact. Two non-blocking Mediums worth addressing:

🟡 Medium — sankey_v2 is wired up but never advertised to the LLM, so it's undiscoverable through the primary tool surface

The type is added to the discriminated union, registry, _CHART_TYPE_ADAPTERS, and _VIZ_CATEGORY, but every prose enumeration an LLM actually reads to pick a chart type still stops at waterfall:

  • generate_chart.py:88 — 'histogram', 'box_plot', 'waterfall' (no sankey_v2)
  • app.py:421 and the per-type description list ending at app.py:410 (DEFAULT_INSTRUCTIONS)

When waterfall was added it was threaded into all of these. Since the feature's whole purpose is MCP discoverability, this omission undercuts it — dynamic discovery (get_chart_type_schema) will surface it, but the primary generate_chart docstring won't.

Fix: add sankey_v2 to those valid-type lists and give it a one-line per-type description/example, mirroring how waterfall is documented.

🟡 Medium — the query orderby only partially reproduces the frontend buildQuery the docstrings claim it "matches"

chart_helpers.py:497-498 emits orderby = [(metric, False)], and only when sort_by_metric is truthy. But superset-frontend/plugins/plugin-chart-echarts/src/Sankey/buildQuery.ts:29-42 appends [source, true] and [target, true] unconditionally, and applies ordering whenever row_limit is nonzero:

if (sort_by_metric && metric) orderby.push([metric, false]);
[source, target].forEach(column => {
  if (column) orderby.push([column, true]);   // unconditional asc tiebreakers
});

So with sort_by_metric=False the frontend still orders by source ASC, target ASC while the MCP path emits no ORDER BY at all. The divergence is silent and only surfaces when the distinct-edge count exceeds row_limit (default 10000): the truncation then keeps a different, nondeterministic subset of edges than the UI, and even with sort_by_metric=True the boundary ties are broken arbitrarily. That contradicts the map_sankey_config (chart_utils.py:1021) and SankeyChartConfig docstrings, which state the mapping "Matches the frontend Sankey buildQuery contract."

Fix: for sankey_v2, append (source, True) and (target, True) after the metric term (and emit them even when sort_by_metric is False), or soften the "matches the frontend contract" wording to "replicates the metric ordering only." Note the current suite wouldn't catch this — test_sankey_chart.py:227 only asserts orderby[0][1] is False.


Also spotted a few Lows (not blocking): the shared _build_single_query_dict change also alters the existing pie query path with no pie test; sort_by_metric defaults to True and is labeled a "(frontend default)" but the Sankey control sets no default (UI-created sankeys are unchecked); and several plugin methods (normalize_column_refs, generate_name, color_scheme mapping, extract_column_refs, row_limit bounds) are untested. Happy to expand on any of these if useful.

@bito-code-review

bito-code-review Bot commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #cfaa97

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset/mcp_service/chart/schemas.py - 1
    • Missing metric validation · Line 1068-1072
      `reject_metric_style_nodes` rejects metrics in `source`/`target` but never validates that `metric` is actually a metric. A plain column (no `aggregate`/`saved_metric`/`sql_expression`) passes through to `create_metric_object`, which emits a non-aggregated SIMPLE expression — invalid for a `GROUP BY source, target` query, producing a broken/incorrect result. Consider mirroring the existing `is_metric` check for `self.metric`.
  • superset/mcp_service/chart/plugins/sankey.py - 1
    • Dynamically typed expressions disallowed · Line 82-82
      Replace `Any` with specific types in method signatures. For example, `config` in `extract_column_refs` can be typed as `object` and narrowed with `isinstance`. Similar issues exist in `to_form_data`, `generate_name`, `resolve_viz_type`, and `normalize_column_refs`.
Review Details
  • Files reviewed - 8 · Commit Range: 93afa07..6a86a0a
    • superset/mcp_service/chart/chart_helpers.py
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/sankey.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/get_chart_data.py
    • superset/mcp_service/chart/tool/get_chart_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_sankey_chart.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 commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

@gkneighb Thanks for fixing the original Sankey grouping and shared-builder ordering issues. A fresh product-completeness pass at head 6a86a0a3 found the typed foundation is still incomplete in the end-to-end MCP paths. Could you please address or explicitly coordinate these before we count Sankey support as complete?

  1. Unsaved query + previews: generate_chart(save_chart=False) still bypasses the Sankey-aware shared query builder, so limited previews can use unordered edges. Saved previews fall through to scatter and unsaved previews to an incomplete bar; add a shared Sankey representation using source, target, and the resolved saved/SIMPLE/SQL metric label, with all-row validation—or return a clear unsupported-format error rather than a false chart.
  2. Native round-trip and updates: adapt returned native string dimensions, derived groupby, and saved/SIMPLE/SQL metric objects back into SankeyChartConfig. Preserve omitted sort_by_metric, row_limit, color, filters, and UI state across immediate, preview-first, and cached updates; explicit values/empty filters should still override.
  3. Sankey semantics: require finite numeric metric output (including saved/SQL metrics), reject canonically identical or case-ambiguous source/target roles, and normalize casing before every tool path maps form data.
  4. Discovery: add a real Sankey recommendation candidate for a two-categorical-plus-numeric result, not only the current-viz exclusion mapping.
  5. Product-path tests: cover generate/update/update-preview/get-data plus saved/form-data-key and unsaved previews, including error/empty/malformed envelopes and row-limit ordering.

I ran the 21 Sankey tests and the full MCP chart suite (1,388 passed) plus adversarial reproductions; these gaps remain despite green CI. Shared builder, normalization, and query-error work in #43737 should be reused/rebased rather than duplicated, while retaining this PR’s Sankey ordering behavior. I did not modify or push to your branch.

@aminghadersohi

Copy link
Copy Markdown
Contributor

Fresh re-review at eb72811b549c confirms progress since bf5628cb: unsaved compile/preview queries now use metric-descending ordering; cached-preview mapping normalizes column case first; omitted filters/UI state survive updates, and explicit empty filters clear them. Ambiguous non-exact column references and failed-status query envelopes are also handled.

The remaining gaps from the previous review are reproducible:

  • Preview fidelity: the same Sankey edge produces an unsaved Vega-Lite bar with only a color encoding, while saved previews still take the scatter fallback. Neither represents weighted flow; please provide a faithful representation or an explicit unsupported-format error.
  • Round-trip/updates: mapper-produced native form data still fails GenerateChartRequest with Unknown field 'groupby' for saved/SIMPLE/SQL metrics. Immediate and cached-preview reproductions reset omitted sort_by_metric=False, row_limit=37, and color_scheme='lyftColors' to True, 10000, and supersetColors. The shared merge only protects omitted mapped controls for Gauge, not Sankey.
  • Validation: string/NaN/infinite weights still pass compilation for saved/SIMPLE/SQL metrics, including an invalid second row. source=Node,target=node with one physical Node column still normalizes to duplicate Node,Node roles. A malformed data: 'malformed' envelope also reports compile success. The result validator remains Gauge-only.
  • Recommendations: two string dimensions plus one numeric column still yields only bar chart/pie chart, not Sankey.

Verification: chart unit suite 1,583 passed, 1 skipped, plus targeted mocked reproductions including the cached-preview tool. Sankey-specific tests remain confined to schema/mapper/registry coverage. GitHub reports mergeable/clean, with 63 checks passing and 12 skipped. No live app was available; no contributor code was changed or pushed.

@bito-code-review

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

Copy link
Copy Markdown
Contributor

Code Review Agent Run #3cfe4d

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset/mcp_service/chart/schemas.py - 1
    • Missing metric-role validation · Line 1381-1385
      `metric` is a plain `ColumnRef` with no metric-role check, unlike sibling single-metric configs `GaugeChartConfig` (line 1288) and `BigNumberChartConfig` (line 2041) which enforce `self.metric.is_metric`. A bare column name passes validation and `create_metric_object` silently defaults to `SUM`, diverging from the documented contract. Add a validator raising `ValueError` when `not self.metric.is_metric`.
  • superset/mcp_service/chart/plugins/sankey.py - 1
    • Dead no-op branch · Line 115-115
      The `if ...get("sql_expression"): pass` branch is a reachable no-op that adds confusion. Restructure to avoid the empty branch, e.g. check `saved_metric` first and only call `get_canonical_column_name` when there is no `sql_expression`. Note this mirrors `pie.py`; consider aligning both.
Review Details
  • Files reviewed - 8 · Commit Range: 7e3d24c..eb72811
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/sankey.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_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_sankey_chart.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

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Sep 11, 2026
@gkneighb
gkneighb force-pushed the feat/mcp-sankey-plugin branch from eb72811 to ce40156 Compare September 19, 2026 02:56
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Sep 19, 2026
@bito-code-review

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

Copy link
Copy Markdown
Contributor

Code Review Agent Run #dc83b2

Actionable Suggestions - 0
Review Details
  • Files reviewed - 8 · Commit Range: fdf5be5..ce40156
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/sankey.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_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_sankey_chart.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

@gkneighb

Copy link
Copy Markdown
Contributor Author

Heads-up for anyone reviewing the red check here: the three failing checks are not from this diff.

All three come from the same six tests in tests/unit_tests/commands/test_base_restore_version_command.py, which fail on current master for every PR built against it — including PRs from other authors (e.g. #44435). This branch touches nothing under superset/commands/. Filed with the root cause and a suggested fix as #44436.

Everything else on this run is green (61 passed, 3 failed, 0 cancelled), and the branch is rebased onto master and mergeable.

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Sep 22, 2026
@rusackas

Copy link
Copy Markdown
Member

This one also seems to need a rebase, sorry things keep churning. Holler if you want a hand on any of these PRs.

@gkneighb
gkneighb force-pushed the feat/mcp-sankey-plugin branch from ce40156 to 073e59e Compare September 24, 2026 02:20
@gkneighb

Copy link
Copy Markdown
Contributor Author

Rebased onto current master, since bubble (#43572) merged on 2026-09-21 and re-conflicted this branch.

Also proactively fixed a defect a reviewer caught on the sibling PRs (#43570, #43571): my earlier Gantt-cascade rebase left a duplicated line in the get_chart_type_schema docstring, so clients received interactive_pivot. twice in the live tool description. It was present on this branch too and is now gone; I audited all four branches in this family and all are clean.

This head also picks up 43fee87672, which fixes the unrelated test_base_restore_version_command failures (#44436) that reddened the previous run.

No change to the plugin itself. Re-verified at this head: _CHART_TYPE_ADAPTERS 16 == _CHART_EXAMPLES 16 with nothing missing, this type plus bubble_v2 and gantt all registered, the chart's own test file passing, ruff/mypy/pylint clean on the committed tree, and git merge-tree clean against master.

@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Sep 24, 2026

@bito-code-review bito-code-review Bot 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.

Code Review Agent Run #218fdd

Actionable Suggestions - 1
  • superset/mcp_service/chart/plugins/sankey.py - 1
Review Details
  • Files reviewed - 8 · Commit Range: c4c74e2..073e59e
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/sankey.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_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_sankey_chart.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

def resolve_viz_type(self, config: Any) -> str:
return "sankey_v2"

def normalize_column_refs(self, config: Any, dataset_context: Any) -> Any:

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.

fields_set inflation resets controls

normalize_column_refs returns SankeyChartConfig.model_validate(config.model_dump()), so every field lands in model_fields_set. On the update path (update_chart.py → DatasetValidator.normalize_column_names → merge_chart_form_data), the color_scheme/row_limit preservation guard (field not in fields_set) never fires, so updates omitting these controls silently reset them to mapper defaults. Use model_dump(exclude_unset=True) as pie.py does.

Code Review Run #218fdd


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

@rusackas rusackas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One thing before this merges, though.

Bito's thread on sankey.py is worth fixing, not just a style nag. I traced it through merge_chart_form_data and it's real: since normalize_column_refs round-trips through a full model_dump(), every field lands in model_fields_set, so any chart update that omits color_scheme/row_limit silently resets them, and existing adhoc filters don't get preserved either.

@gkneighb mind switching to model_dump(exclude_unset=True) like pie.py does?

@gkneighb
gkneighb force-pushed the feat/mcp-sankey-plugin branch from 073e59e to c47db17 Compare September 29, 2026 03:03
@gkneighb

Copy link
Copy Markdown
Contributor Author

@rusackas Done at c47db173bb — you were right that it's real rather than a style nag, and your trace through merge_chart_form_data matches what I measured.

sankey.py now uses model_dump(exclude_unset=True) like pie.py, with a regression test beside the existing pie twin in test_chart_utils.py (test_merge_sankey_preserves_omitted_defaults). It runs the config through DatasetValidator.normalize_column_names first, exactly as update_chart does, then asserts an omitted color_scheme/row_limit survives while explicitly supplied values still win. It fails on the previous head and passes now.

Since the same shape could be anywhere, I audited every registered plugin rather than just this one:

plugin state
sankey, funnel, heatmap had it — fixed on their own PRs
radar had it — fixed earlier on #43571
bubble had it on master — #44618
gantt not affected — it captures model_fields_set before the dump and restores it after, which is equivalent
the other 13 already passed exclude_unset=True

So gantt looked guilty to a grep but is fine; I verified it by running normalize_column_refs and confirming row_limit stays out of model_fields_set.

Thanks for tracing it instead of forwarding the bot thread — that's what made it obvious it was worth chasing across the family.

@bito-code-review bito-code-review Bot 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.

Code Review Agent Run #d00ad7

Actionable Suggestions - 6
  • superset/mcp_service/chart/plugins/sankey.py - 1
  • tests/unit_tests/mcp_service/chart/test_sankey_chart.py - 3
  • superset/mcp_service/chart/schemas.py - 2
Additional Suggestions - 2
  • superset/mcp_service/chart/plugins/sankey.py - 2
    • Missing method docstrings · Line 45-148
      BITO adaptive rule 12147 requires inline docstrings on every newly introduced function. The seven overrides here (`pre_validate`, `extract_column_refs`, `to_form_data`, `generate_name`, `resolve_viz_type`, `normalize_column_refs`, `schema_error_hint`) have none. The `ChartTypePlugin` Protocol documents the contracts, but a one-line docstring per override keeps this file self-describing and satisfies the mandated standard.
    • Replace Any with specific types · Line 82-82
      `typing.Any` is used in multiple method signatures (`config` in `extract_column_refs`, `to_form_data`, `generate_name`, `resolve_viz_type`, `normalize_column_refs`, and `dataset_context` in `normalize_column_refs`). Replace with specific types like `SankeyChartConfig` and a defined `DatasetContext` type. The patch changes `extract_column_refs` to use `SankeyChartConfig` and removes the redundant isinstance check.
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • tests/unit_tests/mcp_service/chart/test_sankey_chart.py - 1
Review Details
  • Files reviewed - 9 · Commit Range: c4c74e2..c47db17
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/sankey.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_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_chart_utils.py
    • tests/unit_tests/mcp_service/chart/test_sankey_chart.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

Comment on lines +107 to +125
for key in ("source", "target"):
col = config_dict.get(key)
if col and not col.get("sql_expression") and not col.get("saved_metric"):
col["name"] = DatasetValidator.get_canonical_column_name(
col["name"], dataset_context
)
if config_dict.get("metric"):
if config_dict["metric"].get("sql_expression"):
pass
elif config_dict["metric"].get("saved_metric"):
config_dict["metric"]["name"] = (
DatasetValidator.get_canonical_metric_name(
config_dict["metric"]["name"], dataset_context
)
)
else:
config_dict["metric"]["name"] = (
DatasetValidator.get_canonical_column_name(
config_dict["metric"]["name"], dataset_context

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.

Duplicated metric normalization

The metric-normalization chain (sql_expression → skip, saved_metric → get_canonical_metric_name, else get_canonical_column_name) is copied verbatim from pie.py/treemap.py and appears in 13 sibling plugins; BaseChartPlugin hosts no shared helper. Extract one helper (e.g. on DatasetValidator beside get_canonical_*) and call it here, so the next canonicalization fix lands once instead of 13 times.

Code Review Run #d00ad7


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

class TestSankeyChartConfigSchema:
"""SankeyChartConfig schema validation."""

def test_basic_sankey_config(self) -> None:

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.

Missing test docstrings

16 of the 19 test functions (e.g. test_basic_sankey_config, test_group_by_and_order_by, test_pre_validate_missing_fields) have no docstring; only test_sankey_source_rejects_aggregate and the two query-builder tests do. BITO rule 12148 requires a docstring on every newly added test function documenting scenario and expected outcome.

Code Review Run #d00ad7


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

)
assert config.source.name == "from_stage"
assert config.target.name == "to_stage"
assert config.sort_by_metric is True # shared control default

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.

Unannotated test locals

Locals such as cfg (line 50), config, form_data, queries, and orderby are assigned without type annotations throughout the file. BITO rule 13153 asks for explicit annotations on test-file locals even when inferable; Any is already imported for this purpose.

Code Review Run #d00ad7


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

single unordered aggregate row.
"""

def test_group_by_and_order_by(self, monkeypatch) -> None:

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.

Unannotated fixture parameter

monkeypatch is a pytest fixture but is the only unannotated parameter in the file. BITO rule 12101 requires explicit annotations on fixture-injected parameters; pytest.MonkeyPatch is available in the repo's pytest 7.4.0.

Code Review Run #d00ad7


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Comment on lines +1696 to +1700
metric: ColumnRef = Field(
...,
description="Metric weighting each edge (use aggregate e.g. SUM, "
"COUNT for ad-hoc, or set saved_metric=True for a saved dataset metric)",
)

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.

Missing implicit-aggregate recording

Unlike BubbleChartConfig.record_implicit_metric_aggregate, no validator records an implicit SUM for a bare metric ref. create_metric_object defaults to SUM at map time, while DatasetValidator._validate_aggregations skips refs without aggregate — so SUM over a text column fails in the database instead of with a clear validation message. Mirror the Bubble validator.

Code Review Run #d00ad7


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

"""source and target are node dimensions, not metrics."""
for col, name in ((self.source, "source"), (self.target, "target")):
_reject_sql_expression_on_dimension(col, name)
if col and col.is_metric:

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.

Dead truthiness guard

source/target are required, non-optional ColumnRef fields, and Pydantic v2 BaseModel instances are always truthy (no __bool__/__len__), so col in col and col.is_metric is always true — a dead guard. Sibling BubbleChartConfig.reject_metric_style_dimensions omits it; drop the col and conjunct for consistency.

Code Review Run #d00ad7


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

@rusackas rusackas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The earlier review rounds all look genuinely closed out. I traced my own model_dump(exclude_unset=True) concern and it's fixed in sankey.py now, the groupby/sort_by_metric gaps codeant flagged are handled by the shared _build_single_query_dict path, and the field-reset issue doesn't reproduce anymore.

CI is red on the current head though: test_tool_inventory.py/test_chart_tool_inventory.py's byte-budget checks are failing for generate_chart, update_chart, and generate_explore_link (51432/50000, 55309/55000, 51144/50000). Those test files only exist on master, not on this branch, so the merge commit is what's actually failing, not the branch tip in isolation, this needs a rebase before it's even testable against current master. Given how close those numbers are to the budget already, I'd guess a rebase alone won't clear it and the new Sankey schema fields need trimming or the budget needs a deliberate bump, worth checking which before pushing.

One real nit while I'm in here: SankeyChartConfig doesn't have a record_implicit_metric_aggregate-style validator for metric the way BubbleChartConfig does for x/y/size, so a bare metric ref on a text column skips the clean validation error and fails at the database instead. Not a blocker, just worth matching Bubble's pattern.

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 2, 2026
Greg Neighbors and others added 2 commits October 1, 2026 23:32
Adds a generate_chart plugin for sankey (viz_type 'sankey_v2'): a source and
target column define the flow edges, a single metric weights them, and
sort_by_metric orders edges by that metric.

Rebased onto current master (past the gauge plugin + follow-up). Emits
groupby=[source, target] explicitly — the backend query builders resolve
grouping from groupby alone and don't read the source/target controls (only
the frontend buildQuery does), so without this the query groups by nothing and
collapses every edge into one aggregate row. Ordering is applied by the shared
query-dict builder via sort_by_metric (a top-level form_data['orderby'] is a
no-op on the MCP path). Adds the generate_chart.py docstring entry + the
get_chart_type_schema core list + get_chart_data category mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recommendation category map keys gauge as 'gauge_chart', so the earlier
apply's 'gauge' anchor silently no-op'd and sankey_v2 never got its category.
test_sankey_in_recommendation_category_map caught it. Add the entry so
get_chart_data categorizes sankey_v2 as 'sankey'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gkneighb
gkneighb force-pushed the feat/mcp-sankey-plugin branch from c47db17 to 6886650 Compare October 2, 2026 03:33
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 2, 2026
@bito-code-review

bito-code-review Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #aec2b0

Actionable Suggestions - 0
Additional Suggestions - 5
  • tests/unit_tests/mcp_service/chart/test_chart_utils.py - 2
    • Missing normalization assertions · Line 193-203
      The sibling `test_merge_pie_preserves_omitted_defaults` and `test_merge_bubble_preserves_omitted_defaults` assert canonical names (e.g. `config.dimension.name == "Product"`) after `DatasetValidator.normalize_column_names`; this sankey variant asserts nothing about normalization, so a regression in `SankeyPlugin.normalize_column_refs` (e.g. names left lowercase) passes silently here. Assert `config.source.name`/`config.metric.name` after the call.
    • Inline imports lack cycle justification · Line 183-184
      BITO rule 12745 requires module-level imports unless a documented circular dependency exists. `map_sankey_config` and `SankeyChartConfig` come from modules already imported at the top of this file (lines 26 and 47), so no cycle exists; add both names to the existing top-level import blocks instead of the inline imports at lines 183-184.
  • superset/mcp_service/chart/chart_utils.py - 1
    • Misleading comment refs builder · Line 1724-1727
      The comment names `_build_single_query_dict`, but the shared builder is `build_single_query_dict` (chart_helpers.py:729) — the underscore-prefixed name exists nowhere in the repo. It also claims top-level `form_data['orderby']` is ignored on the MCP path, yet `build_single_query_dict` copies `form_data['orderby']` into the query dict (chart_helpers.py:739-740). Please correct both so future ordering work starts from accurate facts.
  • tests/unit_tests/mcp_service/chart/test_sankey_chart.py - 1
    • Missing test docstring · Line 265-270
      All other tests in this new file carry docstrings; `test_get_chart_type_schema_includes_sankey` (lines 265-270) is the only one without one. BITO.md adaptive rule 12148 requires docstrings on all new test functions so test intent is visible to reviewers and maintainers. Add a one-line docstring describing the expected registry content.
  • superset/mcp_service/chart/plugins/sankey.py - 1
    • Replace Any with specific type · Line 82-82
      Replace `Any` with `SankeyChartConfig` in `extract_column_refs` to satisfy ANN401. Similar `Any` usages exist in `to_form_data`, `generate_name`, `resolve_viz_type`, and `normalize_column_refs`.
Review Details
  • Files reviewed - 9 · Commit Range: 2b30ecb..6886650
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/sankey.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_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_chart_utils.py
    • tests/unit_tests/mcp_service/chart/test_sankey_chart.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

@gkneighb

gkneighb commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

@rusackas Following up on the CI question from your sankey review — the rebase is done on all four and the picture turned out different from what either of us expected. One correction first: the byte budget isn't failing. All four now fail exactly one test, test_dispatchers_do_not_branch_on_registered_chart_types from #44746, and nothing else:

PR what the guard flags
#43567 funnel preview_utils: {'funnel': generate_funnel_vega_lite_preview}
#43570 heatmap chart_helpers: viz_type == 'heatmap_v2'
#43571 radar get_chart_data: _VIZ_CATEGORY → radar
#43573 sankey get_chart_data: _VIZ_CATEGORY → sankey_v2

The mechanism is worth stating plainly, because it isn't "these PRs add bad code." The guard flags dispatcher branches keyed on registered chart types. Registering a type is therefore enough to turn pre-existing code into a violation.

#43567 is the clean illustration. _FALLBACK_VEGA_RENDERERS = {"funnel": generate_funnel_vega_lite_preview} is master's line, added by #44746 itself at preview_utils.py:1382. It's legal today only because funnel isn't registered, so it isn't in _LEGACY_TYPE_BRANCHES. The moment #43567 registers the plugin, master's own dict becomes the violation. My diff doesn't touch that file.

#43570 is the mirror image: the flagged viz_type == 'heatmap_v2' is a line I wrote during this rebase, adopting your with_x_axis_column() helper to keep the heatmap x-axis reaching GROUP BY. Correct for the pre-#44746 shape, illegal by construction now.

So all four need the same thing — move the per-type behavior into the plugin hooks the new contract provides, so no shared dispatcher names the type. That's a real port, not a line edit, and I don't want to guess at your intent on two points:

  1. Was this anticipated? preview_utils on master already mentions funnel (3×), radar and sankey. If refactor(mcp): one plugin lifecycle contract and compact chart config schemas #44746 was written with these four in mind, you may already have a shape in mind for how they should land — I'd rather implement that than invent a parallel one.
  2. Who should do it? Happy to port all four onto the contract. But if it's cleaner for whoever holds refactor(mcp): one plugin lifecycle contract and compact chart config schemas #44746 to absorb them — four approved plugins is a decent forcing function for the hooks — that's fine by me and probably faster. Parking them until the contract settles is also a reasonable answer.

Separately, your non-blocking nit on this PR is valid: SankeyChartConfig has no record_implicit_metric_aggregate, so a bare metric on a text column fails at the database instead of returning the clean invalid_aggregation. I'll fold that in with whatever port shape you pick rather than pushing it standalone.

All four are rebased, mergeable, and green apart from that one test.

@gkneighb gkneighb mentioned this pull request Oct 7, 2026
1 of 9 tasks
@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires:rebase Requires rebasing on top of current master size/XL viz:charts:sankey Related to the Sankey chart

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants