3737)
3838from sqlmesh .utils .migration import index_text_type , blob_text_type
3939from sqlmesh .utils .date import now_timestamp , TimeLike , to_timestamp
40- from sqlmesh .utils import unique
4140
4241if t .TYPE_CHECKING :
4342 import pandas as pd
@@ -225,12 +224,6 @@ def get_expired_snapshots(
225224 if not expired_candidates :
226225 return None
227226
228- def _is_snapshot_used (snapshot : SnapshotIdAndVersion ) -> bool :
229- return (
230- snapshot .snapshot_id in promoted_snapshot_ids
231- or snapshot .snapshot_id not in expired_candidates
232- )
233-
234227 # Extract cursor values from last row for pagination
235228 last_row = rows [- 1 ]
236229 last_row_boundary = RowBoundary (
@@ -244,51 +237,66 @@ def _is_snapshot_used(snapshot: SnapshotIdAndVersion) -> bool:
244237 end = last_row_boundary ,
245238 )
246239
247- unique_expired_versions = unique (expired_candidates .values ())
248240 expired_snapshot_ids : t .Set [SnapshotId ] = set ()
249241 cleanup_tasks : t .List [SnapshotTableCleanupTask ] = []
250-
251- snapshots = self ._get_snapshots_with_same_version (unique_expired_versions )
252-
242+ # A version identifies interval state, not a physical object. The same
243+ # version may exist in different schemas or gateways. Load the table
244+ # metadata for peers as well as candidates before determining ownership.
245+ peers = self .get_snapshots_by_names (
246+ {s .name for s in expired_candidates }, exclude_expired = False
247+ )
248+ full_snapshots = self ._get_snapshots ([s .snapshot_id for s in peers ])
249+ snapshots = list (full_snapshots .values ())
253250 snapshots_by_version = defaultdict (set )
254251 snapshots_by_dev_version = defaultdict (set )
252+ table_owners = defaultdict (set )
253+
254+ def table_key (snapshot : Snapshot , deployable : bool ) -> t .Tuple [t .Optional [str ], str ]:
255+ return (
256+ snapshot .model_gateway ,
257+ snapshot .table_name (is_deployable = deployable ),
258+ )
259+
255260 for s in snapshots :
256261 snapshots_by_version [(s .name , s .version )].add (s .snapshot_id )
257262 snapshots_by_dev_version [(s .name , s .dev_version )].add (s .snapshot_id )
263+ if s .is_model and not s .is_symbolic :
264+ for deployable in (True , False ):
265+ table_owners [table_key (s , deployable )].add (s .snapshot_id )
258266
259- expired_snapshots = [s for s in snapshots if not _is_snapshot_used (s )]
260- all_expired_snapshot_ids = {s .snapshot_id for s in expired_snapshots }
261-
262- cleanup_targets : t .List [t .Tuple [SnapshotId , bool ]] = []
267+ expired_snapshots = [
268+ full_snapshots [sid ] for sid in expired_candidates if sid in full_snapshots
269+ ]
263270 for snapshot in expired_snapshots :
264- shared_version_snapshots = snapshots_by_version [(snapshot .name , snapshot .version )]
265- shared_version_snapshots .discard (snapshot .snapshot_id )
266-
267- shared_dev_version_snapshots = snapshots_by_dev_version [
268- (snapshot .name , snapshot .dev_version )
269- ]
270- shared_dev_version_snapshots .discard (snapshot .snapshot_id )
271-
272- if not shared_dev_version_snapshots :
273- dev_table_only = bool (shared_version_snapshots )
274- cleanup_targets .append ((snapshot .snapshot_id , dev_table_only ))
275-
276- snapshot_ids_to_cleanup = [snapshot_id for snapshot_id , _ in cleanup_targets ]
277- full_snapshots = self ._get_snapshots (snapshot_ids_to_cleanup )
278- for snapshot_id , dev_table_only in cleanup_targets :
279- if snapshot_id in full_snapshots :
271+ prod_owners = snapshots_by_version [(snapshot .name , snapshot .version )]
272+ dev_owners = snapshots_by_dev_version [(snapshot .name , snapshot .dev_version )]
273+ prod_owners .discard (snapshot .snapshot_id )
274+ dev_owners .discard (snapshot .snapshot_id )
275+ delete_prod = delete_dev = False
276+ if snapshot .is_model and not snapshot .is_symbolic :
277+ prod_table_owners = table_owners [table_key (snapshot , True )]
278+ dev_table_owners = table_owners [table_key (snapshot , False )]
279+ prod_table_owners .discard (snapshot .snapshot_id )
280+ dev_table_owners .discard (snapshot .snapshot_id )
281+ delete_prod = not prod_table_owners
282+ delete_dev = not dev_table_owners
283+ # An interval-only task is required when the last logical version
284+ # expires but a physical object is retained by a different version.
285+ if delete_prod or delete_dev or not prod_owners or not dev_owners :
280286 cleanup_tasks .append (
281287 SnapshotTableCleanupTask (
282- snapshot = full_snapshots [snapshot_id ].table_info ,
283- dev_table_only = dev_table_only ,
288+ snapshot = snapshot .table_info ,
289+ dev_table_only = not delete_prod ,
290+ delete_dev_table = delete_dev ,
291+ delete_prod_intervals = (
292+ None if (not prod_owners ) == delete_prod else not prod_owners
293+ ),
294+ delete_dev_intervals = (
295+ None if (not dev_owners ) == delete_dev else not dev_owners
296+ ),
284297 )
285298 )
286- expired_snapshot_ids .add (snapshot_id )
287- all_expired_snapshot_ids .discard (snapshot_id )
288-
289- # Add any remaining expired snapshots that don't require cleanup
290- if all_expired_snapshot_ids :
291- expired_snapshot_ids .update (all_expired_snapshot_ids )
299+ expired_snapshot_ids .add (snapshot .snapshot_id )
292300
293301 if expired_snapshot_ids or cleanup_tasks :
294302 return ExpiredSnapshotBatch (
0 commit comments