Skip to content

Commit 64af77b

Browse files
authored
Fix: Snapshots promoted in prod shouldn't be restated in dev (#3843)
1 parent 2c5d34a commit 64af77b

File tree

3 files changed

+81
-3
lines changed

3 files changed

+81
-3
lines changed

sqlmesh/core/plan/builder.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,14 @@ def is_restateable_snapshot(snapshot: Snapshot) -> bool:
329329
if not snapshot:
330330
raise PlanError(f"Cannot restate model '{model_fqn}'. Model does not exist.")
331331
if not forward_only_preview_needed:
332-
if (not self._is_dev or not snapshot.is_paused) and snapshot.disable_restatement:
333-
# This is a warning but we print this as error since the Console is lacking API for warnings.
334-
self._console.log_error(
332+
if self._is_dev and not snapshot.is_paused:
333+
self._console.log_warning(
334+
f"Cannot restate model '{model_fqn}' because the current version is used in production. "
335+
"Run the restatement against the production environment instead to restate this model."
336+
)
337+
continue
338+
elif (not self._is_dev or not snapshot.is_paused) and snapshot.disable_restatement:
339+
self._console.log_warning(
335340
f"Cannot restate model '{model_fqn}'. "
336341
"Restatement is disabled for this model to prevent possible data loss."
337342
"If you want to restate this model, change the model's `disable_restatement` setting to `false`."

tests/core/test_integration.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2983,6 +2983,23 @@ def test_prod_restatement_plan_missing_model_in_dev(
29832983
)
29842984

29852985

2986+
@time_machine.travel("2023-01-08 15:00:00 UTC")
2987+
def test_dev_restatement_of_prod_model(init_and_plan_context: t.Callable):
2988+
context, plan = init_and_plan_context("examples/sushi")
2989+
context.apply(plan)
2990+
2991+
model = context.get_model("sushi.waiter_revenue_by_day")
2992+
context.upsert_model(add_projection_to_model(t.cast(SqlModel, model)))
2993+
2994+
context.plan("dev", auto_apply=True, no_prompts=True, skip_tests=True)
2995+
2996+
restatement_plan = context.plan_builder("dev", restate_models=["*"]).build()
2997+
assert set(restatement_plan.restatements) == {
2998+
context.get_snapshot("sushi.waiter_revenue_by_day").snapshot_id,
2999+
context.get_snapshot("sushi.top_waiters").snapshot_id,
3000+
}
3001+
3002+
29863003
@time_machine.travel("2023-01-08 15:00:00 UTC")
29873004
def test_plan_snapshot_table_exists_for_promoted_snapshot(init_and_plan_context: t.Callable):
29883005
context, plan = init_and_plan_context("examples/sushi")

tests/core/test_plan.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2697,3 +2697,59 @@ def test_unaligned_start_model_with_forward_only_preview(make_snapshot):
26972697
assert set(plan.restatements) == {new_snapshot_a.snapshot_id, snapshot_b.snapshot_id}
26982698
assert not plan.deployability_index.is_deployable(new_snapshot_a)
26992699
assert not plan.deployability_index.is_deployable(snapshot_b)
2700+
2701+
2702+
def test_restate_production_model_in_dev(make_snapshot, mocker: MockerFixture):
2703+
snapshot = make_snapshot(
2704+
SqlModel(
2705+
name="test_model_a",
2706+
dialect="duckdb",
2707+
query=parse_one("select 1, ds"),
2708+
kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"),
2709+
)
2710+
)
2711+
2712+
prod_snapshot = make_snapshot(
2713+
SqlModel(
2714+
name="test_model_b",
2715+
dialect="duckdb",
2716+
query=parse_one("select 2, ds"),
2717+
kind=dict(name=ModelKindName.INCREMENTAL_BY_TIME_RANGE, time_column="ds"),
2718+
)
2719+
)
2720+
prod_snapshot.unpaused_ts = 1
2721+
2722+
context_diff = ContextDiff(
2723+
environment="test_environment",
2724+
is_new_environment=False,
2725+
is_unfinalized_environment=True,
2726+
normalize_environment_name=True,
2727+
create_from="prod",
2728+
create_from_env_exists=True,
2729+
added=set(),
2730+
removed_snapshots={},
2731+
modified_snapshots={},
2732+
snapshots={snapshot.snapshot_id: snapshot, prod_snapshot.snapshot_id: prod_snapshot},
2733+
new_snapshots={},
2734+
previous_plan_id=None,
2735+
previously_promoted_snapshot_ids=set(),
2736+
previous_finalized_snapshots=None,
2737+
)
2738+
2739+
mock_console = mocker.Mock()
2740+
2741+
plan = PlanBuilder(
2742+
context_diff,
2743+
DuckDBEngineAdapter.SCHEMA_DIFFER,
2744+
is_dev=True,
2745+
restate_models={snapshot.name, prod_snapshot.name},
2746+
console=mock_console,
2747+
).build()
2748+
2749+
assert len(plan.restatements) == 1
2750+
assert prod_snapshot.snapshot_id not in plan.restatements
2751+
2752+
mock_console.log_warning.assert_called_once_with(
2753+
"Cannot restate model '\"test_model_b\"' because the current version is used in production. "
2754+
"Run the restatement against the production environment instead to restate this model."
2755+
)

0 commit comments

Comments
 (0)