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_s

sqlmesh-restate-empty-intervals.zip

intervals in other environments so a later run therere-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: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:

```python
# 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
    )

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
    target_columns_to_types=self._interval_columns_to_types,
    track_rows_processed=False,
)

If every affected snapshot has no rows in _intervals 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:. `reh 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

sqlmesh-restate-empty-intervals.zip

Minimal standalone reproduction attached (sqlmesh-restateree DuckDB models, a reproduce.sh that needs only uv,plus the full traceback. In short:

  1. sqlmesh plan --auto-apply — build the default (prod)
  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) → rempy:115) → insert_append (base.py:1508).

Versions

  • SQLMesh: 0.235.3 (call site unchanged on main as of 20
  • Engine: DuckDB
  • Python: 3.13

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