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:
sqlmesh plan --auto-apply — build the default (prod)
- Change a model, then
sqlmesh plan devenv --skip-backfill --auto-apply — the new snapshot version is stored in devenv with zero intervals.
- Restore the file so the project matches
prod again.
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
What happened
Restating a model in the prod environment crashes at the interval-removal step with:
The model batches have already been backfilled and committed by this point (
Model batches executedprints just before the crash), and state is left clean — a follow-upsqlmesh planreports "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_ssqlmesh-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:Inside
remove_intervalswithremove_shared_versions=True(sqlmesh/core/state_sync/db/interval.py), the rows to write are rebuilt by querying the_intervalstable for rows sharing those name/versions:If every affected snapshot has no rows in
_intervalsanother environment with--skip-backfill, or is otherwise an un-backfilled preview version — thenall_snapshotsis empty,intervals_to_removeis empty,_intervals_to_dfreturns an empty DataFrame, andinsert_appendraises. 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_intervalschecksif snapshots_intervals:and_push_snapshot_intervalschecksif 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_appendinremove_intervals:Reproduce
sqlmesh-restate-empty-intervals.zip
Minimal standalone reproduction attached (
sqlmesh-restatereeDuckDB models, areproduce.shthat needs onlyuv,plus the full traceback. In short:sqlmesh plan --auto-apply— build the default (prod)sqlmesh plan devenv --skip-backfill --auto-apply— the new snapshot version is stored indevenvwith zero intervals.prodagain.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
mainas of 20