sqlmesh 0.192.3.dev1__py3-none-any.whl → 0.192.3.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ from sqlmesh.core.state_sync import StateReader
17
17
  from sqlmesh.core.snapshot.definition import (
18
18
  SnapshotInfoMixin,
19
19
  )
20
- from sqlmesh.utils import Verbosity, rich as srich
20
+ from sqlmesh.utils import Verbosity, rich as srich, to_snake_case
21
21
  from sqlmesh.utils.date import to_ts
22
22
  from sqlmesh.utils.errors import SQLMeshError
23
23
 
@@ -73,7 +73,7 @@ class RichExplainerConsole(ExplainerConsole):
73
73
  def explain(self, stages: t.List[stages.PlanStage]) -> None:
74
74
  tree = Tree("[bold]Explained plan[/bold]")
75
75
  for stage in stages:
76
- handler_name = f"visit_{_to_snake_case(stage.__class__.__name__)}"
76
+ handler_name = f"visit_{to_snake_case(stage.__class__.__name__)}"
77
77
  if not hasattr(self, handler_name):
78
78
  logger.error("Unexpected stage: %s", stage.__class__.__name__)
79
79
  continue
@@ -97,6 +97,9 @@ class RichExplainerConsole(ExplainerConsole):
97
97
  "[bold]Validate SQL and create physical layer tables and views if they do not exist[/bold]"
98
98
  )
99
99
  for snapshot in stage.snapshots:
100
+ if snapshot.snapshot_id not in stage.snapshots_with_missing_intervals:
101
+ continue
102
+
100
103
  is_deployable = (
101
104
  stage.deployability_index.is_deployable(snapshot)
102
105
  if self.environment_naming_info.name != c.PROD
@@ -114,7 +117,9 @@ class RichExplainerConsole(ExplainerConsole):
114
117
 
115
118
  if snapshot.is_view:
116
119
  create_tree = Tree("Create view if it doesn't exist")
117
- elif snapshot.is_forward_only and snapshot.previous_versions:
120
+ elif (
121
+ snapshot.is_forward_only and snapshot.previous_versions and not snapshot.is_managed
122
+ ):
118
123
  prod_table = snapshot.table_name(True)
119
124
  create_tree = Tree(
120
125
  f"Clone {prod_table} into {table_name} and then update its schema if it doesn't exist"
@@ -224,7 +229,7 @@ class RichExplainerConsole(ExplainerConsole):
224
229
  "[bold]Delete views in the virtual layer for models that were removed[/bold]"
225
230
  )
226
231
  for snapshot in stage.demoted_snapshots:
227
- display_name = self._display_name(snapshot)
232
+ display_name = self._display_name(snapshot, stage.demoted_environment_naming_info)
228
233
  demote_tree.add(display_name)
229
234
 
230
235
  if stage.promoted_snapshots:
@@ -233,14 +238,31 @@ class RichExplainerConsole(ExplainerConsole):
233
238
  tree.add(self._limit_tree(demote_tree))
234
239
  return tree
235
240
 
241
+ def visit_create_snapshot_records_stage(
242
+ self, stage: stages.CreateSnapshotRecordsStage
243
+ ) -> t.Optional[Tree]:
244
+ return None
245
+
236
246
  def visit_environment_record_update_stage(
237
247
  self, stage: stages.EnvironmentRecordUpdateStage
238
248
  ) -> t.Optional[Tree]:
239
249
  return None
240
250
 
241
- def _display_name(self, snapshot: SnapshotInfoMixin) -> str:
251
+ def visit_unpause_stage(self, stage: stages.UnpauseStage) -> t.Optional[Tree]:
252
+ return None
253
+
254
+ def visit_finalize_environment_stage(
255
+ self, stage: stages.FinalizeEnvironmentStage
256
+ ) -> t.Optional[Tree]:
257
+ return None
258
+
259
+ def _display_name(
260
+ self,
261
+ snapshot: SnapshotInfoMixin,
262
+ environment_naming_info: t.Optional[EnvironmentNamingInfo] = None,
263
+ ) -> str:
242
264
  return snapshot.display_name(
243
- self.environment_naming_info,
265
+ environment_naming_info or self.environment_naming_info,
244
266
  self.default_catalog if self.verbosity < Verbosity.VERY_VERBOSE else None,
245
267
  dialect=self.dialect,
246
268
  )
@@ -273,9 +295,3 @@ def _get_explainer_console(
273
295
  verbosity=console.verbosity,
274
296
  console=console.console,
275
297
  )
276
-
277
-
278
- def _to_snake_case(name: str) -> str:
279
- return "".join(
280
- f"_{c.lower()}" if c.isupper() and idx != 0 else c.lower() for idx, c in enumerate(name)
281
- )
@@ -1,11 +1,8 @@
1
1
  import typing as t
2
2
 
3
3
  from dataclasses import dataclass
4
+ from sqlmesh.core.environment import EnvironmentStatements, EnvironmentNamingInfo
4
5
  from sqlmesh.core.plan.definition import EvaluatablePlan
5
- from sqlmesh.core.plan.evaluator import (
6
- get_audit_only_snapshots,
7
- get_snapshots_to_create,
8
- )
9
6
  from sqlmesh.core.state_sync import StateReader
10
7
  from sqlmesh.core.scheduler import merged_missing_intervals, SnapshotToIntervals
11
8
  from sqlmesh.core.snapshot.definition import (
@@ -19,68 +16,193 @@ from sqlmesh.core.snapshot.definition import (
19
16
 
20
17
  @dataclass
21
18
  class BeforeAllStage:
22
- statements: t.List[str]
19
+ """Run environment statements before every other stage.
20
+
21
+ Args:
22
+ statements: Environment statements to run before every other stage.
23
+ all_snapshots: All snapshots in the plan by name.
24
+ """
25
+
26
+ statements: t.List[EnvironmentStatements]
27
+ all_snapshots: t.Dict[str, Snapshot]
23
28
 
24
29
 
25
30
  @dataclass
26
31
  class AfterAllStage:
27
- statements: t.List[str]
32
+ """Run environment statements after all other stages.
33
+
34
+ Args:
35
+ statements: Environment statements to run after all other stages.
36
+ all_snapshots: All snapshots in the plan by name.
37
+ """
38
+
39
+ statements: t.List[EnvironmentStatements]
40
+ all_snapshots: t.Dict[str, Snapshot]
41
+
42
+
43
+ @dataclass
44
+ class CreateSnapshotRecordsStage:
45
+ """Create new snapshot reecords in the state.
46
+
47
+ Args:
48
+ snapshots: New snapshots to create records for.
49
+ """
50
+
51
+ snapshots: t.List[Snapshot]
28
52
 
29
53
 
30
54
  @dataclass
31
55
  class PhysicalLayerUpdateStage:
56
+ """Update the physical layer by creating physical tables and views for given snapshots.
57
+
58
+ Args:
59
+ snapshots: Snapshots to create physical tables and views for. This collection can be empty in which case
60
+ no physical layer update is needed. This can be useful to report the lack of physical layer updates
61
+ back to the user.
62
+ all_snapshots: All snapshots in the plan by snapshot ID.
63
+ snapshots_with_missing_intervals: Snapshots that have missing intervals.
64
+ deployability_index: Deployability index for this stage.
65
+ """
66
+
32
67
  snapshots: t.List[Snapshot]
68
+ all_snapshots: t.Dict[SnapshotId, Snapshot]
69
+ snapshots_with_missing_intervals: t.Set[SnapshotId]
33
70
  deployability_index: DeployabilityIndex
34
71
 
35
72
 
36
73
  @dataclass
37
74
  class AuditOnlyRunStage:
75
+ """Run audits only for given snapshots.
76
+
77
+ Args:
78
+ snapshots: Snapshots to run audits for.
79
+ """
80
+
38
81
  snapshots: t.List[Snapshot]
39
82
 
40
83
 
41
84
  @dataclass
42
85
  class RestatementStage:
86
+ """Restate intervals for given snapshots.
87
+
88
+ Args:
89
+ snapshot_intervals: Intervals to restate.
90
+ all_snapshots: All snapshots in the plan by name.
91
+ """
92
+
43
93
  snapshot_intervals: t.Dict[SnapshotTableInfo, Interval]
94
+ all_snapshots: t.Dict[str, Snapshot]
44
95
 
45
96
 
46
97
  @dataclass
47
98
  class BackfillStage:
99
+ """Backfill given missing intervals.
100
+
101
+ Args:
102
+ snapshot_to_intervals: Intervals to backfill. This collection can be empty in which case no backfill is needed.
103
+ This can be useful to report the lack of backfills back to the user.
104
+ all_snapshots: All snapshots in the plan by name.
105
+ deployability_index: Deployability index for this stage.
106
+ before_promote: Whether this stage is before the promotion stage.
107
+ """
108
+
48
109
  snapshot_to_intervals: SnapshotToIntervals
110
+ all_snapshots: t.Dict[str, Snapshot]
49
111
  deployability_index: DeployabilityIndex
50
112
  before_promote: bool = True
51
113
 
52
114
 
115
+ @dataclass
116
+ class EnvironmentRecordUpdateStage:
117
+ """Update the environment record in the state.
118
+
119
+ Args:
120
+ no_gaps_snapshot_names: Names of snapshots for which there should be no interval gaps.
121
+ """
122
+
123
+ no_gaps_snapshot_names: t.Set[str]
124
+
125
+
53
126
  @dataclass
54
127
  class MigrateSchemasStage:
128
+ """Migrate schemas of physical tables for given snapshots.
129
+
130
+ Args:
131
+ snapshots: Snapshots to migrate schemas for.
132
+ all_snapshots: All snapshots in the plan by snapshot ID.
133
+ deployability_index: Deployability index for this stage.
134
+ """
135
+
55
136
  snapshots: t.List[Snapshot]
137
+ all_snapshots: t.Dict[SnapshotId, Snapshot]
138
+ deployability_index: DeployabilityIndex
56
139
 
57
140
 
58
141
  @dataclass
59
142
  class VirtualLayerUpdateStage:
143
+ """Update the virtual layer by creating and deleting views for given snapshots.
144
+
145
+ Args:
146
+ promoted_snapshots: Snapshots to create views for.
147
+ demoted_snapshots: Snapshots to delete views for.
148
+ demoted_environment_naming_info: Environment naming info of the previous environment record.
149
+ all_snapshots: All snapshots in the plan by snapshot ID.
150
+ deployability_index: Deployability index for this stage.
151
+ """
152
+
60
153
  promoted_snapshots: t.Set[SnapshotTableInfo]
61
154
  demoted_snapshots: t.Set[SnapshotTableInfo]
155
+ demoted_environment_naming_info: t.Optional[EnvironmentNamingInfo]
156
+ all_snapshots: t.Dict[SnapshotId, Snapshot]
62
157
  deployability_index: DeployabilityIndex
63
158
 
64
159
 
65
160
  @dataclass
66
- class EnvironmentRecordUpdateStage:
161
+ class UnpauseStage:
162
+ """Unpause given snapshots that are being deployed to prod.
163
+
164
+ Args:
165
+ promoted_snapshots: Snapshots to unpause.
166
+ """
167
+
168
+ promoted_snapshots: t.Set[SnapshotTableInfo]
169
+
170
+
171
+ @dataclass
172
+ class FinalizeEnvironmentStage:
173
+ """Finalize the enviornment record in the state.
174
+
175
+ Finalization means that all stages have been applied and that the environment has been transitioned
176
+ to the new state successfully. This should be the last stage in the plan application process.
177
+ """
178
+
67
179
  pass
68
180
 
69
181
 
70
182
  PlanStage = t.Union[
71
183
  BeforeAllStage,
72
184
  AfterAllStage,
185
+ CreateSnapshotRecordsStage,
73
186
  PhysicalLayerUpdateStage,
74
187
  AuditOnlyRunStage,
75
188
  RestatementStage,
76
189
  BackfillStage,
190
+ EnvironmentRecordUpdateStage,
77
191
  MigrateSchemasStage,
78
192
  VirtualLayerUpdateStage,
79
- EnvironmentRecordUpdateStage,
193
+ UnpauseStage,
194
+ FinalizeEnvironmentStage,
80
195
  ]
81
196
 
82
197
 
83
198
  class PlanStagesBuilder:
199
+ """The builder for the plan stages.
200
+
201
+ Args:
202
+ state_reader: The state reader to use to read the snapshots and environment.
203
+ default_catalog: The default catalog to use for the snapshots.
204
+ """
205
+
84
206
  def __init__(
85
207
  self,
86
208
  state_reader: StateReader,
@@ -90,6 +212,16 @@ class PlanStagesBuilder:
90
212
  self.default_catalog = default_catalog
91
213
 
92
214
  def build(self, plan: EvaluatablePlan) -> t.List[PlanStage]:
215
+ """Builds the plan stages for the given plan.
216
+
217
+ NOTE: Building the plan stages should NOT produce any side effects in the state or the data warehouse.
218
+
219
+ Args:
220
+ plan: The plan to build the stages for.
221
+
222
+ Returns:
223
+ A list of plan stages.
224
+ """
93
225
  new_snapshots = {s.snapshot_id: s for s in plan.new_snapshots}
94
226
  stored_snapshots = self.state_reader.get_snapshots(plan.environment.snapshots)
95
227
  snapshots = {**new_snapshots, **stored_snapshots}
@@ -99,6 +231,8 @@ class PlanStagesBuilder:
99
231
  s.snapshot_id for s in snapshots.values() if plan.is_selected_for_backfill(s.name)
100
232
  }
101
233
 
234
+ self._adjust_intervals(snapshots_by_name, plan)
235
+
102
236
  deployability_index = DeployabilityIndex.create(snapshots, start=plan.start)
103
237
  deployability_index_for_creation = deployability_index
104
238
  if plan.is_dev:
@@ -119,8 +253,9 @@ class PlanStagesBuilder:
119
253
  s
120
254
  for s in snapshots.values()
121
255
  if s.is_paused
122
- and s.is_materialized
123
- and not deployability_index_for_creation.is_representative(s)
256
+ and s.is_model
257
+ and not s.is_symbolic
258
+ and (not deployability_index_for_creation.is_representative(s) or s.is_view)
124
259
  ]
125
260
 
126
261
  snapshots_to_intervals = self._missing_intervals(
@@ -138,19 +273,26 @@ class PlanStagesBuilder:
138
273
  elif snapshot.snapshot_id in after_promote_snapshots:
139
274
  missing_intervals_after_promote[snapshot] = intervals
140
275
 
276
+ promoted_snapshots, demoted_snapshots, demoted_environment_naming_info = (
277
+ self._get_promoted_demoted_snapshots(plan)
278
+ )
279
+
141
280
  stages: t.List[PlanStage] = []
142
281
 
143
- before_all_stage = self._get_before_all_stage(plan)
282
+ before_all_stage = self._get_before_all_stage(plan, snapshots_by_name)
144
283
  if before_all_stage:
145
284
  stages.append(before_all_stage)
146
285
 
286
+ if plan.new_snapshots:
287
+ stages.append(CreateSnapshotRecordsStage(snapshots=plan.new_snapshots))
288
+
147
289
  stages.append(
148
290
  self._get_physical_layer_update_stage(
149
291
  plan, snapshots, snapshots_to_intervals, deployability_index_for_creation
150
292
  )
151
293
  )
152
294
 
153
- audit_only_snapshots = get_audit_only_snapshots(new_snapshots, self.state_reader)
295
+ audit_only_snapshots = self._get_audit_only_snapshots(new_snapshots)
154
296
  if audit_only_snapshots:
155
297
  stages.append(AuditOnlyRunStage(snapshots=list(audit_only_snapshots.values())))
156
298
 
@@ -162,53 +304,101 @@ class PlanStagesBuilder:
162
304
  stages.append(
163
305
  BackfillStage(
164
306
  snapshot_to_intervals=missing_intervals_before_promote,
307
+ all_snapshots=snapshots_by_name,
165
308
  deployability_index=deployability_index,
166
309
  )
167
310
  )
168
311
  elif not needs_backfill:
169
312
  # Append an empty backfill stage so that explainer can show that the stage is skipped
170
313
  stages.append(
171
- BackfillStage(snapshot_to_intervals={}, deployability_index=deployability_index)
314
+ BackfillStage(
315
+ snapshot_to_intervals={},
316
+ all_snapshots=snapshots_by_name,
317
+ deployability_index=deployability_index,
318
+ )
172
319
  )
173
320
 
174
- stages.append(EnvironmentRecordUpdateStage())
321
+ stages.append(
322
+ EnvironmentRecordUpdateStage(
323
+ no_gaps_snapshot_names={s.name for s in before_promote_snapshots}
324
+ )
325
+ )
175
326
 
176
327
  if snapshots_with_schema_migration:
177
- stages.append(MigrateSchemasStage(snapshots=snapshots_with_schema_migration))
328
+ stages.append(
329
+ MigrateSchemasStage(
330
+ snapshots=snapshots_with_schema_migration,
331
+ all_snapshots=snapshots,
332
+ deployability_index=deployability_index_for_creation,
333
+ )
334
+ )
335
+
336
+ if not plan.is_dev and not plan.ensure_finalized_snapshots and promoted_snapshots:
337
+ # Only unpause at this point if we don't have to use the finalized snapshots
338
+ # for subsequent plan applications. Otherwise, unpause right before updating
339
+ # the virtual layer.
340
+ stages.append(UnpauseStage(promoted_snapshots=promoted_snapshots))
178
341
 
179
342
  if missing_intervals_after_promote:
180
343
  stages.append(
181
344
  BackfillStage(
182
345
  snapshot_to_intervals=missing_intervals_after_promote,
346
+ all_snapshots=snapshots_by_name,
183
347
  deployability_index=deployability_index,
184
348
  )
185
349
  )
186
350
 
187
- virtual_layer_update_stage = self._get_virtual_layer_update_stage(plan, deployability_index)
351
+ if not plan.is_dev and plan.ensure_finalized_snapshots and promoted_snapshots:
352
+ # Unpause right before updating the virtual layer and finalizing the environment in case when
353
+ # we need to use the finalized snapshots for subsequent plan applications.
354
+ # Otherwise, unpause right after updatig the environment record.
355
+ stages.append(UnpauseStage(promoted_snapshots=promoted_snapshots))
356
+
357
+ virtual_layer_update_stage = self._get_virtual_layer_update_stage(
358
+ promoted_snapshots,
359
+ demoted_snapshots,
360
+ demoted_environment_naming_info,
361
+ snapshots,
362
+ deployability_index,
363
+ )
188
364
  if virtual_layer_update_stage:
189
365
  stages.append(virtual_layer_update_stage)
190
366
 
191
- after_all_stage = self._get_after_all_stage(plan)
367
+ stages.append(FinalizeEnvironmentStage())
368
+
369
+ after_all_stage = self._get_after_all_stage(plan, snapshots_by_name)
192
370
  if after_all_stage:
193
371
  stages.append(after_all_stage)
194
372
 
195
373
  return stages
196
374
 
197
- def _get_before_all_stage(self, plan: EvaluatablePlan) -> t.Optional[BeforeAllStage]:
375
+ def _get_before_all_stage(
376
+ self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot]
377
+ ) -> t.Optional[BeforeAllStage]:
198
378
  before_all = [
199
- statement
379
+ environment_statements
200
380
  for environment_statements in plan.environment_statements or []
201
- for statement in environment_statements.before_all
381
+ if environment_statements.before_all
202
382
  ]
203
- return BeforeAllStage(statements=before_all) if before_all else None
383
+ return (
384
+ BeforeAllStage(statements=before_all, all_snapshots=snapshots_by_name)
385
+ if before_all
386
+ else None
387
+ )
204
388
 
205
- def _get_after_all_stage(self, plan: EvaluatablePlan) -> t.Optional[AfterAllStage]:
389
+ def _get_after_all_stage(
390
+ self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot]
391
+ ) -> t.Optional[AfterAllStage]:
206
392
  after_all = [
207
- statement
393
+ environment_statements
208
394
  for environment_statements in plan.environment_statements or []
209
- for statement in environment_statements.after_all
395
+ if environment_statements.after_all
210
396
  ]
211
- return AfterAllStage(statements=after_all) if after_all else None
397
+ return (
398
+ AfterAllStage(statements=after_all, all_snapshots=snapshots_by_name)
399
+ if after_all
400
+ else None
401
+ )
212
402
 
213
403
  def _get_restatement_stage(
214
404
  self, plan: EvaluatablePlan, snapshots_by_name: t.Dict[str, Snapshot]
@@ -220,7 +410,9 @@ class PlanStagesBuilder:
220
410
  snapshot_intervals_to_restate[restated_snapshot.table_info] = interval
221
411
  if not snapshot_intervals_to_restate or plan.is_dev:
222
412
  return None
223
- return RestatementStage(snapshot_intervals=snapshot_intervals_to_restate)
413
+ return RestatementStage(
414
+ snapshot_intervals=snapshot_intervals_to_restate, all_snapshots=snapshots_by_name
415
+ )
224
416
 
225
417
  def _get_physical_layer_update_stage(
226
418
  self,
@@ -229,50 +421,80 @@ class PlanStagesBuilder:
229
421
  snapshots_to_intervals: SnapshotToIntervals,
230
422
  deployability_index: DeployabilityIndex,
231
423
  ) -> PhysicalLayerUpdateStage:
232
- snapshots_to_create = [
233
- s
234
- for s in get_snapshots_to_create(plan, snapshots)
235
- if s in snapshots_to_intervals and s.is_model and not s.is_symbolic
236
- ]
237
424
  return PhysicalLayerUpdateStage(
238
- snapshots=snapshots_to_create,
425
+ snapshots=self._get_snapshots_to_create(plan, snapshots),
426
+ all_snapshots=snapshots,
427
+ snapshots_with_missing_intervals={s.snapshot_id for s in snapshots_to_intervals},
239
428
  deployability_index=deployability_index,
240
429
  )
241
430
 
242
431
  def _get_virtual_layer_update_stage(
243
- self, plan: EvaluatablePlan, deployability_index: DeployabilityIndex
432
+ self,
433
+ promoted_snapshots: t.Set[SnapshotTableInfo],
434
+ demoted_snapshots: t.Set[SnapshotTableInfo],
435
+ demoted_environment_naming_info: t.Optional[EnvironmentNamingInfo],
436
+ all_snapshots: t.Dict[SnapshotId, Snapshot],
437
+ deployability_index: DeployabilityIndex,
244
438
  ) -> t.Optional[VirtualLayerUpdateStage]:
245
- promoted_snapshots, demoted_snapshots = self._get_promoted_demoted_snapshots(plan)
439
+ promoted_snapshots = {s for s in promoted_snapshots if s.is_model and not s.is_symbolic}
440
+ demoted_snapshots = {s for s in demoted_snapshots if s.is_model and not s.is_symbolic}
246
441
  if not promoted_snapshots and not demoted_snapshots:
247
442
  return None
248
443
  return VirtualLayerUpdateStage(
249
444
  promoted_snapshots=promoted_snapshots,
250
445
  demoted_snapshots=demoted_snapshots,
446
+ demoted_environment_naming_info=demoted_environment_naming_info,
447
+ all_snapshots=all_snapshots,
251
448
  deployability_index=deployability_index,
252
449
  )
253
450
 
254
451
  def _get_promoted_demoted_snapshots(
255
452
  self, plan: EvaluatablePlan
256
- ) -> t.Tuple[t.Set[SnapshotTableInfo], t.Set[SnapshotTableInfo]]:
453
+ ) -> t.Tuple[
454
+ t.Set[SnapshotTableInfo], t.Set[SnapshotTableInfo], t.Optional[EnvironmentNamingInfo]
455
+ ]:
257
456
  existing_environment = self.state_reader.get_environment(plan.environment.name)
258
457
  if existing_environment:
259
- snapshots_by_name = {s.name: s for s in existing_environment.snapshots}
260
- demoted_snapshot_names = {s.name for s in existing_environment.promoted_snapshots} - {
458
+ new_table_infos = {
459
+ table_info.name: table_info for table_info in plan.environment.promoted_snapshots
460
+ }
461
+ existing_table_infos = {
462
+ table_info.name: table_info
463
+ for table_info in existing_environment.promoted_snapshots
464
+ }
465
+ views_that_changed_location = {
466
+ existing_table_info
467
+ for existing_table_info in existing_environment.promoted_snapshots
468
+ if existing_table_info.name in new_table_infos
469
+ and existing_table_info.qualified_view_name.for_environment(
470
+ existing_environment.naming_info
471
+ )
472
+ != new_table_infos[existing_table_info.name].qualified_view_name.for_environment(
473
+ plan.environment.naming_info
474
+ )
475
+ }
476
+ missing_model_names = set(existing_table_infos) - {
261
477
  s.name for s in plan.environment.promoted_snapshots
262
478
  }
263
- demoted_snapshots = {snapshots_by_name[name] for name in demoted_snapshot_names}
479
+ demoted_snapshots = {
480
+ existing_table_infos[name] for name in missing_model_names
481
+ } | views_that_changed_location
264
482
  else:
265
483
  demoted_snapshots = set()
484
+
266
485
  promoted_snapshots = set(plan.environment.promoted_snapshots)
267
486
  if existing_environment and plan.environment.can_partially_promote(existing_environment):
268
487
  promoted_snapshots -= set(existing_environment.promoted_snapshots)
269
488
 
270
- def _snapshot_filter(snapshot: SnapshotTableInfo) -> bool:
271
- return snapshot.is_model and not snapshot.is_symbolic
489
+ demoted_environment_naming_info = (
490
+ existing_environment.naming_info if demoted_snapshots and existing_environment else None
491
+ )
272
492
 
273
- return {s for s in promoted_snapshots if _snapshot_filter(s)}, {
274
- s for s in demoted_snapshots if _snapshot_filter(s)
275
- }
493
+ return (
494
+ promoted_snapshots,
495
+ demoted_snapshots,
496
+ demoted_environment_naming_info,
497
+ )
276
498
 
277
499
  def _missing_intervals(
278
500
  self,
@@ -294,6 +516,69 @@ class PlanStagesBuilder:
294
516
  interval_end_per_model=plan.interval_end_per_model,
295
517
  )
296
518
 
519
+ def _get_audit_only_snapshots(
520
+ self, new_snapshots: t.Dict[SnapshotId, Snapshot]
521
+ ) -> t.Dict[SnapshotId, Snapshot]:
522
+ metadata_snapshots = []
523
+ for snapshot in new_snapshots.values():
524
+ if not snapshot.is_metadata or not snapshot.is_model or not snapshot.evaluatable:
525
+ continue
526
+
527
+ metadata_snapshots.append(snapshot)
528
+
529
+ # Bulk load all the previous snapshots
530
+ previous_snapshots = self.state_reader.get_snapshots(
531
+ [
532
+ s.previous_version.snapshot_id(s.name)
533
+ for s in metadata_snapshots
534
+ if s.previous_version
535
+ ]
536
+ ).values()
537
+
538
+ # Check if any of the snapshots have modifications to the audits field by comparing the hashes
539
+ audit_snapshots = {}
540
+ for snapshot, previous_snapshot in zip(metadata_snapshots, previous_snapshots):
541
+ new_audits_hash = snapshot.model.audit_metadata_hash()
542
+ previous_audit_hash = previous_snapshot.model.audit_metadata_hash()
543
+
544
+ if snapshot.model.audits and previous_audit_hash != new_audits_hash:
545
+ audit_snapshots[snapshot.snapshot_id] = snapshot
546
+
547
+ return audit_snapshots
548
+
549
+ def _get_snapshots_to_create(
550
+ self, plan: EvaluatablePlan, snapshots: t.Dict[SnapshotId, Snapshot]
551
+ ) -> t.List[Snapshot]:
552
+ promoted_snapshot_ids = (
553
+ set(plan.environment.promoted_snapshot_ids)
554
+ if plan.environment.promoted_snapshot_ids is not None
555
+ else None
556
+ )
557
+
558
+ def _should_create(s: Snapshot) -> bool:
559
+ if not s.is_model or s.is_symbolic:
560
+ return False
561
+ # Only create tables for snapshots that we're planning to promote or that were selected for backfill
562
+ return (
563
+ plan.is_selected_for_backfill(s.name)
564
+ or promoted_snapshot_ids is None
565
+ or s.snapshot_id in promoted_snapshot_ids
566
+ )
567
+
568
+ return [s for s in snapshots.values() if _should_create(s)]
569
+
570
+ def _adjust_intervals(
571
+ self, snapshots_by_name: t.Dict[str, Snapshot], plan: EvaluatablePlan
572
+ ) -> None:
573
+ # Make sure the intervals are up to date and restatements are reflected
574
+ self.state_reader.refresh_snapshot_intervals(snapshots_by_name.values())
575
+ for new_snapshot in plan.new_snapshots:
576
+ if new_snapshot.is_forward_only:
577
+ # Forward-only snapshots inherit intervals in dev because of cloning
578
+ new_snapshot.dev_intervals = new_snapshot.intervals.copy()
579
+ for s_name, interval in plan.restatements.items():
580
+ snapshots_by_name[s_name].remove_interval(interval)
581
+
297
582
 
298
583
  def build_plan_stages(
299
584
  plan: EvaluatablePlan,