@@ -5609,3 +5609,82 @@ def test_plan_environment_statements_doesnt_cause_extra_diff(tmp_path: Path):
56095609
56105610 # second plan - nothing has changed so should report no changes
56115611 assert not ctx .plan (auto_apply = True , no_prompts = True ).has_changes
5612+
5613+
5614+ def test_janitor_cleanup_order (mocker : MockerFixture , tmp_path : Path ):
5615+ def setup_scenario ():
5616+ models_dir = tmp_path / "models"
5617+
5618+ if not models_dir .exists ():
5619+ models_dir .mkdir ()
5620+
5621+ model1_path = models_dir / "model1.sql"
5622+
5623+ with open (model1_path , "w" ) as f :
5624+ f .write ("MODEL(name test.model1, kind FULL); SELECT 1 AS col" )
5625+
5626+ config = Config (
5627+ model_defaults = ModelDefaultsConfig (dialect = "duckdb" ),
5628+ )
5629+ ctx = Context (paths = [tmp_path ], config = config )
5630+
5631+ ctx .plan ("dev" , no_prompts = True , auto_apply = True )
5632+
5633+ model1_snapshot = ctx .get_snapshot ("test.model1" )
5634+
5635+ # Delete the model file to cause a snapshot expiration
5636+ model1_path .unlink ()
5637+
5638+ ctx .load ()
5639+
5640+ ctx .plan ("dev" , no_prompts = True , auto_apply = True )
5641+
5642+ # Invalidate the environment to cause an environment cleanup
5643+ ctx .invalidate_environment ("dev" )
5644+
5645+ try :
5646+ ctx ._run_janitor (ignore_ttl = True )
5647+ except :
5648+ pass
5649+
5650+ return ctx , model1_snapshot
5651+
5652+ # Case 1: Assume that the snapshot cleanup yields an error, the snapshot records
5653+ # should still exist in the state sync so the next janitor can retry
5654+ mocker .patch (
5655+ "sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup" ,
5656+ side_effect = Exception ("snapshot cleanup error" ),
5657+ )
5658+ ctx , model1_snapshot = setup_scenario ()
5659+
5660+ # - Check that the snapshot record exists in the state sync
5661+ state_snapshot = ctx .state_sync .state_sync .get_snapshots ([model1_snapshot .snapshot_id ])
5662+ assert state_snapshot
5663+
5664+ # - Run the janitor again, this time it should succeed
5665+ mocker .patch ("sqlmesh.core.snapshot.evaluator.SnapshotEvaluator.cleanup" )
5666+ ctx ._run_janitor (ignore_ttl = True )
5667+
5668+ # - Check that the snapshot record does not exist in the state sync anymore
5669+ state_snapshot = ctx .state_sync .state_sync .get_snapshots ([model1_snapshot .snapshot_id ])
5670+ assert not state_snapshot
5671+
5672+ # Case 2: Assume that the view cleanup yields an error, the enviroment
5673+ # record should still exist
5674+ mocker .patch (
5675+ "sqlmesh.core.context.cleanup_expired_views" , side_effect = Exception ("view cleanup error" )
5676+ )
5677+ ctx , model1_snapshot = setup_scenario ()
5678+
5679+ views = ctx .fetchdf ("FROM duckdb_views() SELECT * EXCLUDE(sql) WHERE NOT internal" )
5680+ assert views .empty
5681+
5682+ # - Check that the environment record exists in the state sync
5683+ assert ctx .state_sync .get_environment ("dev" )
5684+
5685+ # - Run the janitor again, this time it should succeed
5686+ mocker .patch ("sqlmesh.core.context.cleanup_expired_views" )
5687+ ctx ._run_janitor (ignore_ttl = True )
5688+
5689+ # - Check that the environment record does not exist in the state sync anymore
5690+ assert not ctx .state_sync .get_environment ("dev" )
0 commit comments