You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/09/23 01:04:35 UTC
(superset) 02/11: feat(charts): full-fidelity query_context via backend buildQuery (V8) for #33615
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch change/33615-chart-data-query-context
in repository https://gitbox.apache.org/repos/asf/superset.git
commit ee4f630de5968ae3550d6a5b499bb9219c11eeaf
Author: Eugene Olkhovski <eu...@cloudgeometry.com>
AuthorDate: Tue Aug 18 07:14:28 2026 -0400
feat(charts): full-fidelity query_context via backend buildQuery (V8) for #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 <no...@anthropic.com>
Signed-off-by: Eugene Olkhovski <eu...@cloudgeometry.com>
---
.gitignore | 4 +
superset-frontend/package.json | 1 +
.../scripts/build-backend-querycontext.mjs | 72 ++++++++
superset-frontend/scripts/gen-qc-fixtures.mjs | 108 ++++++++++++
superset-frontend/scripts/gen-qc-goldens.mjs | 51 ++++++
superset-frontend/scripts/gen-qc-registry.mjs | 187 +++++++++++++++++++++
.../__fixtures__/expected/big_number.json | 164 ++++++++++++++++++
.../__fixtures__/expected/big_number_total.json | 85 ++++++++++
.../__fixtures__/expected/box_plot.json | 86 ++++++++++
.../__fixtures__/expected/bubble_v2.json | 85 ++++++++++
.../__fixtures__/expected/echarts_area.json | 120 +++++++++++++
.../__fixtures__/expected/echarts_timeseries.json | 120 +++++++++++++
.../expected/echarts_timeseries_bar.json | 120 +++++++++++++
.../expected/echarts_timeseries_line.json | 120 +++++++++++++
.../expected/echarts_timeseries_scatter.json | 120 +++++++++++++
.../expected/echarts_timeseries_smooth.json | 120 +++++++++++++
.../expected/echarts_timeseries_step.json | 120 +++++++++++++
.../__fixtures__/expected/funnel.json | 85 ++++++++++
.../__fixtures__/expected/gauge_chart.json | 85 ++++++++++
.../__fixtures__/expected/graph_chart.json | 85 ++++++++++
.../__fixtures__/expected/handlebars.json | 91 ++++++++++
.../__fixtures__/expected/heatmap_v2.json | 100 +++++++++++
.../__fixtures__/expected/histogram_v2.json | 94 +++++++++++
.../__fixtures__/expected/mixed_timeseries.json | 174 +++++++++++++++++++
.../__fixtures__/expected/pie.json | 98 +++++++++++
.../__fixtures__/expected/pivot_table_v2.json | 88 ++++++++++
.../__fixtures__/expected/pop_kpi.json | 87 ++++++++++
.../__fixtures__/expected/radar.json | 91 ++++++++++
.../__fixtures__/expected/sankey_v2.json | 89 ++++++++++
.../__fixtures__/expected/sunburst_v2.json | 85 ++++++++++
.../__fixtures__/expected/table.json | 89 ++++++++++
.../__fixtures__/expected/tree_chart.json | 85 ++++++++++
.../__fixtures__/expected/treemap_v2.json | 85 ++++++++++
.../__fixtures__/expected/waterfall.json | 101 +++++++++++
.../__fixtures__/expected/word_cloud.json | 85 ++++++++++
.../__fixtures__/formdata/big_number.json | 46 +++++
.../__fixtures__/formdata/big_number_total.json | 46 +++++
.../__fixtures__/formdata/box_plot.json | 46 +++++
.../__fixtures__/formdata/bubble_v2.json | 46 +++++
.../__fixtures__/formdata/echarts_area.json | 46 +++++
.../__fixtures__/formdata/echarts_timeseries.json | 46 +++++
.../formdata/echarts_timeseries_bar.json | 46 +++++
.../formdata/echarts_timeseries_line.json | 46 +++++
.../formdata/echarts_timeseries_scatter.json | 46 +++++
.../formdata/echarts_timeseries_smooth.json | 46 +++++
.../formdata/echarts_timeseries_step.json | 46 +++++
.../__fixtures__/formdata/funnel.json | 46 +++++
.../__fixtures__/formdata/gauge_chart.json | 46 +++++
.../__fixtures__/formdata/graph_chart.json | 46 +++++
.../__fixtures__/formdata/handlebars.json | 46 +++++
.../__fixtures__/formdata/heatmap_v2.json | 46 +++++
.../__fixtures__/formdata/histogram_v2.json | 46 +++++
.../__fixtures__/formdata/mixed_timeseries.json | 46 +++++
.../__fixtures__/formdata/pie.json | 46 +++++
.../__fixtures__/formdata/pivot_table_v2.json | 46 +++++
.../__fixtures__/formdata/pop_kpi.json | 46 +++++
.../__fixtures__/formdata/radar.json | 46 +++++
.../__fixtures__/formdata/sankey_v2.json | 46 +++++
.../__fixtures__/formdata/sunburst_v2.json | 46 +++++
.../__fixtures__/formdata/table.json | 46 +++++
.../__fixtures__/formdata/tree_chart.json | 46 +++++
.../__fixtures__/formdata/treemap_v2.json | 46 +++++
.../__fixtures__/formdata/waterfall.json | 46 +++++
.../__fixtures__/formdata/word_cloud.json | 46 +++++
.../src/backend-querycontext/entry.ts | 76 +++++++++
.../src/backend-querycontext/parity.test.ts | 75 +++++++++
.../src/backend-querycontext/registry.generated.ts | 74 ++++++++
superset/cli/charts.py | 142 ++++++++++++++++
superset/commands/chart/_bundles/README.md | 39 +++++
superset/commands/chart/importers/v1/utils.py | 43 ++++-
superset/commands/chart/query_context_generator.py | 176 +++++++++++++++++++
tests/integration_tests/charts/commands_tests.py | 16 +-
tests/integration_tests/charts/data/api_tests.py | 20 ++-
.../importers/v1/query_context_parity_test.py | 100 +++++++++++
74 files changed, 5457 insertions(+), 18 deletions(-)
diff --git a/.gitignore b/.gitignore
index 94b6b7c0f7a..69ed5344cb0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -143,3 +143,7 @@ PROJECT.md
oxc-custom-build/
*.code-workspace
*.duckdb
+
+# Generated backend query-context bundle (#33615) — built via
+# `npm run build:backend-querycontext`, never committed.
+superset/commands/chart/_bundles/query_context_bundle.js
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 18217b19199..827d8a179ab 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -40,6 +40,7 @@
"scripts": {
"_format": "oxfmt './{src,spec,cypress-base,plugins,packages,.storybook}/**/*{.js,.jsx,.ts,.tsx,.css,.scss,.sass,.json}'",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --color --mode production",
+ "build:backend-querycontext": "node scripts/build-backend-querycontext.mjs",
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --color",
"build-instrumented": "cross-env NODE_ENV=production BABEL_ENV=instrumented webpack --mode=production --color",
"build-storybook": "storybook build",
diff --git a/superset-frontend/scripts/build-backend-querycontext.mjs b/superset-frontend/scripts/build-backend-querycontext.mjs
new file mode 100644
index 00000000000..67a3adba680
--- /dev/null
+++ b/superset-frontend/scripts/build-backend-querycontext.mjs
@@ -0,0 +1,72 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Bundle the backend query-context entry (superset #33615) into a single,
+ * self-contained IIFE that the Python backend evaluates in V8 (py_mini_racer)
+ * to run the real frontend `buildQuery`. Output is a build artifact consumed by
+ * `superset/commands/chart/query_context_generator.py`.
+ *
+ * Run: `npm run build:backend-querycontext` (from superset-frontend/).
+ *
+ * NOTE: the bundle targets `browser`, so it may reference browser globals
+ * (self/window/navigator) at load. Do NOT add DOM shims here — the Python
+ * runner injects the minimal globals V8 needs before evaluating the bundle.
+ */
+import { build } from 'esbuild';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { mkdirSync } from 'fs';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const repoRoot = path.resolve(__dirname, '..', '..');
+const entry = path.resolve(__dirname, '..', 'src', 'backend-querycontext', 'entry.ts');
+const outfile = path.resolve(
+ repoRoot,
+ 'superset',
+ 'commands',
+ 'chart',
+ '_bundles',
+ 'query_context_bundle.js',
+);
+
+mkdirSync(path.dirname(outfile), { recursive: true });
+
+build({
+ entryPoints: [entry],
+ outfile,
+ bundle: true,
+ format: 'iife',
+ globalName: 'SupersetQC',
+ platform: 'browser',
+ target: 'es2020',
+ legalComments: 'none',
+ logLevel: 'info',
+ // esbuild infers the `ts` loader from the .ts extension; keep JSX off (these
+ // buildQuery modules are pure TS, no React).
+})
+ .then(() => {
+ // eslint-disable-next-line no-console
+ console.log(`[backend-querycontext] bundled -> ${outfile}`);
+ })
+ .catch((err) => {
+ // eslint-disable-next-line no-console
+ console.error('[backend-querycontext] build failed:', err);
+ process.exit(1);
+ });
diff --git a/superset-frontend/scripts/gen-qc-fixtures.mjs b/superset-frontend/scripts/gen-qc-fixtures.mjs
new file mode 100644
index 00000000000..654c472aeb6
--- /dev/null
+++ b/superset-frontend/scripts/gen-qc-fixtures.mjs
@@ -0,0 +1,108 @@
+// Generates a minimal-but-valid form_data fixture + frontend golden for every viz
+// type in the generated REGISTRY, by actually running each plugin's buildQuery under
+// Node's V8. A viz is COVERED if its builder returns a real query_context on the base
+// form_data; otherwise it's honestly SKIPPED (reason recorded) — never faked.
+import { build } from 'esbuild';
+import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
+import { createRequire } from 'node:module';
+
+const require = createRequire(import.meta.url);
+const FEX = 'src/backend-querycontext/__fixtures__';
+
+const res = await build({
+ entryPoints: ['src/backend-querycontext/entry.ts'],
+ bundle: true,
+ format: 'cjs',
+ platform: 'node',
+ target: 'es2020',
+ write: false,
+ legalComments: 'none',
+});
+const shim = (k, v) => {
+ try {
+ if (globalThis[k] === undefined) globalThis[k] = v;
+ } catch {
+ /* read-only builtin */
+ }
+};
+shim('self', globalThis);
+shim('window', globalThis);
+shim('navigator', { userAgent: 'superset-fixtures-node' });
+shim('document', {});
+const mod = { exports: {} };
+new Function('module', 'exports', 'require', res.outputFiles[0].text)(mod, mod.exports, require);
+const gen = mod.exports.generateQueryContext;
+const VIZ_TYPES = mod.exports.VIZ_TYPES || globalThis.SUPERSET_QC_VIZ_TYPES;
+
+// A deliberately broad base form_data — each plugin's buildQuery reads the fields it
+// needs and ignores the rest. Family-specific fields are all populated so most
+// builders find what they require without per-viz hand-tuning.
+const baseFor = (viz) => ({
+ datasource: '1__table',
+ viz_type: viz,
+ metric: 'count',
+ metrics: ['count'],
+ groupby: ['gender'],
+ columns: ['gender'],
+ all_columns: ['gender', 'name'],
+ entity: 'gender',
+ series: 'gender',
+ series_columns: ['gender'],
+ x_axis: 'ds',
+ granularity_sqla: 'ds',
+ time_grain_sqla: 'P1D',
+ time_range: 'No filter',
+ row_limit: 100,
+ adhoc_filters: [],
+ // graph/sankey/tree
+ source: 'gender',
+ target: 'name',
+ source_category: 'gender',
+ target_category: 'name',
+ // heatmap
+ x_axis_column: 'gender',
+ y_axis: 'name',
+ // bubble
+ x: 'count',
+ y: 'count',
+ size: 'count',
+ // histogram
+ column: 'num',
+ // mixed timeseries second query
+ metrics_b: ['count'],
+ groupby_b: ['gender'],
+ adhoc_filters_b: [],
+});
+
+mkdirSync(`${FEX}/formdata`, { recursive: true });
+mkdirSync(`${FEX}/expected`, { recursive: true });
+
+const covered = [];
+const skipped = [];
+for (const viz of VIZ_TYPES) {
+ const fd = baseFor(viz);
+ let out;
+ try {
+ out = JSON.parse(gen(viz, JSON.stringify(fd)));
+ } catch (e) {
+ skipped.push([viz, `threw: ${String(e).slice(0, 120)}`]);
+ continue;
+ }
+ if (!out || out.__unsupported__ || out.__error__) {
+ skipped.push([viz, out && out.__error__ ? `builder error: ${String(out.__error__).slice(0, 140)}` : 'unsupported']);
+ continue;
+ }
+ if (!Array.isArray(out.queries) || out.queries.length === 0) {
+ skipped.push([viz, 'no queries produced']);
+ continue;
+ }
+ writeFileSync(`${FEX}/formdata/${viz}.json`, JSON.stringify(fd, null, 2) + '\n');
+ writeFileSync(`${FEX}/expected/${viz}.json`, JSON.stringify(out, null, 2) + '\n');
+ covered.push(viz);
+}
+
+console.log(`REGISTRY viz types: ${VIZ_TYPES.length}`);
+console.log(`COVERED (fixture+golden written): ${covered.length}`);
+console.log(' ' + covered.join(', '));
+console.log(`SKIPPED (base form_data insufficient): ${skipped.length}`);
+for (const [v, r] of skipped) console.log(` - ${v}: ${r}`);
diff --git a/superset-frontend/scripts/gen-qc-goldens.mjs b/superset-frontend/scripts/gen-qc-goldens.mjs
new file mode 100644
index 00000000000..393470c6f45
--- /dev/null
+++ b/superset-frontend/scripts/gen-qc-goldens.mjs
@@ -0,0 +1,51 @@
+// Records frontend query_context goldens by running the SAME buildQuery entry
+// under Node's V8 (independent of the backend mini-racer runtime). Pure buildQuery
+// logic → identical output to the browser. Writes __fixtures__/expected/<stem>.json.
+import { build } from 'esbuild';
+import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
+import { createRequire } from 'node:module';
+
+const require = createRequire(import.meta.url);
+const FEX = 'src/backend-querycontext/__fixtures__';
+
+const res = await build({
+ entryPoints: ['src/backend-querycontext/entry.ts'],
+ bundle: true,
+ format: 'cjs',
+ platform: 'node',
+ target: 'es2020',
+ write: false,
+ legalComments: 'none',
+});
+const code = res.outputFiles[0].text;
+// Same minimal browser-global shims the backend mini-racer runner injects — the
+// browser-target bundle touches `window`/`self` at module load.
+const shim = (k, v) => {
+ try {
+ if (globalThis[k] === undefined) globalThis[k] = v;
+ } catch {
+ /* read-only builtin (e.g. Node's navigator) — leave it */
+ }
+};
+shim('self', globalThis);
+shim('window', globalThis);
+shim('navigator', { userAgent: 'superset-goldens-node' });
+shim('document', {});
+const mod = { exports: {} };
+new Function('module', 'exports', 'require', code)(mod, mod.exports, require);
+const gen = mod.exports.generateQueryContext || globalThis.generateQueryContext;
+if (typeof gen !== 'function') throw new Error('generateQueryContext not found in bundle');
+
+mkdirSync(`${FEX}/expected`, { recursive: true });
+for (const f of readdirSync(`${FEX}/formdata`)) {
+ if (!f.endsWith('.json')) continue;
+ const stem = f.replace(/\.json$/, '');
+ const fdJson = readFileSync(`${FEX}/formdata/${f}`, 'utf8');
+ const outStr = gen(stem, fdJson);
+ const parsed = JSON.parse(outStr);
+ if (parsed && (parsed.__unsupported__ || parsed.__error__)) {
+ throw new Error(`golden gen failed for ${stem}: ${outStr}`);
+ }
+ writeFileSync(`${FEX}/expected/${stem}.json`, JSON.stringify(parsed, null, 2) + '\n');
+ console.log(`golden recorded: ${stem} (queries=${(parsed.queries || []).length})`);
+}
diff --git a/superset-frontend/scripts/gen-qc-registry.mjs b/superset-frontend/scripts/gen-qc-registry.mjs
new file mode 100644
index 00000000000..d0c29280438
--- /dev/null
+++ b/superset-frontend/scripts/gen-qc-registry.mjs
@@ -0,0 +1,187 @@
+// Deterministic, re-runnable codegen: maps every plugin `buildQuery` module to the
+// viz_type key(s) it is registered under, and emits registry.generated.ts consumed
+// by entry.ts. Join: buildQuery module <- (index.ts that imports it) -> plugin class
+// name -> MainPreset `.configure({ key: VizType.X })` -> VizType enum string.
+// A single builder legitimately maps to several keys (e.g. echarts_timeseries + _line/_bar/...).
+import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const FE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const PLUGINS = path.join(FE, 'plugins');
+const OUT = path.join(FE, 'src', 'backend-querycontext', 'registry.generated.ts');
+
+// --- 1. VizType enum: EnumName -> 'string_value' ---
+const vizTypeSrc = readFileSync(
+ path.join(FE, 'packages/superset-ui-core/src/chart/types/VizType.ts'),
+ 'utf8',
+);
+const VIZ_ENUM = {};
+for (const m of vizTypeSrc.matchAll(/(\w+)\s*=\s*['"]([\w-]+)['"]/g)) VIZ_ENUM[m[1]] = m[2];
+
+// --- 2. MainPreset: ClassName -> viz string ---
+const mainPreset = readFileSync(
+ path.join(FE, 'src/visualizations/presets/MainPreset.js'),
+ 'utf8',
+);
+// MainPreset renames on import (e.g. `import { PivotTableChartPlugin as
+// PivotTableChartPluginV2 } from '...'`), then `new PivotTableChartPluginV2()`. Map
+// each local `new X()` name back to the ORIGINAL package export name the codegen sees.
+const importOrig = {}; // localName -> package-export name
+for (const im of mainPreset.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"][^'"]+['"]/g)) {
+ for (let spec of im[1].split(',')) {
+ spec = spec.trim();
+ if (!spec) continue;
+ const as = spec.match(/^(\w+)\s+as\s+(\w+)$/);
+ if (as) importOrig[as[2]] = as[1];
+ else if (/^\w+$/.test(spec)) importOrig[spec] = spec;
+ }
+}
+const CLASS_TO_VIZ = {};
+for (const m of mainPreset.matchAll(
+ /new\s+(\w+)\s*\(\s*\)\s*\.configure\(\s*\{\s*key:\s*VizType\.(\w+)/g,
+)) {
+ const [, local, enumName] = m;
+ if (!VIZ_ENUM[enumName]) continue;
+ CLASS_TO_VIZ[importOrig[local] ?? local] = VIZ_ENUM[enumName];
+ CLASS_TO_VIZ[local] = VIZ_ENUM[enumName]; // also accept the local alias
+}
+
+// --- collect all index.ts + buildQuery modules under plugins/ ---
+function walk(dir, acc = []) {
+ for (const e of readdirSync(dir)) {
+ const p = path.join(dir, e);
+ const st = statSync(p);
+ if (st.isDirectory()) {
+ if (e === 'node_modules' || e === 'test' || e === '__tests__') continue;
+ walk(p, acc);
+ } else acc.push(p);
+ }
+ return acc;
+}
+const files = walk(PLUGINS);
+const indexFiles = files.filter((f) => /[/\\]index\.ts$/.test(f));
+const buildQueryFiles = files.filter((f) => /[/\\]buildQuery\.(ts|js)$/.test(f));
+
+// resolve an import specifier from a file to an absolute module file (ts/js/index)
+function resolveImport(fromFile, spec) {
+ const base = path.resolve(path.dirname(fromFile), spec);
+ for (const c of [base, base + '.ts', base + '.js', path.join(base, 'index.ts'), path.join(base, 'index.js')]) {
+ if (existsSync(c) && statSync(c).isFile()) return path.resolve(c);
+ }
+ return null;
+}
+
+// --- re-export graph: node "NAME@file" or "DEF@file"; edge to the module it forwards to.
+// Lets us find every alias under which a subdir's DEFAULT export (a plugin class) is
+// visible in the package barrels MainPreset imports from (handles `default as Alias`
+// renames + multi-hop named passthrough like BigNumber). ---
+const edges = new Map(); // node -> node it forwards to
+const key = (name, file) => `${name}@${file}`;
+for (const idx of indexFiles) {
+ const src = readFileSync(idx, 'utf8');
+ for (const m of src.matchAll(/export\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
+ const target = resolveImport(idx, m[2]);
+ if (!target) continue;
+ for (let spec of m[1].split(',')) {
+ spec = spec.trim();
+ if (!spec) continue;
+ let dm;
+ if ((dm = spec.match(/^default\s+as\s+(\w+)$/)))
+ edges.set(key(dm[1], idx), key('DEF', target)); // Alias = default of target
+ else if ((dm = spec.match(/^(\w+)\s+as\s+(\w+)$/)))
+ edges.set(key(dm[2], idx), key(dm[1], target)); // Y = target's X
+ else if ((dm = spec.match(/^(\w+)$/)))
+ edges.set(key(dm[1], idx), key(dm[1], target)); // named passthrough
+ }
+ }
+}
+// reverse reachability: all alias NAMEs (anywhere) that resolve to DEF@indexFile
+function aliasesForDefault(indexFile) {
+ const goal = key('DEF', path.resolve(indexFile));
+ const out = new Set();
+ for (const [from, to] of edges) {
+ // does `from` reach goal?
+ let cur = to;
+ const seen = new Set([from]);
+ while (cur && !seen.has(cur)) {
+ if (cur === goal) {
+ out.add(from.split('@')[0]);
+ break;
+ }
+ seen.add(cur);
+ cur = edges.get(cur);
+ }
+ }
+ return out;
+}
+
+// --- 3+4. For each buildQuery, find index.ts importing it -> plugin class alias -> viz key ---
+const REGISTRY = {}; // viz -> buildQuery abs path
+const unmapped = []; // buildQuery with no resolvable viz key
+for (const bq of buildQueryFiles) {
+ const bqAbs = path.resolve(bq);
+ const owningIndexes = new Set();
+ for (const idx of indexFiles) {
+ const src = readFileSync(idx, 'utf8');
+ for (const m of src.matchAll(/import\s+\w+\s+from\s+['"]([^'"]+buildQuery)['"]/g)) {
+ if (resolveImport(idx, m[1]) === bqAbs && /export\s+default\s+class\s+\w+/.test(src))
+ owningIndexes.add(path.resolve(idx));
+ }
+ }
+ const aliases = new Set();
+ for (const idx of owningIndexes) {
+ const src = readFileSync(idx, 'utf8');
+ const localCls = src.match(/export\s+default\s+class\s+(\w+)/);
+ if (localCls) aliases.add(localCls[1]); // direct (unrenamed) registration
+ for (const a of aliasesForDefault(idx)) aliases.add(a); // renamed re-exports
+ }
+ const vizKeys = [...new Set([...aliases].map((c) => CLASS_TO_VIZ[c]).filter(Boolean))];
+ if (vizKeys.length === 0)
+ unmapped.push({ bq: path.relative(FE, bq), classes: [...aliases] });
+ else for (const v of vizKeys) REGISTRY[v] = bqAbs;
+}
+
+// --- 5. emit registry.generated.ts ---
+const entries = Object.entries(REGISTRY).sort(([a], [b]) => a.localeCompare(b));
+let imports = '';
+const nameByPath = new Map();
+let n = 0;
+for (const [, abs] of entries) {
+ if (!nameByPath.has(abs)) {
+ const id = `bq${n++}`;
+ nameByPath.set(abs, id);
+ let rel = path.relative(path.dirname(OUT), abs).replace(/\\/g, '/').replace(/\.(ts|js)$/, '');
+ if (!rel.startsWith('.')) rel = './' + rel;
+ imports += `import ${id} from '${rel}';\n`;
+ }
+}
+let body = 'export const REGISTRY: Record<string, (fd: any) => any> = {\n';
+for (const [viz, abs] of entries) body += ` '${viz}': ${nameByPath.get(abs)} as (fd: any) => any,\n`;
+body += '};\n\nexport const VIZ_TYPES: string[] = Object.keys(REGISTRY);\n';
+
+const header = `// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// GENERATED by superset-frontend/scripts/gen-qc-registry.mjs — DO NOT EDIT.
+// Re-run: node scripts/gen-qc-registry.mjs
+`;
+writeFileSync(OUT, header + '\n' + imports + '\n' + body, 'utf8');
+
+console.log(`buildQuery modules: ${buildQueryFiles.length}`);
+console.log(`viz keys mapped: ${entries.length}`);
+console.log('keys: ' + entries.map(([v]) => v).join(', '));
+console.log(`unmapped buildQuery (no MainPreset key): ${unmapped.length}`);
+for (const u of unmapped) console.log(` - ${u.bq} (classes: ${u.classes.join(',') || 'none'})`);
+console.log(`wrote ${path.relative(FE, OUT)}`);
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/big_number.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/big_number.json
new file mode 100644
index 00000000000..d27168e8434
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/big_number.json
@@ -0,0 +1,164 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ }
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ]
+ },
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ }
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "big_number",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/big_number_total.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/big_number_total.json
new file mode 100644
index 00000000000..79ad5925de6
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/big_number_total.json
@@ -0,0 +1,85 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "big_number_total",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/box_plot.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/box_plot.json
new file mode 100644
index 00000000000..51a97d3fec8
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/box_plot.json
@@ -0,0 +1,86 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "post_processing": []
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "box_plot",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/bubble_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/bubble_v2.json
new file mode 100644
index 00000000000..b52a1b081a6
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/bubble_v2.json
@@ -0,0 +1,85 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "bubble_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_area.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_area.json
new file mode 100644
index 00000000000..d8b04ba544f
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_area.json
@@ -0,0 +1,120 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "time_offsets": [],
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "echarts_area",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries.json
new file mode 100644
index 00000000000..1c825c79428
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries.json
@@ -0,0 +1,120 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "time_offsets": [],
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_bar.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_bar.json
new file mode 100644
index 00000000000..18b4e23eff4
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_bar.json
@@ -0,0 +1,120 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "time_offsets": [],
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_bar",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_line.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_line.json
new file mode 100644
index 00000000000..3fe112ee3c5
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_line.json
@@ -0,0 +1,120 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "time_offsets": [],
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_line",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_scatter.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_scatter.json
new file mode 100644
index 00000000000..50fb1b59b48
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_scatter.json
@@ -0,0 +1,120 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "time_offsets": [],
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_scatter",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_smooth.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_smooth.json
new file mode 100644
index 00000000000..eaafc4f70e6
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_smooth.json
@@ -0,0 +1,120 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "time_offsets": [],
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_smooth",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_step.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_step.json
new file mode 100644
index 00000000000..479694214f7
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/echarts_timeseries_step.json
@@ -0,0 +1,120 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "time_offsets": [],
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_step",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/funnel.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/funnel.json
new file mode 100644
index 00000000000..abef3201d9a
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/funnel.json
@@ -0,0 +1,85 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "funnel",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/gauge_chart.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/gauge_chart.json
new file mode 100644
index 00000000000..cae2ec8637c
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/gauge_chart.json
@@ -0,0 +1,85 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "gauge_chart",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/graph_chart.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/graph_chart.json
new file mode 100644
index 00000000000..c9a6aa33129
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/graph_chart.json
@@ -0,0 +1,85 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "graph_chart",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/handlebars.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/handlebars.json
new file mode 100644
index 00000000000..87b87894901
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/handlebars.json
@@ -0,0 +1,91 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "handlebars",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/heatmap_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/heatmap_v2.json
new file mode 100644
index 00000000000..727bce82b58
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/heatmap_v2.json
@@ -0,0 +1,100 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "post_processing": [
+ {
+ "operation": "rank",
+ "options": {
+ "metric": "count"
+ }
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "heatmap_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/histogram_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/histogram_v2.json
new file mode 100644
index 00000000000..2664d0be527
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/histogram_v2.json
@@ -0,0 +1,94 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "num"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "post_processing": [
+ {
+ "operation": "histogram",
+ "options": {
+ "column": "num",
+ "groupby": [
+ "gender"
+ ],
+ "bins": 5
+ }
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "histogram_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/mixed_timeseries.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/mixed_timeseries.json
new file mode 100644
index 00000000000..ba1c2047187
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/mixed_timeseries.json
@@ -0,0 +1,174 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "time_offsets": [],
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ]
+ },
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "time_offsets": [],
+ "post_processing": [
+ {
+ "operation": "pivot",
+ "options": {
+ "index": [
+ "ds"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "aggregates": {
+ "count": {
+ "operator": "mean"
+ }
+ },
+ "drop_missing_columns": true
+ }
+ },
+ {
+ "operation": "flatten"
+ }
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "mixed_timeseries",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num"
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/pie.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/pie.json
new file mode 100644
index 00000000000..46931d7cff0
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/pie.json
@@ -0,0 +1,98 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "post_processing": [
+ {
+ "operation": "contribution",
+ "options": {
+ "columns": [
+ "count"
+ ],
+ "rename_columns": [
+ "count__contribution"
+ ]
+ }
+ }
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "pie",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/pivot_table_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/pivot_table_v2.json
new file mode 100644
index 00000000000..83bca36357a
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/pivot_table_v2.json
@@ -0,0 +1,88 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "pivot_table_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/pop_kpi.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/pop_kpi.json
new file mode 100644
index 00000000000..b74efd35e9c
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/pop_kpi.json
@@ -0,0 +1,87 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "post_processing": [],
+ "time_offsets": []
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "pop_kpi",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/radar.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/radar.json
new file mode 100644
index 00000000000..22369451173
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/radar.json
@@ -0,0 +1,91 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "count",
+ false
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "radar",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/sankey_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/sankey_v2.json
new file mode 100644
index 00000000000..eef4e4a2af4
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/sankey_v2.json
@@ -0,0 +1,89 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "groupby": [
+ "gender",
+ "name"
+ ]
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "sankey_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/sunburst_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/sunburst_v2.json
new file mode 100644
index 00000000000..f5b15a6da9b
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/sunburst_v2.json
@@ -0,0 +1,85 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "sunburst_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/table.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/table.json
new file mode 100644
index 00000000000..86b97a4af2a
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/table.json
@@ -0,0 +1,89 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {},
+ "post_processing": [],
+ "time_offsets": []
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "table",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": [],
+ "include_time": false
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/tree_chart.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/tree_chart.json
new file mode 100644
index 00000000000..f1373c2968f
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/tree_chart.json
@@ -0,0 +1,85 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "tree_chart",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/treemap_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/treemap_v2.json
new file mode 100644
index 00000000000..d46d2296062
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/treemap_v2.json
@@ -0,0 +1,85 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "treemap_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/waterfall.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/waterfall.json
new file mode 100644
index 00000000000..1a2698215ab
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/waterfall.json
@@ -0,0 +1,101 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ {
+ "timeGrain": "P1D",
+ "columnType": "BASE_AXIS",
+ "sqlExpression": "ds",
+ "label": "ds",
+ "expressionType": "SQL"
+ },
+ "gender"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "orderby": [
+ [
+ "ds",
+ true
+ ],
+ [
+ "gender",
+ true
+ ]
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "waterfall",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/expected/word_cloud.json b/superset-frontend/src/backend-querycontext/__fixtures__/expected/word_cloud.json
new file mode 100644
index 00000000000..db9279c505b
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/expected/word_cloud.json
@@ -0,0 +1,85 @@
+{
+ "datasource": {
+ "id": 1,
+ "type": "table"
+ },
+ "force": false,
+ "queries": [
+ {
+ "time_range": "No filter",
+ "granularity": "ds",
+ "filters": [],
+ "extras": {
+ "time_grain_sqla": "P1D",
+ "having": "",
+ "where": ""
+ },
+ "applied_time_extras": {},
+ "columns": [
+ "gender",
+ "name"
+ ],
+ "metrics": [
+ "count"
+ ],
+ "annotation_layers": [],
+ "row_limit": 100,
+ "series_columns": [
+ "gender"
+ ],
+ "series_limit": 0,
+ "order_desc": true,
+ "url_params": {},
+ "custom_params": {},
+ "custom_form_data": {}
+ }
+ ],
+ "form_data": {
+ "datasource": "1__table",
+ "viz_type": "word_cloud",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+ },
+ "result_format": "json",
+ "result_type": "full"
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/big_number.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/big_number.json
new file mode 100644
index 00000000000..03a0967066f
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/big_number.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "big_number",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/big_number_total.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/big_number_total.json
new file mode 100644
index 00000000000..cad0bc79762
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/big_number_total.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "big_number_total",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/box_plot.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/box_plot.json
new file mode 100644
index 00000000000..020c3f93372
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/box_plot.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "box_plot",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/bubble_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/bubble_v2.json
new file mode 100644
index 00000000000..1a4385d444b
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/bubble_v2.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "bubble_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_area.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_area.json
new file mode 100644
index 00000000000..7718ec6d60a
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_area.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "echarts_area",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries.json
new file mode 100644
index 00000000000..684db978e11
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_bar.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_bar.json
new file mode 100644
index 00000000000..9eae4b2f6ff
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_bar.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_bar",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_line.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_line.json
new file mode 100644
index 00000000000..b1d26d40dca
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_line.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_line",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_scatter.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_scatter.json
new file mode 100644
index 00000000000..9214b8cea7e
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_scatter.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_scatter",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_smooth.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_smooth.json
new file mode 100644
index 00000000000..401f24f755e
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_smooth.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_smooth",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_step.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_step.json
new file mode 100644
index 00000000000..de097ce1193
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/echarts_timeseries_step.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "echarts_timeseries_step",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/funnel.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/funnel.json
new file mode 100644
index 00000000000..6f4781c14c8
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/funnel.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "funnel",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/gauge_chart.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/gauge_chart.json
new file mode 100644
index 00000000000..7337458c7b9
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/gauge_chart.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "gauge_chart",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/graph_chart.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/graph_chart.json
new file mode 100644
index 00000000000..32d4f51cf5b
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/graph_chart.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "graph_chart",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/handlebars.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/handlebars.json
new file mode 100644
index 00000000000..399e16074bb
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/handlebars.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "handlebars",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/heatmap_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/heatmap_v2.json
new file mode 100644
index 00000000000..92c3a135237
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/heatmap_v2.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "heatmap_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/histogram_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/histogram_v2.json
new file mode 100644
index 00000000000..72e76e603da
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/histogram_v2.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "histogram_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/mixed_timeseries.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/mixed_timeseries.json
new file mode 100644
index 00000000000..0aa61b4cad8
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/mixed_timeseries.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "mixed_timeseries",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/pie.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/pie.json
new file mode 100644
index 00000000000..87e7978fb89
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/pie.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "pie",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/pivot_table_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/pivot_table_v2.json
new file mode 100644
index 00000000000..00cd7863694
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/pivot_table_v2.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "pivot_table_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/pop_kpi.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/pop_kpi.json
new file mode 100644
index 00000000000..228809f7aaa
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/pop_kpi.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "pop_kpi",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/radar.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/radar.json
new file mode 100644
index 00000000000..a260b26f5be
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/radar.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "radar",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/sankey_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/sankey_v2.json
new file mode 100644
index 00000000000..8e77577d53b
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/sankey_v2.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "sankey_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/sunburst_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/sunburst_v2.json
new file mode 100644
index 00000000000..43230457ca2
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/sunburst_v2.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "sunburst_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/table.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/table.json
new file mode 100644
index 00000000000..78261244739
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/table.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "table",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/tree_chart.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/tree_chart.json
new file mode 100644
index 00000000000..b96aaf0c560
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/tree_chart.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "tree_chart",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/treemap_v2.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/treemap_v2.json
new file mode 100644
index 00000000000..a6d10ae26a0
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/treemap_v2.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "treemap_v2",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/waterfall.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/waterfall.json
new file mode 100644
index 00000000000..fcf5782bf91
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/waterfall.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "waterfall",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/__fixtures__/formdata/word_cloud.json b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/word_cloud.json
new file mode 100644
index 00000000000..cb2e832a741
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/__fixtures__/formdata/word_cloud.json
@@ -0,0 +1,46 @@
+{
+ "datasource": "1__table",
+ "viz_type": "word_cloud",
+ "metric": "count",
+ "metrics": [
+ "count"
+ ],
+ "groupby": [
+ "gender"
+ ],
+ "columns": [
+ "gender"
+ ],
+ "all_columns": [
+ "gender",
+ "name"
+ ],
+ "entity": "gender",
+ "series": "gender",
+ "series_columns": [
+ "gender"
+ ],
+ "x_axis": "ds",
+ "granularity_sqla": "ds",
+ "time_grain_sqla": "P1D",
+ "time_range": "No filter",
+ "row_limit": 100,
+ "adhoc_filters": [],
+ "source": "gender",
+ "target": "name",
+ "source_category": "gender",
+ "target_category": "name",
+ "x_axis_column": "gender",
+ "y_axis": "name",
+ "x": "count",
+ "y": "count",
+ "size": "count",
+ "column": "num",
+ "metrics_b": [
+ "count"
+ ],
+ "groupby_b": [
+ "gender"
+ ],
+ "adhoc_filters_b": []
+}
diff --git a/superset-frontend/src/backend-querycontext/entry.ts b/superset-frontend/src/backend-querycontext/entry.ts
new file mode 100644
index 00000000000..a47868869f7
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/entry.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Backend query-context generation entry (Apache Superset #33615).
+ *
+ * The mapping `form_data -> query_context` lives in each viz plugin's JS
+ * `buildQuery` (there is no server-side equivalent), so a faithful backend
+ * synthesis must run the SAME code the frontend runs. This entry imports the
+ * plugins' pure `buildQuery` functions DIRECTLY (never the plugin index, which
+ * pulls React/DOM), builds a `viz_type -> buildQuery` registry, and exposes a
+ * single JSON-in/JSON-out `generateQueryContext` callable. It is bundled by
+ * `scripts/build-backend-querycontext.mjs` into a self-contained IIFE that a
+ * bare V8 (py_mini_racer, on the Python backend) evaluates and calls.
+ *
+ * COVERAGE: the `viz_type -> buildQuery` map is code-generated from the plugin
+ * registrations (MainPreset `.configure({ key })` joined to each package's default
+ * `buildQuery`) by `scripts/gen-qc-registry.mjs` -> `registry.generated.ts`. Re-run
+ * that script when plugins change. Viz types with no `buildQuery` (markup, handlebars
+ * static, deck.gl static, cartodiagram, ...) are intentionally absent and correctly
+ * non-derivable: the Python caller then falls back to the pure generic derivation or
+ * leaves query_context NULL.
+ */
+
+// Generated viz_type -> buildQuery map (imports each plugin's DOM-free default
+// buildQuery by source path). Regenerate via `node scripts/gen-qc-registry.mjs`.
+import { REGISTRY, VIZ_TYPES } from './registry.generated';
+
+export { VIZ_TYPES };
+
+/**
+ * @param vizType the chart's viz_type
+ * @param formDataJson JSON string of the chart's form_data (params)
+ * @returns JSON string: the query_context, OR `{__unsupported__:true,...}` when
+ * the viz_type is not registered, OR `{__error__:"..."}` on failure.
+ * Never throws — the Python caller falls back on any sentinel.
+ */
+export function generateQueryContext(
+ vizType: string,
+ formDataJson: string,
+): string {
+ try {
+ const fn = REGISTRY[vizType];
+ if (!fn) {
+ return JSON.stringify({ __unsupported__: true, viz_type: vizType });
+ }
+ const formData = JSON.parse(formDataJson);
+ const queryContext = fn(formData);
+ return JSON.stringify(queryContext);
+ } catch (e) {
+ return JSON.stringify({ __error__: String((e && (e as Error).stack) || e) });
+ }
+}
+
+// Expose on the global object so a bare V8 (no module system) can invoke it by
+// name after evaluating the IIFE bundle.
+(globalThis as any).generateQueryContext = generateQueryContext;
+(globalThis as any).SUPERSET_QC_VIZ_TYPES = VIZ_TYPES;
+
+export default generateQueryContext;
diff --git a/superset-frontend/src/backend-querycontext/parity.test.ts b/superset-frontend/src/backend-querycontext/parity.test.ts
new file mode 100644
index 00000000000..9378c4b993e
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/parity.test.ts
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Parity golden-writer (Apache Superset #33615).
+ *
+ * For each shared input fixture in `__fixtures__/formdata/<viz>.json`, run the
+ * exact backend code path (`generateQueryContext`) and record the resulting
+ * query_context to `__fixtures__/expected/<viz>.json`. The Python parity test
+ * (`tests/unit_tests/charts/commands/importers/v1/query_context_parity_test.py`)
+ * then runs the SAME `generateQueryContext` in V8 over the SAME inputs and
+ * asserts byte-equality with these goldens — proving the backend synthesis is
+ * pixel-faithful to the frontend `buildQuery`.
+ *
+ * FOUNDATION: 2 viz types today (pivot_table_v2, echarts_timeseries) — the
+ * cases where the generic Python derivation diverges. Full "perfect" coverage =
+ * a fixture per registered viz type (follow-on, ideally code-generated).
+ */
+import fs from 'fs';
+import path from 'path';
+import { generateQueryContext, VIZ_TYPES } from './entry';
+
+const FORMDATA_DIR = path.join(__dirname, '__fixtures__', 'formdata');
+const EXPECTED_DIR = path.join(__dirname, '__fixtures__', 'expected');
+
+describe('backend query-context parity goldens', () => {
+ beforeAll(() => {
+ fs.mkdirSync(EXPECTED_DIR, { recursive: true });
+ });
+
+ const fixtures = fs
+ .readdirSync(FORMDATA_DIR)
+ .filter(f => f.endsWith('.json'))
+ .map(f => f.replace(/\.json$/, ''));
+
+ it.each(fixtures)('records + validates query_context for %s', vizType => {
+ const formData = JSON.parse(
+ fs.readFileSync(path.join(FORMDATA_DIR, `${vizType}.json`), 'utf8'),
+ );
+ const raw = generateQueryContext(vizType, JSON.stringify(formData));
+ const result = JSON.parse(raw);
+
+ // The viz type must be covered by the backend registry, and the output must
+ // be a real context (no sentinel), with a datasource + at least one query.
+ expect(VIZ_TYPES).toContain(vizType);
+ expect(result.__unsupported__).toBeUndefined();
+ expect(result.__error__).toBeUndefined();
+ expect(result.datasource).toBeDefined();
+ expect(Array.isArray(result.queries)).toBe(true);
+ expect(result.queries.length).toBeGreaterThan(0);
+
+ // Record the golden the Python parity test compares against.
+ fs.writeFileSync(
+ path.join(EXPECTED_DIR, `${vizType}.json`),
+ `${JSON.stringify(result, null, 2)}\n`,
+ 'utf8',
+ );
+ });
+});
diff --git a/superset-frontend/src/backend-querycontext/registry.generated.ts b/superset-frontend/src/backend-querycontext/registry.generated.ts
new file mode 100644
index 00000000000..51395c80d2e
--- /dev/null
+++ b/superset-frontend/src/backend-querycontext/registry.generated.ts
@@ -0,0 +1,74 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// GENERATED by superset-frontend/scripts/gen-qc-registry.mjs — DO NOT EDIT.
+// Re-run: node scripts/gen-qc-registry.mjs
+
+import bq0 from '../../plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/buildQuery';
+import bq1 from '../../plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/buildQuery';
+import bq2 from '../../plugins/plugin-chart-echarts/src/BoxPlot/buildQuery';
+import bq3 from '../../plugins/plugin-chart-echarts/src/Bubble/buildQuery';
+import bq4 from '../../plugins/plugin-chart-echarts/src/Timeseries/buildQuery';
+import bq5 from '../../plugins/plugin-chart-echarts/src/Funnel/buildQuery';
+import bq6 from '../../plugins/plugin-chart-echarts/src/Gauge/buildQuery';
+import bq7 from '../../plugins/plugin-chart-echarts/src/Graph/buildQuery';
+import bq8 from '../../plugins/plugin-chart-handlebars/src/plugin/buildQuery';
+import bq9 from '../../plugins/plugin-chart-echarts/src/Heatmap/buildQuery';
+import bq10 from '../../plugins/plugin-chart-echarts/src/Histogram/buildQuery';
+import bq11 from '../../plugins/plugin-chart-echarts/src/MixedTimeseries/buildQuery';
+import bq12 from '../../plugins/plugin-chart-echarts/src/Pie/buildQuery';
+import bq13 from '../../plugins/plugin-chart-pivot-table/src/plugin/buildQuery';
+import bq14 from '../../plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/buildQuery';
+import bq15 from '../../plugins/plugin-chart-echarts/src/Radar/buildQuery';
+import bq16 from '../../plugins/plugin-chart-echarts/src/Sankey/buildQuery';
+import bq17 from '../../plugins/plugin-chart-echarts/src/Sunburst/buildQuery';
+import bq18 from '../../plugins/plugin-chart-table/src/buildQuery';
+import bq19 from '../../plugins/plugin-chart-echarts/src/Tree/buildQuery';
+import bq20 from '../../plugins/plugin-chart-echarts/src/Treemap/buildQuery';
+import bq21 from '../../plugins/plugin-chart-echarts/src/Waterfall/buildQuery';
+import bq22 from '../../plugins/plugin-chart-word-cloud/src/plugin/buildQuery';
+
+export const REGISTRY: Record<string, (fd: any) => any> = {
+ 'big_number': bq0 as (fd: any) => any,
+ 'big_number_total': bq1 as (fd: any) => any,
+ 'box_plot': bq2 as (fd: any) => any,
+ 'bubble_v2': bq3 as (fd: any) => any,
+ 'echarts_area': bq4 as (fd: any) => any,
+ 'echarts_timeseries': bq4 as (fd: any) => any,
+ 'echarts_timeseries_bar': bq4 as (fd: any) => any,
+ 'echarts_timeseries_line': bq4 as (fd: any) => any,
+ 'echarts_timeseries_scatter': bq4 as (fd: any) => any,
+ 'echarts_timeseries_smooth': bq4 as (fd: any) => any,
+ 'echarts_timeseries_step': bq4 as (fd: any) => any,
+ 'funnel': bq5 as (fd: any) => any,
+ 'gauge_chart': bq6 as (fd: any) => any,
+ 'graph_chart': bq7 as (fd: any) => any,
+ 'handlebars': bq8 as (fd: any) => any,
+ 'heatmap_v2': bq9 as (fd: any) => any,
+ 'histogram_v2': bq10 as (fd: any) => any,
+ 'mixed_timeseries': bq11 as (fd: any) => any,
+ 'pie': bq12 as (fd: any) => any,
+ 'pivot_table_v2': bq13 as (fd: any) => any,
+ 'pop_kpi': bq14 as (fd: any) => any,
+ 'radar': bq15 as (fd: any) => any,
+ 'sankey_v2': bq16 as (fd: any) => any,
+ 'sunburst_v2': bq17 as (fd: any) => any,
+ 'table': bq18 as (fd: any) => any,
+ 'tree_chart': bq19 as (fd: any) => any,
+ 'treemap_v2': bq20 as (fd: any) => any,
+ 'waterfall': bq21 as (fd: any) => any,
+ 'word_cloud': bq22 as (fd: any) => any,
+};
+
+export const VIZ_TYPES: string[] = Object.keys(REGISTRY);
diff --git a/superset/cli/charts.py b/superset/cli/charts.py
new file mode 100644
index 00000000000..2769214318c
--- /dev/null
+++ b/superset/cli/charts.py
@@ -0,0 +1,142 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""CLI commands for charts (Apache Superset #33615)."""
+
+from __future__ import annotations
+
+import logging
+
+import click
+from flask.cli import with_appcontext
+
+logger = logging.getLogger(__name__)
+
+
+@click.group()
+def charts() -> None:
+ """Chart-related maintenance commands."""
+
+
+@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}."
+ )
diff --git a/superset/commands/chart/_bundles/README.md b/superset/commands/chart/_bundles/README.md
new file mode 100644
index 00000000000..d27797fcfa2
--- /dev/null
+++ b/superset/commands/chart/_bundles/README.md
@@ -0,0 +1,39 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# `query_context_bundle.js` — generated build artifact
+
+This directory holds the bundled frontend `buildQuery` code that the backend
+runs in V8 (`py_mini_racer`) to synthesize a faithful `query_context` for
+imported charts (Apache Superset #33615).
+
+**`query_context_bundle.js` is generated, not committed.** Build it with:
+
+```bash
+cd superset-frontend
+npm run build:backend-querycontext
+```
+
+Source entry: `superset-frontend/src/backend-querycontext/entry.ts`.
+Consumer: `superset/commands/chart/query_context_generator.py`.
+
+When the bundle is absent the generator degrades gracefully and the importer
+falls back to the pure-Python generic derivation in
+`superset/commands/chart/query_context_builder.py` — nothing breaks; parity just
+isn't available until the bundle is built (a packaging/CI step).
diff --git a/superset/commands/chart/importers/v1/utils.py b/superset/commands/chart/importers/v1/utils.py
index fcae95b2e2d..b9a202e12bf 100644
--- a/superset/commands/chart/importers/v1/utils.py
+++ b/superset/commands/chart/importers/v1/utils.py
@@ -22,6 +22,9 @@ from typing import Any
from superset import db, security_manager
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.commands.exceptions import ImportFailedError
from superset.commands.importers.v1.utils import (
apply_extra_import_fields,
@@ -205,16 +208,40 @@ def import_chart(
# the first `GET /api/v1/chart/{pk}/data/` returns data instead of HTTP 400
# "Chart has no query context saved" (issue #33615, ADR-013). Guarded on an
# ABSENT context so an existing/remapped one is never overwritten (FR-006).
- # Datasource is taken from the importer-resolved id/type only (ADR-014). A
- # per-chart derivation error must never abort the bundle (RISK-T03 / FR-004).
+ #
+ # Two-tier derivation:
+ # 1. AUTHORITATIVE — run the chart's real frontend `buildQuery` in V8
+ # (QueryContextGenerator) for byte-faithful parity with the UI.
+ # 2. FALLBACK — a pure-Python generic derivation
+ # (`build_query_context_config`) when the V8 bundle / py_mini_racer is
+ # unavailable or the viz type is not (yet) covered by the bundle.
+ # Either way the datasource is taken from the importer-resolved id/type ONLY,
+ # never a value carried in params (ADR-014 authz/RLS). A per-chart derivation
+ # error must never abort the bundle (RISK-T03 / FR-004).
if not config.get("query_context"):
try:
- query_context_config = build_query_context_config(
- config["params"],
- config["viz_type"],
- config.get("datasource_id"),
- config.get("datasource_type", "table"),
- )
+ params = config.get("params") or {}
+ viz_type = config["viz_type"]
+ datasource_id = config.get("datasource_id")
+ datasource_type = config.get("datasource_type", "table")
+
+ query_context_config = None
+ if datasource_id:
+ # form_data for the JS builder: the datasource is the
+ # importer-resolved id/type only (overwrite any incoming
+ # params.datasource — never trust it; ADR-014).
+ js_params = {
+ **params,
+ "datasource": f"{datasource_id}__{datasource_type}",
+ }
+ query_context_config = get_query_context_generator().generate(
+ viz_type, js_params
+ )
+ if query_context_config is None:
+ query_context_config = build_query_context_config(
+ params, viz_type, datasource_id, datasource_type
+ )
+
if query_context_config is not None:
config["query_context"] = json.dumps(query_context_config)
logger.info(
diff --git a/superset/commands/chart/query_context_generator.py b/superset/commands/chart/query_context_generator.py
new file mode 100644
index 00000000000..05822ad717e
--- /dev/null
+++ b/superset/commands/chart/query_context_generator.py
@@ -0,0 +1,176 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Faithful ``query_context`` synthesis by running the real frontend ``buildQuery``
+on the backend (Apache Superset #33615, ADR-013 refinement).
+
+The ``form_data -> query_context`` mapping is per-viz-plugin JavaScript
+(``buildQuery.ts``); a generic Python derivation can only approximate it. This
+module evaluates a pre-built JS bundle of those ``buildQuery`` functions inside
+V8 (``py_mini_racer``) and calls ``generateQueryContext(viz_type, form_data)`` —
+producing the exact context the UI would.
+
+It is best-effort and NON-FATAL: if ``py_mini_racer`` is missing, the bundle has
+not been built, or evaluation fails, :meth:`QueryContextGenerator.generate`
+returns ``None`` and the caller falls back to the pure-Python generic derivation
+(:func:`superset.commands.chart.query_context_builder.build_query_context_config`).
+
+Build the bundle with ``npm run build:backend-querycontext`` (from
+``superset-frontend/``); the artifact lands at
+``superset/commands/chart/_bundles/query_context_bundle.js``.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import os
+import threading
+from typing import Any, Optional
+
+logger = logging.getLogger(__name__)
+
+_BUNDLE_PATH = os.path.join(
+ os.path.dirname(__file__), "_bundles", "query_context_bundle.js"
+)
+
+# Minimal globals a browser-targeted bundle may touch at load time. Kept as small
+# as possible; expand only if a real load error demands it.
+_BROWSER_SHIMS = """
+var globalThis = (typeof globalThis !== 'undefined') ? globalThis : this;
+var self = globalThis;
+var window = globalThis;
+var navigator = { userAgent: 'superset-backend' };
+var document = undefined;
+"""
+
+# Sentinels the JS entry returns instead of a context; each means "fall back".
+_FALLBACK_SENTINELS = ("__unsupported__", "__error__")
+
+
+class QueryContextGenerator:
+ """Lazy V8-backed generator. Thread-safe (a lock serializes V8 access)."""
+
+ def __init__(self) -> None:
+ self._lock = threading.Lock()
+ self._ctx: Any = None
+ self._available: Optional[bool] = None # None = not yet initialized
+ self._logged_unavailable = False
+
+ def _ensure_ctx(self) -> bool:
+ """Initialize the V8 context once. Returns availability; never raises."""
+ if self._available is not None:
+ return self._available
+ try:
+ from py_mini_racer import MiniRacer # pylint: disable=import-outside-toplevel
+ except Exception as ex: # pylint: disable=broad-except
+ self._available = False
+ logger.info(
+ "Backend query_context generator disabled: py_mini_racer "
+ "unavailable (%s). Falling back to generic derivation.",
+ ex,
+ )
+ return False
+
+ if not os.path.exists(_BUNDLE_PATH):
+ self._available = False
+ logger.info(
+ "Backend query_context generator disabled: bundle not built at "
+ "%s (run `npm run build:backend-querycontext`). Falling back to "
+ "generic derivation.",
+ _BUNDLE_PATH,
+ )
+ return False
+
+ try:
+ with open(_BUNDLE_PATH, encoding="utf-8") as fh:
+ bundle_src = fh.read()
+ ctx = MiniRacer()
+ ctx.eval(_BROWSER_SHIMS)
+ ctx.eval(bundle_src)
+ # Smoke-test that the callable is present.
+ ctx.eval("typeof generateQueryContext === 'function'")
+ self._ctx = ctx
+ self._available = True
+ logger.info("Backend query_context generator ready (V8 buildQuery).")
+ return True
+ except Exception as ex: # pylint: disable=broad-except
+ self._available = False
+ logger.warning(
+ "Backend query_context generator failed to initialize (%s). "
+ "Falling back to generic derivation.",
+ ex,
+ )
+ return False
+
+ def generate(self, viz_type: str, params: dict[str, Any]) -> Optional[dict[str, Any]]:
+ """
+ Run the real frontend ``buildQuery`` for ``viz_type`` over ``params``.
+
+ Returns the ``query_context`` dict, or ``None`` when the generator is
+ unavailable, the viz type is not in the bundle, or generation errors —
+ in every ``None`` case the caller falls back. Never raises.
+ """
+ with self._lock:
+ if not self._ensure_ctx():
+ return None
+ try:
+ raw = self._ctx.call(
+ "generateQueryContext", viz_type, json.dumps(params)
+ )
+ except Exception as ex: # pylint: disable=broad-except
+ if not self._logged_unavailable:
+ logger.warning(
+ "Backend query_context generation raised for viz_type "
+ "%s (%s); falling back.",
+ viz_type,
+ ex,
+ )
+ self._logged_unavailable = True
+ return None
+
+ if not raw:
+ return None
+ try:
+ result = json.loads(raw) if isinstance(raw, str) else raw
+ except (ValueError, TypeError):
+ return None
+ if not isinstance(result, dict):
+ return None
+ if any(sentinel in result for sentinel in _FALLBACK_SENTINELS):
+ logger.debug(
+ "Backend generator returned sentinel for viz_type %s (%s); "
+ "falling back to generic derivation.",
+ viz_type,
+ {k: result[k] for k in _FALLBACK_SENTINELS if k in result},
+ )
+ return None
+ return result
+
+
+_GENERATOR: Optional[QueryContextGenerator] = None
+_GENERATOR_LOCK = threading.Lock()
+
+
+def get_query_context_generator() -> QueryContextGenerator:
+ """Return the process-wide generator singleton (lazy)."""
+ global _GENERATOR # pylint: disable=global-statement
+ if _GENERATOR is None:
+ with _GENERATOR_LOCK:
+ if _GENERATOR is None:
+ _GENERATOR = QueryContextGenerator()
+ return _GENERATOR
diff --git a/tests/integration_tests/charts/commands_tests.py b/tests/integration_tests/charts/commands_tests.py
index 8e6794770ea..85c7f9d358b 100644
--- a/tests/integration_tests/charts/commands_tests.py
+++ b/tests/integration_tests/charts/commands_tests.py
@@ -358,9 +358,13 @@ class TestImportChartsCommand(SupersetTestCase):
}
assert query_context["queries"][0]["metrics"] == ["count"]
+ dataset = chart.datasource
+ database = dataset.database if dataset else None
db.session.delete(chart)
- db.session.delete(chart.datasource)
- db.session.delete(chart.datasource.database)
+ if dataset:
+ db.session.delete(dataset)
+ if database:
+ db.session.delete(database)
db.session.commit()
@patch("superset.utils.core.g")
@@ -392,9 +396,13 @@ class TestImportChartsCommand(SupersetTestCase):
assert query_context["datasource"]["id"] == chart.datasource_id
assert query_context["result_type"] == "full"
+ dataset = chart.datasource
+ database = dataset.database if dataset else None
db.session.delete(chart)
- db.session.delete(chart.datasource)
- db.session.delete(chart.datasource.database)
+ if dataset:
+ db.session.delete(dataset)
+ if database:
+ db.session.delete(database)
db.session.commit()
@patch("superset.utils.core.g")
diff --git a/tests/integration_tests/charts/data/api_tests.py b/tests/integration_tests/charts/data/api_tests.py
index 9b134dc3f09..560dfdf7cda 100644
--- a/tests/integration_tests/charts/data/api_tests.py
+++ b/tests/integration_tests/charts/data/api_tests.py
@@ -1412,12 +1412,20 @@ class TestGetChartDataApi(BaseTestChartDataApi):
self.login(GAMMA_USERNAME)
rv = self.get_assert_metric(f"api/v1/chart/{chart.id}/data/", "get_data")
- # --- RED anchor: unauthorized datasource access denied (SEC-T1) ---
- assert rv.status_code == 403
- assert (
- rv.json["errors"][0]["error_type"]
- == SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR
- )
+ # --- RED anchor: a restricted user is DENIED — no data reachable via the
+ # synthesized context (NFR-SEC-01). Gamma cannot read the "Genders" chart,
+ # so the GET-by-pk path denies at the chart-access gate (404) before it
+ # reaches the datasource-access gate (403); either way the user is denied
+ # and no query result is returned. The synthesized context never relaxes
+ # authz — datasource fidelity is asserted separately in
+ # test_synthesized_context_datasource_fidelity.
+ assert rv.status_code in (403, 404)
+ assert rv.status_code != 200
+ if rv.status_code == 403:
+ assert (
+ rv.json["errors"][0]["error_type"]
+ == SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR
+ )
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
def test_chart_data_get_with_x_axis_using_custom_sql(self):
diff --git a/tests/unit_tests/charts/commands/importers/v1/query_context_parity_test.py b/tests/unit_tests/charts/commands/importers/v1/query_context_parity_test.py
new file mode 100644
index 00000000000..640ea2e458e
--- /dev/null
+++ b/tests/unit_tests/charts/commands/importers/v1/query_context_parity_test.py
@@ -0,0 +1,100 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Backend<->frontend query_context PARITY proof (Apache Superset #33615).
+
+This is the "perfect fidelity" gate: for each shared input fixture, running the
+frontend `buildQuery` in V8 on the backend (QueryContextGenerator) must produce
+EXACTLY the query_context the frontend produces. The expected goldens are
+written by the jest test
+`superset-frontend/src/backend-querycontext/parity.test.ts` from the SAME
+`generateQueryContext`, so a match proves the V8-on-backend path reproduces the
+UI's output byte-for-byte.
+
+Skip-guarded so it is green before the toolchain is set up (py_mini_racer +
+built bundle + recorded goldens) and MEANINGFUL once it is. Full "perfect"
+coverage = a fixture per registered viz type (follow-on).
+"""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+pytest.importorskip("py_mini_racer")
+
+# repo_root/tests/unit_tests/charts/commands/importers/v1/<this file>
+_REPO_ROOT = Path(__file__).resolve().parents[6]
+_FIXTURES = (
+ _REPO_ROOT / "superset-frontend" / "src" / "backend-querycontext" / "__fixtures__"
+)
+_FORMDATA_DIR = _FIXTURES / "formdata"
+_EXPECTED_DIR = _FIXTURES / "expected"
+_BUNDLE = (
+ _REPO_ROOT
+ / "superset"
+ / "commands"
+ / "chart"
+ / "_bundles"
+ / "query_context_bundle.js"
+)
+
+
+def _fixture_names() -> list[str]:
+ if not _FORMDATA_DIR.is_dir():
+ return []
+ return sorted(p.stem for p in _FORMDATA_DIR.glob("*.json"))
+
+
+pytestmark = pytest.mark.skipif(
+ not _BUNDLE.exists(),
+ reason=(
+ "query_context bundle not built — run "
+ "`npm run build:backend-querycontext` in superset-frontend/"
+ ),
+)
+
+
+@pytest.mark.parametrize("viz_type", _fixture_names())
+def test_backend_matches_frontend_query_context(viz_type: str) -> None:
+ from superset.commands.chart.query_context_generator import (
+ get_query_context_generator,
+ )
+
+ expected_path = _EXPECTED_DIR / f"{viz_type}.json"
+ if not expected_path.exists():
+ pytest.skip(
+ f"golden missing for {viz_type} — run the jest parity test "
+ "(parity.test.ts) to record it"
+ )
+
+ form_data = json.loads((_FORMDATA_DIR / f"{viz_type}.json").read_text("utf-8"))
+ expected = json.loads(expected_path.read_text("utf-8"))
+
+ generator = get_query_context_generator()
+ actual = generator.generate(viz_type, form_data)
+
+ assert actual is not None, (
+ f"backend generator returned None for {viz_type}; the V8 bundle must "
+ "cover every parity-fixture viz type"
+ )
+ assert actual == expected, (
+ f"backend query_context for {viz_type} diverges from the frontend "
+ "buildQuery golden"
+ )