Repository navigation
Conversation
Charts imported from a dashboard ZIP persisted with query_context = NULL, so
GET /api/v1/chart/{pk}/data/ returned HTTP 400 "Chart has no query context
saved. Please save the chart again." until each chart was manually re-opened
in Explore — infeasible for bulk imports (~100 charts).
import_chart now derives and persists a query_context from the chart's params
+ importer-resolved datasource when one is absent, via a new pure helper
build_query_context_config. Properties:
- Non-destructive/idempotent: an existing query_context is never overwritten.
- Authz/RLS-preserving: the datasource is taken from the importer-resolved id
only, never from params, so the synthesized context names the same real
datasource the read path authorizes.
- Honest-fail: non-derivable charts (no datasource, nothing to query, or a
datasource-less viz such as markup/handlebars/divider) are left NULL rather
than given a fabricated/invalid context.
- Resilient: a per-chart derivation error is logged and skipped, never
aborting the import bundle.
- The chart-data read path (charts/data/api.py) is unchanged.
Also logs a per-import summary of synthesized/preserved/non-derivable charts.
Tests: new unit suite for the helper (derivation, the non-derivable decision
table, datasource-from-argument, adhoc-filter translation, idempotency) plus
import round-trip and chart-data API coverage; the existing chart-data API
tests are kept unmodified as a no-regression guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Eugene Olkhovski <eugeneo@cloudgeometry.com>
… for apache#33615 Follow-up to the targeted import-time synthesis. For perfect fidelity across all chart types, run the REAL frontend `buildQuery` on the backend via an embedded V8 (py_mini_racer) instead of the approximate generic Python derivation — the form_data -> query_context mapping is per-plugin JS, so reusing it is the only way to match the frontend exactly (per the upstream discussion on this issue). - superset-frontend/src/backend-querycontext: a DOM-free entry that bundles each plugin's default `buildQuery` directly (generated REGISTRY covering 29 viz keys) and exposes `generateQueryContext(vizType, formData)`. Built with esbuild (`npm run build:backend-querycontext`); the bundle is a generated artifact (gitignored). - superset/commands/chart/query_context_generator.py: a lazy, thread-guarded MiniRacer singleton that loads the bundle (with minimal browser-global shims) and runs it. `generate()` returns None (never raises) when the engine/bundle is unavailable or the viz is unsupported, so callers fall back cleanly. - import_chart now tries the V8 generator first (authoritative) and falls back to the pure `build_query_context_config`. The datasource is still taken only from the importer-resolved id/type (authz/RLS preserved); synthesis stays non-destructive/idempotent and never aborts the bundle. - `superset charts backfill-query-context [--dry-run]`: re-encode charts that were imported before the fix and still have a NULL query_context. - Parity harness: `query_context_parity_test.py` asserts the backend engine output is byte-identical to the frontend `buildQuery` golden for every registered viz type (29/29). Shipping py_mini_racer as a runtime dependency plus the JS bundle is a packaging decision intended for upstream (SIP) discussion; the change degrades gracefully to the pure-Python synthesis when the engine is absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Eugene Olkhovski <eugeneo@cloudgeometry.com>
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The flagged issue is correct. The generator script currently only writes new files and does not clean up stale fixtures, which can lead to parity test failures when visualization types are removed or renamed. To resolve this, you should clear the target fixture directories before generating the new set. Here is a concise implementation to add to import { rmSync } from 'fs';
// ... existing setup ...
// Clear existing fixtures to prevent stale files
[fixtureDir1, fixtureDir2].forEach(dir => {
rmSync(dir, { recursive: true, force: true });
mkdirSync(dir, { recursive: true });
});
// ... proceed with generation ...I have checked the available PR context, and there are no other review comments to address. Would you like me to perform any other tasks related to this PR? superset-frontend/scripts/gen-qc-fixtures.mjs |
| const raw = generateQueryContext(vizType, JSON.stringify(formData)); | ||
| const result = JSON.parse(raw); |
There was a problem hiding this comment.
Suggestion: This test creates its expected output by invoking the same generateQueryContext implementation that the backend uses, then writes that output as the golden. Consequently, a wrong buildQuery mapping or incorrect query-context implementation can produce matching goldens and leave the test green; it only checks serialization/runtime consistency, not frontend/backend parity. The expected fixtures need to be produced by an independent frontend execution or independently maintained expected values. [incomplete implementation]
Severity Level: Minor 🧹
- ⚠️ Parity CI can miss incorrect registry mappings.
- ⚠️ Frontend/backend query-context divergence may reach imports.
- ⚠️ Golden files provide consistency, not independent correctness.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** superset-frontend/src/backend-querycontext/parity.test.ts
**Line:** 56:57
**Comment:**
*Incomplete Implementation: This test creates its expected output by invoking the same `generateQueryContext` implementation that the backend uses, then writes that output as the golden. Consequently, a wrong `buildQuery` mapping or incorrect query-context implementation can produce matching goldens and leave the test green; it only checks serialization/runtime consistency, not frontend/backend parity. The expected fixtures need to be produced by an independent frontend execution or independently maintained expected values.
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 fixThere was a problem hiding this comment.
Acknowledged, and it is noted in the test docstring: the JS golden guards runtime and serialization consistency, and the actual frontend/backend parity is enforced by the Python parity test that asserts the pure-Python fallback matches these goldens. Seeding goldens from an independent frontend execution is reasonable hardening, but I would prefer to track it separately. OK to keep the two-sided setup for this PR?
There was a problem hiding this comment.
✅ Customized review instruction saved!
Instruction:
For frontend/backend parity tests, do not require independently generated JavaScript goldens when a separate parity test validates the Python implementation against those goldens; consistency and cross-implementation validation may be split across tests.
Applied to:
**/test/****/tests/****/*test*.ts
💡 To manage or update this instruction, visit: CodeAnt AI Settings
| # else: unmappable SIMPLE filter — dropped, no crash (RISK-T05). | ||
| elif expression_type == "SQL": | ||
| sql_expression = adhoc_filter.get("sqlExpression") | ||
| if sql_expression: | ||
| clause = (adhoc_filter.get("clause") or "WHERE").upper() | ||
| if clause == "HAVING": | ||
| having_expressions.append(sql_expression) | ||
| else: | ||
| where_expressions.append(sql_expression) |
There was a problem hiding this comment.
Suggestion: Malformed or unsupported adhoc filters are silently omitted while a query context is still generated. This means a saved chart with an unmappable filter can be imported with a broader query that returns rows the chart's form data intended to exclude. Treat an unmappable filter as non-derivable, or preserve it in a form the query processor understands, rather than silently dropping it. [logic error]
Severity Level: Major ⚠️
- ⚠️ Imported charts can display unintended rows.
- ⚠️ Saved query context no longer matches chart filters.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** superset/commands/chart/query_context_builder.py
**Line:** 81:89
**Comment:**
*Logic Error: Malformed or unsupported adhoc filters are silently omitted while a query context is still generated. This means a saved chart with an unmappable filter can be imported with a broader query that returns rows the chart's form data intended to exclude. Treat an unmappable filter as non-derivable, or preserve it in a form the query processor understands, rather than silently dropping it.
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 fixThere was a problem hiding this comment.
Agreed—dropping an unmappable filter while still persisting a context turns an invalid imported chart into a broader query than its form data requested. Could the fallback return non-derivable when any filter cannot be preserved?
There was a problem hiding this comment.
Agreed. The fallback should be conservative: if any adhoc_filter cannot be translated, it must return None so the chart remains non-derivable rather than persisting a broader query.
A minimal fix is to validate the filters before calling the shared splitter:
def _translate_adhoc_filters(
adhoc_filters: list[Any] | None,
) -> tuple[list[dict[str, Any]], str, str] | None:
sanitized: list[dict[str, Any]] = []
for filter_ in adhoc_filters or []:
if not isinstance(filter_, dict):
return None
expression_type = filter_.get("expressionType")
if expression_type == "SIMPLE":
if not filter_.get("subject") or not filter_.get("operator"):
return None
elif expression_type == "SQL":
if not isinstance(filter_.get("sqlExpression"), str):
return None
else:
return None
sanitized.append(filter_)
form_data: dict[str, Any] = {"adhoc_filters": sanitized}
split_adhoc_filters_into_base_filters(form_data)
return (
form_data.get("filters") or [],
form_data.get("where") or "",
form_data.get("having") or "",
)Then propagate the result in build_query_context_config:
translated = _translate_adhoc_filters(params.get("adhoc_filters", []))
if translated is None:
return None
filters, where, having = translatedThe existing tests expecting malformed filters to be silently dropped should be updated to assert None; add coverage for unknown expression types and malformed SIMPLE/SQL filters. The V8 path remains authoritative, while the Python fallback now fails honestly whenever it cannot preserve the chart’s filters.
There was a problem hiding this comment.
Good point. My inclination is to fail closed: return non-derivable (None) when any filter cannot be preserved, since a 400 is more honest than a silently broadened result set. That does mean one malformed adhoc filter drops the whole synthesized context. If you agree I will implement it with a test that an unmappable WHERE yields None.
There was a problem hiding this comment.
Code Review Agent Run #c84a60
Actionable Suggestions - 6
-
superset/commands/chart/importers/v1/utils.py - 1
- TypeError on string params in query_context synthesis · Line 96-96
-
superset/cli/charts.py - 2
- Missing test coverage for CLI command · Line 34-142
- Catching generic Exception is blind · Line 34-142
-
superset/commands/chart/query_context_generator.py - 2
- Catch specific exceptions instead · Line 80-80
- Catch specific exceptions instead · Line 111-111
-
superset-frontend/scripts/gen-qc-registry.mjs - 1
- Regex excludes constructor-arg plugins · Line 41-43
Additional Suggestions - 6
-
superset/cli/charts.py - 1
-
Truthiness check for numeric id · Line 101-102Line 101 assigns `datasource_id` from `Column(Integer)` — a falsy zero would bypass the V8/generator attempt and fall through to `build_query_context_config`. While `datasource_id=0` is a rare edge case for saved charts, using explicit `None` check (`datasource_id is not None`) makes the guard unambiguous.
-
-
superset/commands/chart/importers/v1/utils.py - 2
-
Magic string in datasource_type default · Line 99-99Line 99 defaults `datasource_type` to `"table"` with no inline comment. This magic string controls which datasource type is synthesized when the imported config omits it. Callers and maintainers may not infer that `"table"` is the intentional supported-only value versus an accidental default. A brief comment noting the constraint improves maintainability.
-
Catching broad Exception instead of specific · Line 130-130The bare `except Exception:` catches all exceptions, including those that may not be appropriate to swallow silently. Consider catching specific exceptions that are expected from the query context generation logic.
-
-
superset-frontend/scripts/build-backend-querycontext.mjs - 1
-
Inconsistent Node.js ESM import style · Line 35-35The import at line 35 uses bare `'fs'` while other scripts in `scripts/` (gen-qc-fixtures.mjs, gen-qc-goldens.mjs) use the explicit `node:fs` protocol. Aligning import style improves consistency and follows the modern Node.js ESM convention.
-
-
superset-frontend/scripts/gen-qc-fixtures.mjs - 1
-
Dead fallback in VIZ_TYPES · Line 35-35The fallback `|| globalThis.SUPERSET_QC_VIZ_TYPES` will never be taken because `mod.exports.VIZ_TYPES` is always set by the bundled CJS code from `registry.generated.ts`. The fallback only makes sense if the bundle used default-only exports, which never happens here.
-
-
tests/integration_tests/charts/data/api_tests.py - 1
-
Mutable class attribute needs ClassVar annotation · Line 1055-1055Mutable class attribute `_GENDERS_PARAMS` at line 1055-1062 should be annotated with `typing.ClassVar` to indicate it's a class-level mutable default.
-
Filtered by Review Rules
Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.
-
superset-frontend/src/backend-querycontext/registry.generated.ts - 1
- Type safety: `any` erases buildQuery signatures · Line 42-42
Review Details
-
Files reviewed - 17 · Commit Range:
87463b5..407d0e1- superset-frontend/scripts/build-backend-querycontext.mjs
- superset-frontend/scripts/gen-qc-fixtures.mjs
- superset-frontend/scripts/gen-qc-goldens.mjs
- superset-frontend/scripts/gen-qc-registry.mjs
- superset-frontend/src/backend-querycontext/entry.ts
- superset-frontend/src/backend-querycontext/parity.test.ts
- superset-frontend/src/backend-querycontext/registry.generated.ts
- superset/cli/charts.py
- superset/commands/chart/importers/v1/__init__.py
- superset/commands/chart/importers/v1/utils.py
- superset/commands/chart/query_context_builder.py
- superset/commands/chart/query_context_generator.py
- tests/integration_tests/charts/commands_tests.py
- tests/integration_tests/charts/data/api_tests.py
- tests/unit_tests/charts/commands/importers/v1/import_test.py
- tests/unit_tests/charts/commands/importers/v1/query_context_builder_test.py
- tests/unit_tests/charts/commands/importers/v1/query_context_parity_test.py
-
Files skipped - 61
- .gitignore - Reason: Filter setting
- superset-frontend/package.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/big_number.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/big_number_total.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/box_plot.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/bubble_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_area.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_bar.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_line.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_scatter.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_smooth.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_step.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/funnel.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/gauge_chart.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/graph_chart.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/handlebars.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/heatmap_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/histogram_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/mixed_timeseries.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/pie.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/pivot_table_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/pop_kpi.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/radar.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/sankey_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/sunburst_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/table.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/tree_chart.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/treemap_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/waterfall.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/expected/word_cloud.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/big_number.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/big_number_total.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/box_plot.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/bubble_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_area.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_bar.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_line.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_scatter.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_smooth.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_step.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/funnel.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/gauge_chart.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/graph_chart.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/handlebars.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/heatmap_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/histogram_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/mixed_timeseries.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/pie.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/pivot_table_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/pop_kpi.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/radar.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/sankey_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/sunburst_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/table.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/tree_chart.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/treemap_v2.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/waterfall.json - Reason: Filter setting
- superset-frontend/src/backend-querycontext/__fixtures__/formdata/word_cloud.json - Reason: Filter setting
- superset/commands/chart/_bundles/README.md - Reason: Filter setting
-
Tools
- MyPy (Static Code Analysis) - ✔︎ Successful
- Astral Ruff (Static Code Analysis) - ✔︎ Successful
- Whispers (Secret Scanner) - ✔︎ Successful
- Detect-secrets (Secret Scanner) - ✔︎ Successful
- Eslint (Linter) - ✔︎ Successful
Bito Usage Guide
Commands
Type the following command in the pull request comment and save the comment.
-
/review- 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
| @charts.command("backfill-query-context") | ||
| @with_appcontext | ||
| @click.option( | ||
| "--dry-run", | ||
| is_flag=True, | ||
| default=False, | ||
| help="Report what would change without writing.", | ||
| ) | ||
| @click.option( | ||
| "--viz-type", | ||
| "viz_types", | ||
| multiple=True, | ||
| help="Restrict to these viz types (repeatable). Default: all.", | ||
| ) | ||
| @click.option( | ||
| "--batch-size", | ||
| type=int, | ||
| default=200, | ||
| show_default=True, | ||
| help="Commit every N updated charts.", | ||
| ) | ||
| def backfill_query_context( | ||
| dry_run: bool, viz_types: tuple[str, ...], batch_size: int | ||
| ) -> None: | ||
| """ | ||
| Backfill a synthesized ``query_context`` on saved charts that have none. | ||
|
|
||
| Repairs charts imported before the import-time synthesis landed (issue | ||
| #33615): each chart with ``query_context IS NULL`` gets a context derived | ||
| from its ``params`` + datasource — authoritatively via the frontend | ||
| ``buildQuery`` (V8) when available, else the pure-Python generic derivation. | ||
| Non-derivable charts are left untouched (never a fabricated context). | ||
| """ | ||
| # Imported lazily so the module imports cleanly without an app context. | ||
| from superset.commands.chart.query_context_builder import ( | ||
| build_query_context_config, | ||
| ) | ||
| from superset.commands.chart.query_context_generator import ( | ||
| get_query_context_generator, | ||
| ) | ||
| from superset.extensions import db | ||
| from superset.models.slice import Slice | ||
| from superset.utils import json | ||
|
|
||
| generator = get_query_context_generator() | ||
|
|
||
| # `enable_eagerloads(False)` is required for `yield_per`: Slice has eager | ||
| # (joined) collection relationships that otherwise raise | ||
| # "Can't use yield_per with eager loaders that require uniquing/buffering". | ||
| query = ( | ||
| db.session.query(Slice) | ||
| .filter(Slice.query_context.is_(None)) | ||
| .enable_eagerloads(False) | ||
| ) | ||
| if viz_types: | ||
| query = query.filter(Slice.viz_type.in_(viz_types)) | ||
|
|
||
| updated = 0 | ||
| non_derivable = 0 | ||
| errors = 0 | ||
| pending = 0 | ||
|
|
||
| for chart in query.yield_per(batch_size): | ||
| try: | ||
| params = json.loads(chart.params) if chart.params else {} | ||
| if not isinstance(params, dict): | ||
| params = {} | ||
| datasource_id = chart.datasource_id | ||
| datasource_type = chart.datasource_type or "table" | ||
|
|
||
| context = None | ||
| if datasource_id: | ||
| js_params = { | ||
| **params, | ||
| "datasource": f"{datasource_id}__{datasource_type}", | ||
| } | ||
| context = generator.generate(chart.viz_type, js_params) | ||
| if context is None: | ||
| context = build_query_context_config( | ||
| params, chart.viz_type, datasource_id, datasource_type | ||
| ) | ||
|
|
||
| if context is None: | ||
| non_derivable += 1 | ||
| continue | ||
|
|
||
| updated += 1 | ||
| if dry_run: | ||
| continue | ||
|
|
||
| chart.query_context = json.dumps(context) | ||
| pending += 1 | ||
| if pending >= batch_size: | ||
| db.session.commit() | ||
| pending = 0 | ||
| except Exception as ex: # pylint: disable=broad-except | ||
| errors += 1 | ||
| logger.warning( | ||
| "backfill-query-context: chart id=%s failed: %s", chart.id, ex | ||
| ) | ||
|
|
||
| if not dry_run and pending: | ||
| db.session.commit() | ||
|
|
||
| prefix = "[dry-run] would update" if dry_run else "updated" | ||
| click.echo( | ||
| f"backfill-query-context: {prefix} {updated}, " | ||
| f"non-derivable (left null) {non_derivable}, errors {errors}." | ||
| ) |
There was a problem hiding this comment.
The backfill-query-context command has zero test coverage — no unit or integration tests were found in the test suite. Other CLI commands in cli_tests.py use app.test_cli_runner() for coverage. A test should verify dry-run output, batch commits, and non-derivable chart handling.
Catching broad Exception may hide important errors. Catch specific exceptions or log and re-raise to avoid silent failures.
Code Review Run #c84a60
Should Bito avoid suggestions like this for future reviews? (Manage Rules)
- Yes, avoid them
There was a problem hiding this comment.
Fair. The backfill-query-context command has no tests yet; a proper one needs an app context and a DB session (existing charts with and without a query_context), so I would add it as an integration test rather than a unit test. Planning it as a follow-up unless you would rather it block here.
There was a problem hiding this comment.
Adding an integration test is a solid approach for verifying the CLI command, as it allows you to properly mock or set up the application context and database session. Since this command interacts directly with the database and relies on the app context, an integration test is more appropriate than a unit test to ensure the backfill-query-context logic behaves correctly in a real-world scenario. Proceeding with this as a follow-up is reasonable, provided it is tracked to ensure the coverage gap is addressed.
The master merge into this branch left tests/.../import_test.py scrambled (test bodies shuffled, undefined names admin/config/original_id) and a stray blank line in the v1 importers __init__.py. Reconstruct import_test.py from master's soft-delete suite plus the three query_context synthesis tests, and clear the remaining lint/type failures so CI is green: - ruff: TID251 (use superset.utils.json), E501, I001, and C901 — extract helpers _synthesize_query_context_if_absent (import_chart) and _derive_query_context (backfill CLI) to cut cyclomatic complexity. - pylint: annotate the two batched CLI commits with consider-using-transaction (streaming backfill, not a single unit of work). - mypy: guard the Optional SqlaTable datasource before .database. - oxlint (frontend): fix no-new-func, prefer-template, no-plusplus, no-unused-vars and jest consistent-test-it in the gen-qc scripts and parity.test.ts. - RAT license check: add Apache headers to the three gen-qc *.mjs scripts. - oxfmt: format the generated registry/entry and the query_context fixtures. - .gitignore: restore entries dropped by the merge and ignore the generated _bundles/*.js artifact (documented as not committed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review Agent Run #654874Actionable Suggestions - 0Additional Suggestions - 1
Filtered by Review RulesBito filtered these suggestions based on rules created automatically for your feedback. Manage rules.
Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #43303 +/- ##
==========================================
+ Coverage 65.02% 65.09% +0.07%
==========================================
Files 3039 3045 +6
Lines 198382 198865 +483
Branches 46213 46389 +176
==========================================
+ Hits 128992 129447 +455
- Misses 66489 66492 +3
- Partials 2901 2926 +25
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The query_context-synthesis rework left `_import` calling `import_chart` twice on the same config dict. The first call serializes `config["params"]` to a JSON string; the second call then passes that string to `filter_chart_annotations`, which does `params.get(...)` and raises AttributeError: 'str' object has no attribute 'get', surfacing as HTTP 500 on chart/dashboard import (test-postgres/-sqlite/-mysql). Collapse to a single call that passes `default_viewers`, deriving the per-chart synthesis counters from its result. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review Agent Run #c44232Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
Code Review Agent Run #0f7a9eActionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
…3615) A master merge added the echarts Candlestick chart's buildQuery, so registry.generated.ts drifted and lint-frontend's freshness check failed. Re-ran `npm run gen:backend-querycontext-registry`; the only change is the new `candlestick` mapping and the deterministic renumbering that follows it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review Agent Run #210cdeActionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
|
Running CI 🤞 Not sure if @sadpandajoe's comments are addressed or not. Please feel free to reply/resolve, but let us know if/when this is ready for another review pass, if CI passes. |
| if not metrics and not columns: | ||
| return None | ||
|
|
||
| translated = _translate_adhoc_filters(params.get("adhoc_filters", [])) |
There was a problem hiding this comment.
When V8 is unavailable, a chart with where: "region = 'EMEA'" persists extras.where: "", and an extra_filters region restriction is also dropped, so the saved-chart data endpoint returns a broader result set than the chart defines. Could the fallback preserve these supported filter sources, or leave the chart non-derivable when it cannot?
| # misclassified as non-derivable (#33615: Big Number left with a NULL context). | ||
| if not metrics and params.get("metric"): | ||
| metrics = [params["metric"]] | ||
| # `groupby` is the deprecated alias of `columns`. |
There was a problem hiding this comment.
With the optional V8 runtime absent, a pivot_table_v2 chart using groupbyRows and groupbyColumns but no generic columns/groupby is saved as a single aggregate with columns: [], losing both pivot axes. Could unsupported visualization-specific shapes remain non-derivable rather than persisting a context that returns the wrong data?
| } | ||
|
|
||
| return { | ||
| "datasource": {"id": datasource_id, "type": datasource_type}, |
There was a problem hiding this comment.
This fallback omits form_data, which QueryContextFactory reads to enable server pagination; a saved table with server_pagination: true is therefore reconstructed with pagination disabled and its requested row limit is clamped to the lower SQL_MAX_ROW cap. Could the persisted context retain the necessary form data with destination-local identity, and cover this through the query-context schema?
| // here instead of silently dropping the entry. | ||
| const EXPLICIT_BUILD_QUERY = { | ||
| chord: 'plugins/plugin-chart-chord/src/buildQuery.ts', | ||
| cartodiagram: 'plugins/plugin-chart-cartodiagram/src/plugin/buildQuery.ts', |
There was a problem hiding this comment.
Cartodiagram's builder calls getChartBuildQueryRegistry().get(selectedChart.viz_type), but this standalone bundle never registers the imported builders in that singleton, so selecting a Pie chart throws and returns __error__ despite this mapping. Could the bundle initialize the nested-builder registry and test Cartodiagram through the generated entry without test-side registration?
rusackas
left a comment
There was a problem hiding this comment.
Checking back in since my last comment, since you've now gotten a fresh round from @sadpandajoe (posted this morning, plus one from the 29th) that's still open. I traced two of these myself against the current head rather than just relaying them:
_derive_orderby really does fall back to the raw sort_by_metric boolean as the sort column when timeseries_limit_metric is absent, so a synthesized context can end up ordering by true instead of the chart's actual metric.
And the pure-Python fallback builder (query_context_builder.py) never reads extra_filters at all, only adhoc_filters. The fail-closed logic in _translate_adhoc_filters is good, but it doesn't cover this case, so a chart scoped by a dashboard-level extra_filters restriction gets a broader, unscoped context when the optional V8 runtime isn't installed. That's the same "query a broader row set than the chart defines" risk the fail-closed design elsewhere in this file is explicitly trying to avoid.
The other four open threads (CLI backfill not stripping slice_id/not propagating cache_timeout, the pivot_table fallback losing both axes, server-pagination tables losing their row cap, Cartodiagram's standalone bundle not registering builders) all read as real to me too, though I didn't trace each one personally the way I did the two above.
Separate from the line-level stuff: the PR's own description flags the py_mini_racer/embedded-V8 dependency and bundle as maybe warranting a SIP rather than landing in this PR. I don't think that's something to resolve in review comments, that's a call for the list, not a thread. Worth raising there before this goes further regardless of how the correctness findings shake out.
Holding off on approving until the fallback-path bugs get a look, since those are the ones with real data-correctness consequences rather than just missing coverage.
|
Thanks @eugeneo17, CI looks green now. Before another pass, the adhoc-filter fail-closed change and the I'd also lean toward splitting the V8 bundle and |
| filters, where, having = translated | ||
|
|
||
| query_object = { | ||
| "time_range": params.get("time_range", " : "), |
There was a problem hiding this comment.
For a chart with an adhoc TEMPORAL_RANGE filter and no top-level time_range (including charts migrated by _migrate_temporal_filter), this default makes QueryContextFactory overwrite the filter’s January-only comparator with " : ", querying all dates instead. Could an absent time_range remain null or omitted so the factory can infer it from the temporal filter?
| # (DEV_MODE=false); without it the chart importer falls back to the pure-Python | ||
| # generic derivation. Must come after `COPY superset superset` so it is not | ||
| # clobbered by the source tree (which never contains the generated bundle). | ||
| COPY --from=superset-node /app/superset/commands/chart/_bundles/query_context_bundle.j[s] superset/commands/chart/_bundles/query_context_bundle.js |
There was a problem hiding this comment.
With DEV_MODE=true, the node stage skips the build and never creates /app/superset/commands/chart/_bundles, so this COPY fails with "lstat .../_bundles: no such file or directory"; the optional glob only tolerates a missing file when its parent exists. Could that directory be created in the node stage even when frontend compilation is skipped?
| sort_metric = params.get("timeseries_limit_metric") or params.get("sort_by_metric") | ||
| if sort_metric: | ||
| return [[sort_metric, order_asc]] | ||
| return [] |
There was a problem hiding this comment.
A raw table with order_by_cols: ['["revenue", false]'] and row_limit: 1 gets orderby: [], so its saved-chart data read returns an arbitrary row instead of the highest-revenue row when V8 is unavailable. Could this preserve the raw-table ordering, as orderby_from_form_data and the frontend do?
| "columns": columns, | ||
| "metrics": metrics, | ||
| "orderby": _derive_orderby(params), | ||
| "annotation_layers": [], |
There was a problem hiding this comment.
This discards annotation layers that the importer has already resolved to destination-local IDs, so a chart with a native annotation layer gets an empty annotation_data response when V8 is unavailable. Could the fallback preserve the resolved layers, or leave such a chart non-derivable?
| "row_limit": params.get("row_limit"), | ||
| "timeseries_limit": 0, | ||
| "order_desc": params.get("order_desc", True), | ||
| "url_params": {}, |
There was a problem hiding this comment.
A chart with url_params: {"region": "EMEA"} over a virtual dataset using url_param("region", "APAC") loses its stored default here, so saved-chart reads query APAC instead of EMEA when V8 is unavailable. Could the synthesized query retain the chart’s URL-parameter mapping?
| @@ -538,71 +710,3 @@ def test_import_tag_logic_for_charts(session_with_schema: Session): | |||
| .all() | |||
| ) | |||
| assert len(associated_tags) == 0 | |||
There was a problem hiding this comment.
Removing test_import_tag_savepoint_keeps_session_usable drops the regression for a tag association failing during SAVEPOINT flush while a second tag still imports; the remaining test only covers successful imports and the disabled flag. Could that regression remain, including its assertions that only the successful tag is reported and the session is still queryable?
| if not metrics and params.get("metric"): | ||
| metrics = [params["metric"]] | ||
| # `groupby` is the deprecated alias of `columns`. | ||
| columns = params.get("columns") or params.get("groupby") or [] |
There was a problem hiding this comment.
Without the V8 bundle, any chart that has a metric or groupby gets a context saved, but that context ignores viz-specific semantics. For example, the bundled Big_Number_with_Trendline.yaml has x_axis: order_date and metric: count. The fallback ignores x_axis and never sets is_timeseries, so the saved-chart data read returns a single aggregate instead of the monthly trendline series. A big_number with aggregation: LAST_VALUE returns the period total instead of the last bucket. Pie with threshold_for_other is missing the contribution post-processing, so the "Other" grouping never happens. Because the context is persisted, installing the V8 extra later does not repair it: both synthesis and the backfill only act on a missing context. Could the fallback return non-derivable for viz types or shapes whose semantics it cannot reproduce (time axis, aggregation, post-processing), and keep the generic mapping for the shapes it does cover?
| # (DEV_MODE=false); without it the chart importer falls back to the pure-Python | ||
| # generic derivation. Must come after `COPY superset superset` so it is not | ||
| # clobbered by the source tree (which never contains the generated bundle). | ||
| COPY --from=superset-node /app/superset/commands/chart/_bundles/query_context_bundle.j[s] superset/commands/chart/_bundles/query_context_bundle.js |
There was a problem hiding this comment.
This copies the V8 bundle into the image, but nothing in the image installs the querycontext extra: the lean stage runs uv pip install -e . --no-deps and the default stage installs only .[postgres,mysql,fastmcp]. _ensure_ctx() therefore fails its py_mini_racer import in every published image, so the bundle is never used and imports silently use the approximate Python fallback. Is the intent to also install .[querycontext] in the image, or should the bundle copy be dropped until the runtime dependency ships?
| } | ||
| return acc; | ||
| } | ||
| const files = walk(PLUGINS); |
There was a problem hiding this comment.
Discovery only walks plugins/, but TimeTable is registered in MainPreset with its builder under src/visualizations/TimeTable/config/buildQuery.ts, so time_table never appears in the generated registry even with V8 available. Importing a TimeTable chart with metrics: ["count"] and a daily granularity_sqla falls through to the Python fallback, which omits is_timeseries, so the saved-chart read returns an aggregate with no time series. Could the generator also scan src/visualizations/ (or the registry check fail when a MainPreset plugin with a buildQuery has no entry)?
| "row_limit": 100, | ||
| "series_columns": ["gender"], | ||
| "series_limit": 0, | ||
| "order_desc": true, |
There was a problem hiding this comment.
This golden has no group_others_when_limit_reached, but buildQueryObject.ts always emits group_others_when_limit_reached: false, and none of the goldens contain it. parity.test.ts rewrites these files on every run rather than comparing them, and the Python parity test is skipped in CI (no querycontext extra, no bundle). So the committed goldens are already stale and nothing detects it; once someone enables the Python parity test, actual == expected fails for word_cloud. Could the goldens be regenerated at this head and the Python parity test run in a CI job that has the bundle and extra installed?
| try: | ||
| # --- RED anchor: produced payload constructs a real QueryContext --- | ||
| QueryContextFactory().create(**payload) | ||
| except Exception as exc: # pragma: no cover - environment-dependent |
There was a problem hiding this comment.
The broad except Exception turns any failure of QueryContextFactory().create(**payload) into pytest.skip. The test uses datasource id 1 with no resolvable datasource, so a real payload-shape error (a TypeError from an unexpected kwarg, for instance) skips instead of failing, and the "real end-to-end validity check" can never go red. Could the test create a datasource with the unit-test session fixture and let construction errors fail, asserting the resolved datasource and the query fields?
| # synthesized (#33615 review). | ||
| if had_query_context.get(str(config["uuid"])): | ||
| n_preserved += 1 | ||
| elif config.get("query_context"): |
There was a problem hiding this comment.
import_chart() rebinds its local config to the deep-copied result of migrate_chart() and sets query_context on that copy, but import_charts() yields the caller's original config. So config.get("query_context") here is falsy for every chart that did not already have one, and the summary logs successfully synthesized charts as non-derivable (0 queryable, N non-derivable). Could the count be based on the persisted chart.query_context instead?
| } | ||
|
|
||
| @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") | ||
| def test_synthesized_query_context_returns_data(self): |
There was a problem hiding this comment.
These four new read-path tests sit inside TestGetChartDataApi, which is still decorated with the class-level @pytest.mark.skip (line 1212). None of them run, even with a configured database, so a synthesized query_context that the GET /data/ endpoint rejects with a 400 would still pass the importer persistence assertions. Could they move to a class or module that actually executes, so the 200-with-rows and denies-unauthorized-datasource assertions are exercised?
| return JSON.stringify({ __unsupported__: true, viz_type: vizType }); | ||
| } | ||
| const formData = JSON.parse(formDataJson); | ||
| const queryContext = fn(formData); |
There was a problem hiding this comment.
The stored params go straight into the plugin buildQuery, without the defaults Explore applies to form data first. For a box_plot chart whose params have metrics and dimensions but no whiskerOptions, boxplotOperator returns undefined, so the persisted query_context has post_processing: [] (the committed expected/box_plot.json golden shows the same). The chart would then return raw rows instead of box statistics on its first /data/ read. Should defaults be merged into the form data here before calling the builder?
| const require = createRequire(import.meta.url); | ||
| const FEX = 'src/backend-querycontext/__fixtures__'; | ||
|
|
||
| const res = await build({ |
There was a problem hiding this comment.
This esbuild call has no loader map, but build-backend-querycontext.mjs needed .css/.svg/font/image entries mapped to empty because the entry.ts barrel imports transitively pull in theme and UI assets. As written, running node scripts/gen-qc-goldens.mjs (and gen-qc-fixtures.mjs:31, which has the same config) should fail while bundling, so the goldens and fixtures can't be refreshed when a buildQuery changes. Could these scripts share the production script's loader config?
| ) | ||
| # --- RED anchor: derivable → context; non-derivable → NULL (FR-003) --- | ||
| assert derivable_chart.query_context is not None | ||
| assert markup_chart.query_context is None |
There was a problem hiding this comment.
The markup chart simply yields no derivation (it returns None), so this passes even if the per-chart except in _synthesize_query_context_if_absent is removed. The bundle-isolation guarantee (one chart's synthesis raising must not abort the import) is never exercised. Could this add a chart whose synthesis actually raises, for example adhoc_filters: 123 on a big_number_total, and assert the import still succeeds, both charts persist, and only the failing one keeps a NULL query_context?
| query_context = json.loads(chart.query_context) | ||
| # --- RED anchor: datasource taken from resolved id, not params (RISK-T02) --- | ||
| assert query_context["datasource"] == {"id": 1, "type": "table"} | ||
| assert query_context["queries"][0]["metrics"] == ["count"] |
There was a problem hiding this comment.
This test only checks queries[0] against a chart that falls back to the Python derivation when the V8 runtime isn't installed, and the other synthesis tests force the generator to return None. No import test has the generator return a real payload, so an importer that rewrote or dropped authoritative output (for example a mixed_timeseries with two queries) would still pass. Could one test mock generate() to return a two-query context and assert import_chart persists it unchanged without calling the fallback?
The backfill_po.py run re-flagged the already-confirmed "Markup type" translation as a machine-translated "#, fuzzy" guess in fi/ko/th, which downgraded a confirmed translation and tripped check_translation_regression (translated -> fuzzy). The msgstr is unchanged from master; only the stray backfill comment and fuzzy flag are removed, restoring the entry to its confirmed state and clearing the babel-extract regression check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| } | ||
| context = generator.generate(chart.viz_type, js_params) | ||
| if context is None: | ||
| context = build_query_context_config( |
There was a problem hiding this comment.
The import path re-injects the chart's cache_timeout as custom_cache_timeout (_synthesize_query_context_if_absent), but this backfill path doesn't. A chart with cache_timeout=-1 (caching disabled) on a dataset with a positive timeout gets a backfilled context with no override, and the saved-chart data read then falls back to the datasource timeout and starts caching it. Should the backfill apply the same cache-timeout carry-over, ideally via a shared helper so the two paths can't drift?
| const removed = []; | ||
| for (const dir of [`${FEX}/formdata`, `${FEX}/expected`]) { | ||
| for (const f of readdirSync(dir)) { | ||
| if (f.endsWith('.json') && !keep.has(f)) { |
There was a problem hiding this comment.
This reconcile deletes the fixtures of every viz type that isn't in covered, and covered excludes anything whose builder now throws or returns __error__. If a still-registered builder such as table regresses and returns an error, regenerating removes its formdata/expected files, and the parity tests (which iterate the remaining files) pass without it. Should pruning be limited to viz types no longer in the registry, with a still-registered type that stops producing a context failing the generator?
| ) as get_generator: | ||
| get_generator.return_value.generate.return_value = None | ||
| runner = current_app.test_cli_runner() | ||
| response = runner.invoke(superset.cli.charts.backfill_query_context, ()) |
There was a problem hiding this comment.
This only exercises a single eligible chart with the default batch size, so the loop runs exactly one batch. The id-snapshot paging and per-batch commit that replaced the yield_per streaming (which broke on PostgreSQL once a backfill spanned more than one batch) aren't covered. Could the test create three charts with a NULL context, invoke the command with --batch-size 1, and assert all three end up with a persisted context?
Resolve conflicts in fi/ko/th messages.po: keep master's new backfill
entry ("Marks charts created through the MCP geographic contract ...")
and the now-confirmed (non-fuzzy) "Markup type" translation. Both sides
agree "Markup type" is no longer fuzzy, so no translation regression.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks @rusackas for the review, and good to hear CI is green. All three threads are valid and I will close them here:
On splitting out V8 and mini-racer: reasonable idea, but I would keep it in this PR for now, because:
Proposal: I resolve the three correctn PR unified for this pass. If you stillprefer the split, happy to discuss. Thanks again. |
| return [] | ||
|
|
||
|
|
||
| def build_query_context_config( |
There was a problem hiding this comment.
This is a second Python form-data-to-query mapping, and it drops roles that superset.common.form_data_query_context.build_query_context_from_form_data already handles. For example, a World Map chart with entity: country, metric: count, secondary_metric: revenue gets columns=[] and metrics=[count] here, so the backfilled/imported chart returns a single aggregate row instead of per-country rows with both metrics (that viz isn't covered by the V8 path, so this fallback is what runs). Could this reuse or extend the shared helper instead of maintaining another mapping?
| return None | ||
| if not isinstance(result, dict): | ||
| return None | ||
| if any(sentinel in result for sentinel in _FALLBACK_SENTINELS): |
There was a problem hiding this comment.
The sentinel guard here is the only thing keeping {"__unsupported__": true} / {"__error__": ...} from being persisted as a non-NULL query_context, but every importer test mocks generate() to return None, so the guard is never exercised. If it were removed, the tests would stay green while charts got an invalid context that 400s on the data endpoint and is skipped by the backfill (it only targets NULL). Could you add a unit test where the mocked _ctx.call returns each sentinel and asserts generate() returns None (and import falls back to the Python derivation)?
SUMMARY
Fixes #33615.
Charts imported as part of a dashboard ZIP are persisted with
Slice.query_context = NULL— the v1 importer never generates one. The ChartData read path requires it, so the first
GET /api/v1/chart/{pk}/data/returns400 "Chart has no query context saved. Please save the chart again."until eachchart is manually re-opened and saved in Explore. That workaround does not scale
(the reporter has ~100 imported charts).
Root cause:
import_chart(superset/commands/chart/importers/v1/utils.py) copiesthe exported chart config verbatim;
migrate_chartonly rewrites an existingquery_contextand never synthesizes one,aquery_contextyields aNULLcolumn, whichChartDataRestApi.get_data(
superset/charts/data/api.py) rejects with a 400.Fix.
import_chartnow derives and persists aquery_contextwhen the importedchart lacks one. Design decisions:
query_contextis neveroverwritten (re-importing is safe).
only from the importer-resolved datasource id/type, never from the chart
params, so it names the same real datasource the read path authorizes (noRLS/authz bypass).
query, or a datasource-less viz such as
markup/handlebars/divider) are leftNULLrather than given a fabricated/inthe import bundle.
A
superset charts backfill-query-context es charts that were imported before this fix and still have aNULLquery_context`.**Design note / open question for maintainontext
mapping is per-plugin frontend logic (buildQuery.ts), so a pure-backend derivation is only approximate for complex viz types. This branch also includes an optional full-fidelity path that runs the the backend via an embedded V8 (py_mini_racer) over a generated bundle, with byte-for-byte parity across 29 viz types;import_chart` uses it when available and falls back tothe pure Python derivation otherwise. Shiptime
dependency + the JS bundle is a packaging decision that likely warrants a SIP — I'm
happy to split it into a separate PR/SIP and keep this PR to the dependency-free
synthesis if preferred.
BEFORE/AFTER SCREENSHOTS OR ANIMATED G
API behavior (no UI change):
GET /api/v1/chart/{pk}/data/→400 {"message": "Chart has no query context saved. Please save the chart again."}with no manual re-save.
TESTING INSTRUCTIONS
New/updated tests: