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"
+    )