eugeneo17 commented on code in PR #43303:
URL: https://github.com/apache/superset/pull/43303#discussion_r4004771500


##########
superset/commands/chart/importers/v1/utils.py:
##########
@@ -70,6 +77,64 @@ def import_chart(
 
     filter_chart_annotations(config)
 
+    # Synthesize a query_context for imported charts that arrive without one, 
so
+    # 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).
+    #
+    # 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:
+            params = config.get("params") or {}

Review Comment:
   Fixed: params is normalized before use. It is a dict early in the import 
flow but a JSON string once serialized ahead of migration, so synthesis accepts 
either (json.loads on a string, fall back to {} on error).



##########
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,
+)) {

Review Comment:
   Fixed in d7ae729487. Cartodiagram is now mapped via an explicit override in 
the generator. I kept the matcher itself narrow and scoped the change to the 
viz types raised in review.



##########
superset/commands/chart/query_context_builder.py:
##########
@@ -0,0 +1,184 @@
+# 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.
+"""
+Derive a ``query_context`` payload from a chart's stored ``params``.
+
+Charts imported via a v1 ZIP bundle persist with ``Slice.query_context = NULL``
+(the importer never synthesizes one), so the first
+``GET /api/v1/chart/{pk}/data/`` returns HTTP 400 "Chart has no query context
+saved" (Apache Superset #33615). This module builds a valid ``query_context``
+payload from the chart's viz ``params`` + its importer-resolved datasource, so
+the imported row becomes queryable on first read — or classifies the chart
+non-derivable and returns ``None`` (honest-fail; never a fabricated context).
+
+The function is **pure** (no DB / network / analytical-DB access). Its output,
+when passed to ``QueryContextFactory.create(**payload)``, constructs a valid
+``QueryContext``. See ADR-013 (synthesize-at-import) and ADR-014
+(datasource authz/RLS preservation).
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+# Datasource-less viz types render static/markdown content rather than a
+# datasource-backed query; a NULL query_context is the *correct* state for them
+# (FR-003), not a bug. Kept as a small, explicit, conservatively-expanded set.
+_NON_DATASOURCE_VIZ: frozenset[str] = frozenset({"markup", "handlebars", 
"divider"})
+
+# Default row limit mirrors the query-object default used across the read path.
+_DEFAULT_ROW_LIMIT = 5000
+
+
+def _translate_adhoc_filters(
+    adhoc_filters: list[Any] | None,
+) -> tuple[list[dict[str, Any]], list[str], list[str]]:
+    """
+    Translate viz ``adhoc_filters`` into simple ``{col, op, val}`` filters.
+
+    - ``expressionType == "SIMPLE"`` → a ``{col, op, val}`` filter.
+    - ``expressionType == "SQL"``    → routed to ``extras.where`` (or
+      ``extras.having`` for a HAVING clause) so a hand-written SQL predicate is
+      preserved rather than lost.
+    - Anything that cannot be mapped cleanly (missing subject/operator, 
malformed
+      entry) is dropped **without raising** (RISK-T05) — an imported chart must
+      never abort its bundle over a single unmappable filter.
+
+    Returns ``(filters, where_expressions, having_expressions)``.
+    """
+    filters: list[dict[str, Any]] = []
+    where_expressions: list[str] = []
+    having_expressions: list[str] = []
+    for adhoc_filter in adhoc_filters or []:
+        if not isinstance(adhoc_filter, dict):
+            continue
+        expression_type = adhoc_filter.get("expressionType") or "SIMPLE"
+        if expression_type == "SIMPLE":
+            subject = adhoc_filter.get("subject")
+            operator = adhoc_filter.get("operator")
+            if subject and operator:
+                filters.append(
+                    {
+                        "col": subject,
+                        "op": operator,
+                        "val": adhoc_filter.get("comparator"),
+                    }
+                )
+            # else: unmappable SIMPLE filter — dropped, no crash (RISK-T05).
+        elif expression_type == "SQL":
+            sql_expression = adhoc_filter.get("sqlExpression")
+            if sql_expression:
+                clause = (adhoc_filter.get("clause") or "WHERE").upper()
+                if clause == "HAVING":
+                    having_expressions.append(sql_expression)
+                else:
+                    where_expressions.append(sql_expression)
+    return filters, where_expressions, having_expressions
+
+
+def _derive_orderby(params: dict[str, Any]) -> list[list[Any]]:
+    """
+    Best-effort ordering from ``params`` (FR-002).
+
+    Handles an explicit ``orderby`` list (either ``[[col, asc_bool], ...]`` or 
a
+    flat list of expressions) and a single sort metric
+    (``timeseries_limit_metric`` / ``sort_by_metric``). Falls back to no
+    ordering when nothing is derivable — the query builder supplies defaults.
+    """
+    order_asc = not params.get("order_desc", True)
+
+    orderby = params.get("orderby")
+    if isinstance(orderby, list) and orderby:
+        normalized: list[list[Any]] = []
+        for entry in orderby:
+            if isinstance(entry, (list, tuple)) and len(entry) == 2:
+                normalized.append([entry[0], bool(entry[1])])
+            else:
+                normalized.append([entry, order_asc])
+        return normalized
+
+    sort_metric = params.get("timeseries_limit_metric") or 
params.get("sort_by_metric")
+    if sort_metric:
+        return [[sort_metric, order_asc]]
+    return []
+
+
+def build_query_context_config(
+    params: dict[str, Any] | None,
+    viz_type: str,
+    datasource_id: int | None,
+    datasource_type: str = "table",
+) -> dict[str, Any] | None:
+    """
+    Map a chart's ``params`` + resolved datasource to a ``query_context`` 
payload.
+
+    :param params: the chart's viz form-data (a dict at ``import_chart`` time).
+    :param viz_type: the chart's viz type (classification input).
+    :param datasource_id: the importer-resolved datasource id. **Datasource is
+        taken from this argument only, never from ``params`` (ADR-014 / 
RISK-T02)**
+        so the synthesized context names the same real datasource the authz 
layer
+        vets at read time.
+    :param datasource_type: the datasource type — ``"table"`` on import.
+    :returns: a ``query_context`` payload dict whose keys are the kwargs of
+        ``QueryContextFactory.create``, or ``None`` when the chart is
+        non-derivable (FR-003): no datasource, nothing to query, or a
+        datasource-less viz type. Never returns a fabricated/invalid context.
+    """
+    params = params or {}
+
+    # FR-003 non-derivable classification — return None, importer leaves NULL.
+    if not datasource_id or viz_type in _NON_DATASOURCE_VIZ:
+        return None
+    metrics = params.get("metrics") or []

Review Comment:
   Fixed: the singular `metric` is normalized into `metrics` before the 
emptiness check, so Big Number (`metric: count`) is derivable. Covered by 
test_singular_metric_is_normalized_and_derivable.



##########
superset/commands/chart/query_context_builder.py:
##########
@@ -0,0 +1,184 @@
+# 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.
+"""
+Derive a ``query_context`` payload from a chart's stored ``params``.
+
+Charts imported via a v1 ZIP bundle persist with ``Slice.query_context = NULL``
+(the importer never synthesizes one), so the first
+``GET /api/v1/chart/{pk}/data/`` returns HTTP 400 "Chart has no query context
+saved" (Apache Superset #33615). This module builds a valid ``query_context``
+payload from the chart's viz ``params`` + its importer-resolved datasource, so
+the imported row becomes queryable on first read — or classifies the chart
+non-derivable and returns ``None`` (honest-fail; never a fabricated context).
+
+The function is **pure** (no DB / network / analytical-DB access). Its output,
+when passed to ``QueryContextFactory.create(**payload)``, constructs a valid
+``QueryContext``. See ADR-013 (synthesize-at-import) and ADR-014
+(datasource authz/RLS preservation).
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+# Datasource-less viz types render static/markdown content rather than a
+# datasource-backed query; a NULL query_context is the *correct* state for them
+# (FR-003), not a bug. Kept as a small, explicit, conservatively-expanded set.
+_NON_DATASOURCE_VIZ: frozenset[str] = frozenset({"markup", "handlebars", 
"divider"})
+
+# Default row limit mirrors the query-object default used across the read path.
+_DEFAULT_ROW_LIMIT = 5000
+
+
+def _translate_adhoc_filters(
+    adhoc_filters: list[Any] | None,
+) -> tuple[list[dict[str, Any]], list[str], list[str]]:
+    """
+    Translate viz ``adhoc_filters`` into simple ``{col, op, val}`` filters.
+
+    - ``expressionType == "SIMPLE"`` → a ``{col, op, val}`` filter.
+    - ``expressionType == "SQL"``    → routed to ``extras.where`` (or
+      ``extras.having`` for a HAVING clause) so a hand-written SQL predicate is
+      preserved rather than lost.
+    - Anything that cannot be mapped cleanly (missing subject/operator, 
malformed
+      entry) is dropped **without raising** (RISK-T05) — an imported chart must
+      never abort its bundle over a single unmappable filter.
+
+    Returns ``(filters, where_expressions, having_expressions)``.
+    """
+    filters: list[dict[str, Any]] = []
+    where_expressions: list[str] = []
+    having_expressions: list[str] = []
+    for adhoc_filter in adhoc_filters or []:
+        if not isinstance(adhoc_filter, dict):
+            continue
+        expression_type = adhoc_filter.get("expressionType") or "SIMPLE"
+        if expression_type == "SIMPLE":
+            subject = adhoc_filter.get("subject")
+            operator = adhoc_filter.get("operator")
+            if subject and operator:
+                filters.append(
+                    {
+                        "col": subject,
+                        "op": operator,
+                        "val": adhoc_filter.get("comparator"),
+                    }
+                )
+            # else: unmappable SIMPLE filter — dropped, no crash (RISK-T05).
+        elif expression_type == "SQL":
+            sql_expression = adhoc_filter.get("sqlExpression")
+            if sql_expression:
+                clause = (adhoc_filter.get("clause") or "WHERE").upper()
+                if clause == "HAVING":
+                    having_expressions.append(sql_expression)
+                else:
+                    where_expressions.append(sql_expression)
+    return filters, where_expressions, having_expressions
+
+
+def _derive_orderby(params: dict[str, Any]) -> list[list[Any]]:
+    """
+    Best-effort ordering from ``params`` (FR-002).
+
+    Handles an explicit ``orderby`` list (either ``[[col, asc_bool], ...]`` or 
a
+    flat list of expressions) and a single sort metric
+    (``timeseries_limit_metric`` / ``sort_by_metric``). Falls back to no
+    ordering when nothing is derivable — the query builder supplies defaults.
+    """
+    order_asc = not params.get("order_desc", True)
+
+    orderby = params.get("orderby")
+    if isinstance(orderby, list) and orderby:
+        normalized: list[list[Any]] = []
+        for entry in orderby:
+            if isinstance(entry, (list, tuple)) and len(entry) == 2:
+                normalized.append([entry[0], bool(entry[1])])
+            else:
+                normalized.append([entry, order_asc])
+        return normalized
+
+    sort_metric = params.get("timeseries_limit_metric") or 
params.get("sort_by_metric")
+    if sort_metric:
+        return [[sort_metric, order_asc]]
+    return []
+
+
+def build_query_context_config(
+    params: dict[str, Any] | None,
+    viz_type: str,
+    datasource_id: int | None,
+    datasource_type: str = "table",
+) -> dict[str, Any] | None:
+    """
+    Map a chart's ``params`` + resolved datasource to a ``query_context`` 
payload.
+
+    :param params: the chart's viz form-data (a dict at ``import_chart`` time).
+    :param viz_type: the chart's viz type (classification input).
+    :param datasource_id: the importer-resolved datasource id. **Datasource is
+        taken from this argument only, never from ``params`` (ADR-014 / 
RISK-T02)**
+        so the synthesized context names the same real datasource the authz 
layer
+        vets at read time.
+    :param datasource_type: the datasource type — ``"table"`` on import.
+    :returns: a ``query_context`` payload dict whose keys are the kwargs of
+        ``QueryContextFactory.create``, or ``None`` when the chart is
+        non-derivable (FR-003): no datasource, nothing to query, or a
+        datasource-less viz type. Never returns a fabricated/invalid context.
+    """
+    params = params or {}
+
+    # FR-003 non-derivable classification — return None, importer leaves NULL.
+    if not datasource_id or viz_type in _NON_DATASOURCE_VIZ:
+        return None
+    metrics = params.get("metrics") or []
+    # `groupby` is the deprecated alias of `columns`.
+    columns = params.get("columns") or params.get("groupby") or []
+    if not metrics and not columns:
+        return None
+
+    filters, where_expressions, having_expressions = _translate_adhoc_filters(
+        params.get("adhoc_filters", [])
+    )
+
+    query_object = {
+        "time_range": params.get("time_range", " : "),
+        "granularity": params.get("granularity_sqla") or 
params.get("granularity"),
+        "filters": filters,
+        "extras": {
+            "time_grain_sqla": params.get("time_grain_sqla"),
+            "having": " AND ".join(having_expressions),
+            "where": " AND ".join(where_expressions),

Review Comment:
   Fixed: _translate_adhoc_filters delegates to 
split_adhoc_filters_into_base_filters, so each predicate is parenthesized 
(preserving OR precedence) and a trailing line comment is neutralized. Covered 
by test_adhoc_sql_or_predicate_is_parenthesized and 
test_adhoc_sql_trailing_comment_is_neutralized.



##########
superset/commands/chart/query_context_builder.py:
##########
@@ -0,0 +1,184 @@
+# 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.
+"""
+Derive a ``query_context`` payload from a chart's stored ``params``.
+
+Charts imported via a v1 ZIP bundle persist with ``Slice.query_context = NULL``
+(the importer never synthesizes one), so the first
+``GET /api/v1/chart/{pk}/data/`` returns HTTP 400 "Chart has no query context
+saved" (Apache Superset #33615). This module builds a valid ``query_context``
+payload from the chart's viz ``params`` + its importer-resolved datasource, so
+the imported row becomes queryable on first read — or classifies the chart
+non-derivable and returns ``None`` (honest-fail; never a fabricated context).
+
+The function is **pure** (no DB / network / analytical-DB access). Its output,
+when passed to ``QueryContextFactory.create(**payload)``, constructs a valid
+``QueryContext``. See ADR-013 (synthesize-at-import) and ADR-014
+(datasource authz/RLS preservation).
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+# Datasource-less viz types render static/markdown content rather than a
+# datasource-backed query; a NULL query_context is the *correct* state for them
+# (FR-003), not a bug. Kept as a small, explicit, conservatively-expanded set.
+_NON_DATASOURCE_VIZ: frozenset[str] = frozenset({"markup", "handlebars", 
"divider"})

Review Comment:
   Fixed: handlebars is no longer in the datasource-less set; it has a real 
buildQuery and a golden, so it stays on the derivable path. Covered by 
test_handlebars_is_derivable.



##########
superset/cli/charts.py:
##########
@@ -0,0 +1,158 @@
+# 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
+from typing import Any, Optional
+
+import click
+from flask.cli import with_appcontext
+
+logger = logging.getLogger(__name__)
+
+
[email protected]()
+def charts() -> None:
+    """Chart-related maintenance commands."""
+
+
+def _derive_query_context(chart: Any, generator: Any) -> Optional[dict[str, 
Any]]:
+    """
+    Derive a ``query_context`` config for ``chart``, or ``None`` if 
non-derivable.
+
+    Prefers the authoritative frontend ``buildQuery`` (V8) and falls back to 
the
+    pure-Python generic derivation; the datasource is taken from the chart's 
own
+    resolved id/type, never from ``params`` (authz-preserving).
+    """
+    from superset.commands.chart.query_context_builder import (
+        build_query_context_config,
+    )
+    from superset.utils import json
+
+    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
+        )
+    return context
+
+
[email protected]("backfill-query-context")
+@with_appcontext
[email protected](
+    "--dry-run",
+    is_flag=True,
+    default=False,
+    help="Report what would change without writing.",
+)
[email protected](
+    "--viz-type",
+    "viz_types",
+    multiple=True,
+    help="Restrict to these viz types (repeatable). Default: all.",
+)
[email protected](
+    "--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_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:
+            context = _derive_query_context(chart, generator)
+        except Exception as ex:  # pylint: disable=broad-except
+            errors += 1
+            logger.warning(
+                "backfill-query-context: chart id=%s failed: %s", chart.id, ex
+            )
+            continue
+
+        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()  # pylint: disable=consider-using-transaction

Review Comment:
   Fixed: the backfill no longer streams with yield_per and commits 
mid-iteration. It snapshots candidate ids up front and pages by id, so 
committing between batches is safe on PostgreSQL.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to