Skip to content

Commit c932559

Browse files
authored
Fix: Ignore non-restateable models and print out a warning instead of failing the plan (#2605)
1 parent 757bd86 commit c932559

File tree

3 files changed

+91
-20
lines changed

3 files changed

+91
-20
lines changed

sqlmesh/core/context.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,10 +1004,6 @@ def plan_builder(
10041004
expanded_restate_models = None
10051005
if restate_models is not None:
10061006
expanded_restate_models = model_selector.expand_model_selections(restate_models)
1007-
if not expanded_restate_models:
1008-
self.console.log_error(
1009-
f"Provided restated models do not match any models. No models will be included in plan. Provided: {', '.join(restate_models)}"
1010-
)
10111007

10121008
snapshots = self._snapshots(models_override)
10131009
context_diff = self._context_diff(
@@ -1083,6 +1079,7 @@ def plan_builder(
10831079
),
10841080
end_bounded=not run,
10851081
ensure_finalized_snapshots=self.config.plan.use_finalized_state,
1082+
console=self.console,
10861083
)
10871084

10881085
def apply(

sqlmesh/core/plan/builder.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import re
44
import sys
55
import typing as t
6+
import logging
67
from collections import defaultdict
78
from datetime import datetime
89
from functools import cached_property
910

11+
from sqlmesh.core.console import Console, get_console
1012
from sqlmesh.core.config import (
1113
AutoCategorizationMode,
1214
CategorizerConfig,
@@ -28,6 +30,9 @@
2830
from sqlmesh.utils.errors import NoChangesPlanError, PlanError, SQLMeshError
2931

3032

33+
logger = logging.getLogger(__name__)
34+
35+
3136
class PlanBuilder:
3237
"""Plan Builder constructs a Plan based on user choices for how they want to backfill, preview, etc. their changes.
3338
@@ -88,6 +93,7 @@ def __init__(
8893
enable_preview: bool = False,
8994
end_bounded: bool = False,
9095
ensure_finalized_snapshots: bool = False,
96+
console: t.Optional[Console] = None,
9197
):
9298
self._context_diff = context_diff
9399
self._no_gaps = no_gaps
@@ -101,12 +107,13 @@ def __init__(
101107
self._categorizer_config = categorizer_config or CategorizerConfig()
102108
self._auto_categorization_enabled = auto_categorization_enabled
103109
self._include_unmodified = include_unmodified
104-
self._restate_models = set(restate_models or [])
110+
self._restate_models = set(restate_models) if restate_models is not None else None
105111
self._effective_from = effective_from
106112
self._execution_time = execution_time
107113
self._backfill_models = backfill_models
108114
self._end = end or default_end
109115
self._apply = apply
116+
self._console = console or get_console()
110117

111118
self._start = start
112119
if not self._start and self._forward_only_preview_needed:
@@ -286,9 +293,15 @@ def is_restateable_snapshot(snapshot: Snapshot) -> bool:
286293
return False
287294
return not snapshot.is_symbolic and not snapshot.is_seed
288295

289-
restatements: t.Dict[SnapshotId, Interval] = {}
290-
dummy_interval = (sys.maxsize, -sys.maxsize)
291296
restate_models = self._restate_models
297+
if restate_models == set():
298+
# This is a warning but we print this as error since the Console is lacking API for warnings.
299+
self._console.log_error(
300+
"Provided restated models do not match any models. No models will be included in plan."
301+
)
302+
return {}
303+
304+
restatements: t.Dict[SnapshotId, Interval] = {}
292305
forward_only_preview_needed = self._forward_only_preview_needed
293306
if not restate_models and forward_only_preview_needed:
294307
# Add model names for new forward-only snapshots to the restatement list
@@ -298,18 +311,27 @@ def is_restateable_snapshot(snapshot: Snapshot) -> bool:
298311
for s in self._context_diff.new_snapshots.values()
299312
if s.is_materialized and (self._forward_only or s.model.forward_only)
300313
}
314+
301315
if not restate_models:
302-
return restatements
316+
return {}
303317

304318
# Add restate snapshots and their downstream snapshots
319+
dummy_interval = (sys.maxsize, -sys.maxsize)
305320
for model_fqn in restate_models:
306321
snapshot = self._model_fqn_to_snapshot.get(model_fqn)
307-
if not snapshot or (
308-
not forward_only_preview_needed and not is_restateable_snapshot(snapshot)
309-
):
310-
raise PlanError(
311-
f"Cannot restate from '{model_fqn}'. Either such model doesn't exist, no other materialized model references it, or restatement was disabled for this model."
312-
)
322+
if not snapshot:
323+
raise PlanError(f"Cannot restate model '{model_fqn}'. Model does not exist.")
324+
if not forward_only_preview_needed:
325+
if not self._is_dev and snapshot.disable_restatement:
326+
# This is a warning but we print this as error since the Console is lacking API for warnings.
327+
self._console.log_error(
328+
f"Cannot restate model '{model_fqn}'. Restatement is disabled for this model."
329+
)
330+
continue
331+
elif snapshot.is_symbolic or snapshot.is_seed:
332+
logger.info("Skipping restatement for model '%s'", model_fqn)
333+
continue
334+
313335
restatements[snapshot.snapshot_id] = dummy_interval
314336
for downstream_s_id in dag.downstream(snapshot.snapshot_id):
315337
if is_restateable_snapshot(self._context_diff.snapshots[downstream_s_id]):
@@ -632,7 +654,9 @@ def _ensure_no_broken_references(self) -> None:
632654
)
633655

634656
def _ensure_no_new_snapshots_with_restatements(self) -> None:
635-
if self._restate_models and self._context_diff.new_snapshots:
657+
if self._restate_models is not None and (
658+
self._context_diff.new_snapshots or self._context_diff.modified_snapshots
659+
):
636660
raise PlanError(
637661
"Model changes and restatements can't be a part of the same plan. "
638662
"Revert or apply changes before proceeding with restatements."

tests/core/test_plan.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def test_restate_models_with_existing_missing_intervals(sushi_context: Context):
357357
assert plan.requires_backfill
358358

359359

360-
def test_restate_model_with_merge_strategy(make_snapshot, mocker: MockerFixture):
360+
def test_restate_symbolic_model(make_snapshot, mocker: MockerFixture):
361361
snapshot_a = make_snapshot(
362362
SqlModel(
363363
name="a",
@@ -381,11 +381,61 @@ def test_restate_model_with_merge_strategy(make_snapshot, mocker: MockerFixture)
381381
previous_finalized_snapshots=None,
382382
)
383383

384+
plan = PlanBuilder(context_diff, restate_models=[snapshot_a.name]).build()
385+
assert not plan.restatements
386+
387+
388+
def test_restate_seed_model(make_snapshot, mocker: MockerFixture):
389+
snapshot_a = make_snapshot(
390+
SeedModel(
391+
name="a",
392+
kind=SeedKind(path="./path/to/seed"),
393+
seed=Seed(content="new_content"),
394+
column_hashes={"col": "hash2"},
395+
depends_on=set(),
396+
)
397+
)
398+
399+
context_diff = ContextDiff(
400+
environment="test_environment",
401+
is_new_environment=True,
402+
is_unfinalized_environment=False,
403+
create_from="prod",
404+
added=set(),
405+
removed_snapshots={},
406+
modified_snapshots={},
407+
snapshots={snapshot_a.snapshot_id: snapshot_a},
408+
new_snapshots={},
409+
previous_plan_id=None,
410+
previously_promoted_snapshot_ids=set(),
411+
previous_finalized_snapshots=None,
412+
)
413+
414+
plan = PlanBuilder(context_diff, restate_models=[snapshot_a.name]).build()
415+
assert not plan.restatements
416+
417+
418+
def test_restate_missing_model(make_snapshot, mocker: MockerFixture):
419+
context_diff = ContextDiff(
420+
environment="test_environment",
421+
is_new_environment=True,
422+
is_unfinalized_environment=False,
423+
create_from="prod",
424+
added=set(),
425+
removed_snapshots={},
426+
modified_snapshots={},
427+
snapshots={},
428+
new_snapshots={},
429+
previous_plan_id=None,
430+
previously_promoted_snapshot_ids=set(),
431+
previous_finalized_snapshots=None,
432+
)
433+
384434
with pytest.raises(
385435
PlanError,
386-
match="Cannot restate from 'a'. Either such model doesn't exist, no other materialized model references it.*",
436+
match=r"Cannot restate model 'missing'. Model does not exist.",
387437
):
388-
PlanBuilder(context_diff, restate_models=["a"]).build()
438+
PlanBuilder(context_diff, restate_models=["missing"]).build()
389439

390440

391441
def test_new_snapshots_with_restatements(make_snapshot, mocker: MockerFixture):
@@ -1111,8 +1161,8 @@ def test_disable_restatement(make_snapshot, mocker: MockerFixture):
11111161
previous_finalized_snapshots=None,
11121162
)
11131163

1114-
with pytest.raises(PlanError, match="""Cannot restate from '"a"'.*"""):
1115-
PlanBuilder(context_diff, restate_models=['"a"']).build()
1164+
plan = PlanBuilder(context_diff, restate_models=['"a"']).build()
1165+
assert not plan.restatements
11161166

11171167
# Effective from doesn't apply to snapshots for which restatements are disabled.
11181168
plan = PlanBuilder(context_diff, forward_only=True, effective_from="2023-01-01").build()

0 commit comments

Comments
 (0)