Skip to content

Commit 4a28125

Browse files
authored
Fix: Ensure that removed models are not referenced downstream during plan creation (#706) (#708)
1 parent b474cca commit 4a28125

File tree

2 files changed

+51
-8
lines changed

2 files changed

+51
-8
lines changed

sqlmesh/core/plan/definition.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def __init__(
115115
self._ensure_valid_date_range(self._start, self._end)
116116
self._ensure_no_forward_only_revert()
117117
self._ensure_no_forward_only_new_models()
118+
self._ensure_no_broken_references()
118119

119120
directly_indirectly_modified = self._build_directly_and_indirectly_modified()
120121
self.directly_modified = directly_indirectly_modified[0]
@@ -540,6 +541,14 @@ def _ensure_no_forward_only_new_models(self) -> None:
540541
if self.forward_only and self.context_diff.added:
541542
raise PlanError("New models can't be added as part of the forward-only plan.")
542543

544+
def _ensure_no_broken_references(self) -> None:
545+
for snapshot in self.context_diff.snapshots.values():
546+
broken_references = self.context_diff.removed & snapshot.model.depends_on
547+
if broken_references:
548+
raise PlanError(
549+
f"Removed models {broken_references} are referenced in model '{snapshot.name}'. Please remove broken references before proceeding."
550+
)
551+
543552

544553
class PlanStatus(str, Enum):
545554
STARTED = "started"

tests/core/test_plan.py

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def test_forward_only_plan_sets_version(make_snapshot, mocker: MockerFixture):
3838

3939
context_diff_mock = mocker.Mock()
4040
context_diff_mock.snapshots = {"a": snapshot_a, "b": snapshot_b}
41-
context_diff_mock.added = {}
41+
context_diff_mock.added = set()
42+
context_diff_mock.removed = set()
4243
context_diff_mock.modified_snapshots = {"b": (snapshot_b, snapshot_b)}
4344
context_diff_mock.new_snapshots = {snapshot_b.snapshot_id: snapshot_b}
4445

@@ -69,7 +70,8 @@ def test_forward_only_dev(make_snapshot, mocker: MockerFixture):
6970

7071
context_diff_mock = mocker.Mock()
7172
context_diff_mock.snapshots = {"a": snapshot_a}
72-
context_diff_mock.added = {}
73+
context_diff_mock.added = set()
74+
context_diff_mock.removed = set()
7375
context_diff_mock.modified_snapshots = {}
7476
context_diff_mock.new_snapshots = {snapshot_a.snapshot_id: snapshot_a}
7577

@@ -101,6 +103,7 @@ def test_forward_only_plan_new_models_not_allowed(make_snapshot, mocker: MockerF
101103
context_diff_mock = mocker.Mock()
102104
context_diff_mock.snapshots = {"a": snapshot_a}
103105
context_diff_mock.added = {"a"}
106+
context_diff_mock.removed = set()
104107
context_diff_mock.modified_snapshots = {}
105108
context_diff_mock.new_snapshots = {}
106109

@@ -133,7 +136,8 @@ def test_paused_forward_only_parent(make_snapshot, mocker: MockerFixture):
133136

134137
context_diff_mock = mocker.Mock()
135138
context_diff_mock.snapshots = {"a": snapshot_a, "b": snapshot_b}
136-
context_diff_mock.added = {}
139+
context_diff_mock.added = set()
140+
context_diff_mock.removed = set()
137141
context_diff_mock.modified_snapshots = {"b": (snapshot_b, snapshot_b)}
138142
context_diff_mock.new_snapshots = {snapshot_b.snapshot_id: snapshot_b}
139143

@@ -170,7 +174,8 @@ def test_restate_model_with_merge_strategy(make_snapshot, mocker: MockerFixture)
170174

171175
context_diff_mock = mocker.Mock()
172176
context_diff_mock.snapshots = {"a": snapshot_a}
173-
context_diff_mock.added = {}
177+
context_diff_mock.added = set()
178+
context_diff_mock.removed = set()
174179
context_diff_mock.modified_snapshots = {}
175180
context_diff_mock.new_snapshots = {}
176181

@@ -190,7 +195,8 @@ def test_new_snapshots_with_restatements(make_snapshot, mocker: MockerFixture):
190195

191196
context_diff_mock = mocker.Mock()
192197
context_diff_mock.snapshots = {"a": snapshot_a}
193-
context_diff_mock.added = {}
198+
context_diff_mock.added = set()
199+
context_diff_mock.removed = set()
194200
context_diff_mock.modified_snapshots = {}
195201
context_diff_mock.new_snapshots = {snapshot_a.snapshot_id: snapshot_a}
196202

@@ -216,7 +222,8 @@ def test_end_validation(make_snapshot, mocker: MockerFixture):
216222

217223
context_diff_mock = mocker.Mock()
218224
context_diff_mock.snapshots = {"a": snapshot_a}
219-
context_diff_mock.added = {}
225+
context_diff_mock.added = set()
226+
context_diff_mock.removed = set()
220227
context_diff_mock.modified_snapshots = {}
221228
context_diff_mock.new_snapshots = {snapshot_a.snapshot_id: snapshot_a}
222229

@@ -272,6 +279,7 @@ def test_forward_only_revert_not_allowed(make_snapshot, mocker: MockerFixture):
272279
context_diff_mock = mocker.Mock()
273280
context_diff_mock.snapshots = {"a": snapshot}
274281
context_diff_mock.added = set()
282+
context_diff_mock.removed = set()
275283
context_diff_mock.modified_snapshots = {"a": (snapshot, forward_only_snapshot)}
276284
context_diff_mock.new_snapshots = {}
277285

@@ -319,7 +327,8 @@ def test_forward_only_plan_seed_models(make_snapshot, mocker: MockerFixture):
319327

320328
context_diff_mock = mocker.Mock()
321329
context_diff_mock.snapshots = {"a": snapshot_a_updated}
322-
context_diff_mock.added = {}
330+
context_diff_mock.added = set()
331+
context_diff_mock.removed = set()
323332
context_diff_mock.modified_snapshots = {"a": (snapshot_a_updated, snapshot_a)}
324333
context_diff_mock.new_snapshots = {snapshot_a_updated.snapshot_id: snapshot_a_updated}
325334

@@ -344,6 +353,7 @@ def test_start_inference(make_snapshot, mocker: MockerFixture):
344353
context_diff_mock = mocker.Mock()
345354
context_diff_mock.snapshots = {"a": snapshot_a, "b": snapshot_b}
346355
context_diff_mock.added = set()
356+
context_diff_mock.removed = set()
347357
context_diff_mock.modified_snapshots = {}
348358
context_diff_mock.new_snapshots = {snapshot_b.snapshot_id: snapshot_b}
349359

@@ -370,6 +380,7 @@ def test_auto_categorization(make_snapshot, mocker: MockerFixture):
370380
context_diff_mock = mocker.Mock()
371381
context_diff_mock.snapshots = {"a": updated_snapshot}
372382
context_diff_mock.added = set()
383+
context_diff_mock.removed = set()
373384
context_diff_mock.modified_snapshots = {"a": (updated_snapshot, snapshot)}
374385
context_diff_mock.new_snapshots = {updated_snapshot.snapshot_id: updated_snapshot}
375386

@@ -399,7 +410,8 @@ def test_end_from_missing_instead_of_now(make_snapshot, mocker: MockerFixture):
399410

400411
context_diff_mock = mocker.Mock()
401412
context_diff_mock.snapshots = {"a": snapshot_a}
402-
context_diff_mock.added = {}
413+
context_diff_mock.added = set()
414+
context_diff_mock.removed = set()
403415
context_diff_mock.modified_snapshots = {}
404416
context_diff_mock.new_snapshots = {snapshot_a.snapshot_id: snapshot_a}
405417

@@ -415,3 +427,25 @@ def test_end_from_missing_instead_of_now(make_snapshot, mocker: MockerFixture):
415427

416428
assert plan.start == to_timestamp(expected_start)
417429
assert plan.end == expected_end
430+
431+
432+
def test_broken_references(make_snapshot, mocker: MockerFixture):
433+
snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("select 2, ds FROM a")))
434+
snapshot_b.set_version()
435+
436+
dag = DAG[str]({"b": set()})
437+
438+
context_diff_mock = mocker.Mock()
439+
context_diff_mock.snapshots = {"b": snapshot_b}
440+
context_diff_mock.added = set()
441+
context_diff_mock.removed = {"a"}
442+
context_diff_mock.modified_snapshots = {}
443+
context_diff_mock.new_snapshots = {}
444+
445+
state_reader_mock = mocker.Mock()
446+
447+
with pytest.raises(
448+
PlanError,
449+
match=r"Removed models {'a'} are referenced in model 'b'.*",
450+
):
451+
Plan(context_diff_mock, dag, state_reader_mock)

0 commit comments

Comments
 (0)