Skip to content

Commit 38afca9

Browse files
committed
fix(janitor): include previous finalized snapshots in scoped cleanup
When the invalidated environment is unfinalized, it still points at the snapshots of its last finalized plan. Include them in the scoped cleanup target, mirroring how SnapshotState.get_expired_snapshots treats them. Also fold the duplicated global and scoped delete_expired_snapshots calls into one, and revert an unrelated rewrite of the GitHub controller test. Signed-off-by: mday-io <mdaytn@gmail.com>
1 parent 613a0e7 commit 38afca9

3 files changed

Lines changed: 72 additions & 27 deletions

File tree

‎sqlmesh/core/context.py‎

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3185,7 +3185,17 @@ def _run_janitor(
31853185
if expired_environments:
31863186
expired_env = self.state_reader.get_environment(expired_environments[0].name)
31873187
if expired_env:
3188-
target_snapshot_ids = {s.snapshot_id for s in expired_env.snapshots}
3188+
# An unfinalized environment may still point at the snapshots of its last
3189+
# finalized plan, which would otherwise be left behind.
3190+
expired_env_snapshots = (
3191+
expired_env.snapshots
3192+
if expired_env.finalized_ts is not None
3193+
else [
3194+
*expired_env.snapshots,
3195+
*(expired_env.previous_finalized_snapshots or []),
3196+
]
3197+
)
3198+
target_snapshot_ids = {s.snapshot_id for s in expired_env_snapshots}
31893199

31903200
# Clean up expired environments by removing their views and schemas
31913201
failures.extend(
@@ -3194,28 +3204,19 @@ def _run_janitor(
31943204
)
31953205
)
31963206

3197-
if environment is None:
3198-
failures.extend(
3199-
delete_expired_snapshots(
3200-
self.state_sync,
3201-
self.snapshot_evaluator,
3202-
current_ts=current_ts,
3203-
ignore_ttl=ignore_ttl,
3204-
force_delete=force_delete,
3205-
console=self.console,
3206-
batch_size=self.config.janitor.expired_snapshots_batch_size,
3207-
)
3208-
)
3209-
self.state_sync.compact_intervals()
3210-
elif (
3211-
ignore_ttl
3212-
and target_snapshot_ids
3207+
scoped_cleanup = (
3208+
environment is not None
3209+
and ignore_ttl
3210+
and bool(target_snapshot_ids)
32133211
and not self.state_reader.get_environment(environment)
3214-
):
3212+
)
3213+
if scoped_cleanup:
32153214
self.console.log_warning(
32163215
"Scoped snapshot cleanup will permanently delete unreferenced physical snapshot "
32173216
f"tables formerly referenced by environment '{environment}'."
32183217
)
3218+
3219+
if environment is None or scoped_cleanup:
32193220
failures.extend(
32203221
delete_expired_snapshots(
32213222
self.state_sync,
@@ -3225,10 +3226,13 @@ def _run_janitor(
32253226
force_delete=force_delete,
32263227
console=self.console,
32273228
batch_size=self.config.janitor.expired_snapshots_batch_size,
3228-
target_snapshot_ids=target_snapshot_ids,
3229+
target_snapshot_ids=target_snapshot_ids if scoped_cleanup else None,
32293230
)
32303231
)
32313232

3233+
if environment is None:
3234+
self.state_sync.compact_intervals()
3235+
32323236
if failures:
32333237
failure_string = "\n - ".join(failures)
32343238
summary = f"Janitor completed with failures:\n {failure_string}"

‎tests/core/integration/test_aux_commands.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,49 @@ def test_janitor_environment_ignore_ttl_cleans_only_scoped_snapshots(
599599
assert ctx.state_sync.get_snapshots([dev_b_snapshot.snapshot_id])
600600

601601

602+
def test_invalidate_environment_cleanup_snapshots_includes_previous_finalized(tmp_path: Path):
603+
"""An unfinalized environment's previous finalized snapshots are cleaned up too."""
604+
models_dir = tmp_path / "models"
605+
models_dir.mkdir()
606+
model_path = models_dir / "model1.sql"
607+
model_path.write_text("MODEL(name test.model1, kind FULL); SELECT 1 AS col")
608+
609+
ctx = Context(
610+
paths=[tmp_path],
611+
config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")),
612+
)
613+
614+
ctx.plan("dev", no_prompts=True, auto_apply=True)
615+
old_snapshot = ctx.get_snapshot("test.model1")
616+
assert old_snapshot is not None
617+
618+
model_path.write_text("MODEL(name test.model1, kind FULL); SELECT 2 AS col")
619+
ctx.load()
620+
ctx.plan("dev", no_prompts=True, auto_apply=True)
621+
new_snapshot = ctx.get_snapshot("test.model1")
622+
assert new_snapshot is not None
623+
assert old_snapshot.snapshot_id != new_snapshot.snapshot_id
624+
625+
# Simulate a plan that failed partway: the environment is left unfinalized and still
626+
# points at the snapshots from its last finalized plan.
627+
dev_env = ctx.state_sync.get_environment("dev")
628+
assert dev_env is not None
629+
ctx.state_sync.state_sync.environment_state.update_environment( # type: ignore
630+
dev_env.copy(
631+
update={
632+
"finalized_ts": None,
633+
"previous_finalized_snapshots": [old_snapshot.table_info],
634+
}
635+
)
636+
)
637+
638+
ctx.invalidate_environment("dev", cleanup_snapshots=True)
639+
640+
assert ctx.state_sync.get_environment("dev") is None
641+
assert not ctx.state_sync.get_snapshots([old_snapshot.snapshot_id])
642+
assert not ctx.state_sync.get_snapshots([new_snapshot.snapshot_id])
643+
644+
602645
@time_machine.travel("2023-01-08 15:00:00 UTC")
603646
def test_evaluate_uncategorized_snapshot(init_and_plan_context: t.Callable):
604647
context, plan = init_and_plan_context("examples/sushi")

‎tests/integrations/github/cicd/test_github_controller.py‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -578,23 +578,21 @@ def test_try_invalidate_pr_environment(github_client, make_controller, mocker: M
578578
invalidate_controller = make_controller(
579579
"tests/fixtures/github/pull_request_synchronized.json", github_client
580580
)
581-
invalidate_environment = mocker.patch.object(
582-
invalidate_controller._context, "invalidate_environment"
583-
)
581+
invalidate_controller._context._state_sync = mocker.MagicMock()
584582
invalidate_controller.try_invalidate_pr_environment()
585-
invalidate_environment.assert_called_once_with("hello_world_2")
583+
invalidate_controller._context._state_sync.invalidate_environment.assert_called_once_with(
584+
"hello_world_2"
585+
)
586586

587587
no_invalidate_controller = make_controller(
588588
"tests/fixtures/github/pull_request_synchronized.json",
589589
github_client,
590590
bot_config=GithubCICDBotConfig(invalidate_environment_after_deploy=False),
591591
)
592-
no_invalidate_environment = mocker.patch.object(
593-
no_invalidate_controller._context, "invalidate_environment"
594-
)
592+
no_invalidate_controller._context._state_sync = mocker.MagicMock()
595593
no_invalidate_controller.try_invalidate_pr_environment()
596594

597-
no_invalidate_environment.assert_not_called()
595+
assert not no_invalidate_controller._context._state_sync.invalidate_environment.called
598596

599597

600598
def test_try_merge_pr(github_client, make_controller, mocker: MockerFixture):

0 commit comments

Comments
 (0)