Skip to content

remove_intervals raises "Cannot construct source query from an empty DataFrame" during a prod restatement when another environment holds an un-backfilled snapshot version #5909

Description

@mitches-got-glitches

What happened

Restating a model in the prod environment crashes at the interval-removal step with:

Error: Cannot construct source query from an empty DataFrame. This error is commonly related to Python models that produce no data. For such models, consider yielding from an empty generator if the resulting set is empty, i.e. use `yield from ()`.

The model batches have already been backfilled and committed by this point (Model batches executed prints just before the crash), and state is left clean — a follow-up sqlmesh plan reports "No changes to plan". But the CLI exits non-zero, which aborts any script wrapping the restatement.

Root cause

When restating prod, PlanEvaluator.visit_restatement_stage clears the restated intervals in other environments so a later run there re-populates them, via identify_restatement_intervals_across_snapshot_versions (sqlmesh/core/plan/common.py). That function deliberately collects only snapshots whose version differs from prod (it skips anything sharing prod's version), then:

# sqlmesh/core/plan/evaluator.py
if filtered_intervals_to_clear:
    self.state_sync.remove_intervals(
        snapshot_intervals=filtered_intervals_to_clear,
        remove_shared_versions=plan.is_prod,   # True for prod
    )

Inside remove_intervals with remove_shared_versions=True (sqlmesh/core/state_sync/db/interval.py), the rows to write are rebuilt by querying the _intervals table for rows sharing those name/versions:

intervals_to_remove = [
    (snapshot, name_version_mapping[snapshot.name_version])
    for snapshot in all_snapshots            # <- from SELECT ... FROM _intervals
]
# ...
self.engine_adapter.insert_append(
    self.intervals_table,
    _intervals_to_df(intervals_to_remove, is_dev=False, is_removed=True),
    target_columns_to_types=self._interval_columns_to_types,
    track_rows_processed=False,
)

If every affected snapshot has no rows in _intervals — e.g. it was promoted to another environment with --skip-backfill, or is otherwise an un-backfilled preview version — then all_snapshots is empty, intervals_to_remove is empty, _intervals_to_df returns an empty DataFrame, and insert_append raises. The debug log shows the tell: Removing interval for snapshots: with nothing after the colon.

The sibling insert paths in the same file already guard this: add_snapshots_intervals checks if snapshots_intervals: and _push_snapshot_intervals checks if new_intervals:. remove_intervals has no such guard.

Expected

An empty set of intervals to remove is a no-op, not an error — matching the guards already on the neighbouring insert paths.

Suggested fix

Skip the write when there's nothing to remove, e.g. before the insert_append in remove_intervals:

if not intervals_to_remove:
    return

Reproduce

A minimal standalone reproduction is attached at the bottom of this issue: three DuckDB models, a reproduce.sh that needs only uv, plus the full traceback. In short:

  1. sqlmesh plan --auto-apply — build the default (prod) environment.
  2. Change a model, then sqlmesh plan devenv --skip-backfill --auto-apply — the new snapshot version is stored in devenv with zero intervals.
  3. Restore the file so the project matches prod again.
  4. sqlmesh plan --restate-model <model> --auto-apply — the model backfills, then the crash fires in post-backfill interval bookkeeping.

Full stack: RestatementStage (evaluator.py:347) → remove_intervals (interval.py:115) → insert_append (base.py:1508).

Versions

  • SQLMesh: 0.235.3 (reproduced). The call site is unchanged on main and in the latest release, 0.236.1, as of 2026-08-03.
  • Engine: DuckDB
  • Python: 3.13
  • OS: macOS (not OS-specific — the crash is in engine-agnostic state sync)

Attachment

sqlmesh-restate-empty-intervals.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions