sqlmesh 0.217.1.dev1__py3-none-any.whl → 0.227.2.dev20__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.
Files changed (190) hide show
  1. sqlmesh/__init__.py +12 -2
  2. sqlmesh/_version.py +2 -2
  3. sqlmesh/cli/project_init.py +10 -2
  4. sqlmesh/core/_typing.py +1 -0
  5. sqlmesh/core/audit/definition.py +8 -2
  6. sqlmesh/core/config/__init__.py +1 -1
  7. sqlmesh/core/config/connection.py +20 -5
  8. sqlmesh/core/config/dbt.py +13 -0
  9. sqlmesh/core/config/janitor.py +12 -0
  10. sqlmesh/core/config/loader.py +7 -0
  11. sqlmesh/core/config/model.py +2 -0
  12. sqlmesh/core/config/root.py +3 -0
  13. sqlmesh/core/console.py +80 -2
  14. sqlmesh/core/constants.py +1 -1
  15. sqlmesh/core/context.py +112 -35
  16. sqlmesh/core/dialect.py +3 -0
  17. sqlmesh/core/engine_adapter/_typing.py +2 -0
  18. sqlmesh/core/engine_adapter/base.py +330 -23
  19. sqlmesh/core/engine_adapter/base_postgres.py +17 -1
  20. sqlmesh/core/engine_adapter/bigquery.py +146 -7
  21. sqlmesh/core/engine_adapter/clickhouse.py +17 -13
  22. sqlmesh/core/engine_adapter/databricks.py +50 -2
  23. sqlmesh/core/engine_adapter/fabric.py +110 -29
  24. sqlmesh/core/engine_adapter/mixins.py +142 -48
  25. sqlmesh/core/engine_adapter/mssql.py +15 -4
  26. sqlmesh/core/engine_adapter/mysql.py +2 -2
  27. sqlmesh/core/engine_adapter/postgres.py +9 -3
  28. sqlmesh/core/engine_adapter/redshift.py +4 -0
  29. sqlmesh/core/engine_adapter/risingwave.py +1 -0
  30. sqlmesh/core/engine_adapter/shared.py +6 -0
  31. sqlmesh/core/engine_adapter/snowflake.py +82 -11
  32. sqlmesh/core/engine_adapter/spark.py +14 -10
  33. sqlmesh/core/engine_adapter/trino.py +5 -2
  34. sqlmesh/core/janitor.py +181 -0
  35. sqlmesh/core/lineage.py +1 -0
  36. sqlmesh/core/linter/rules/builtin.py +15 -0
  37. sqlmesh/core/loader.py +17 -30
  38. sqlmesh/core/macros.py +35 -13
  39. sqlmesh/core/model/common.py +2 -0
  40. sqlmesh/core/model/definition.py +72 -4
  41. sqlmesh/core/model/kind.py +66 -2
  42. sqlmesh/core/model/meta.py +107 -2
  43. sqlmesh/core/node.py +101 -2
  44. sqlmesh/core/plan/builder.py +15 -10
  45. sqlmesh/core/plan/common.py +196 -2
  46. sqlmesh/core/plan/definition.py +21 -6
  47. sqlmesh/core/plan/evaluator.py +72 -113
  48. sqlmesh/core/plan/explainer.py +90 -8
  49. sqlmesh/core/plan/stages.py +42 -21
  50. sqlmesh/core/renderer.py +26 -18
  51. sqlmesh/core/scheduler.py +60 -19
  52. sqlmesh/core/selector.py +137 -9
  53. sqlmesh/core/signal.py +64 -1
  54. sqlmesh/core/snapshot/__init__.py +1 -0
  55. sqlmesh/core/snapshot/definition.py +109 -25
  56. sqlmesh/core/snapshot/evaluator.py +610 -50
  57. sqlmesh/core/state_sync/__init__.py +0 -1
  58. sqlmesh/core/state_sync/base.py +31 -27
  59. sqlmesh/core/state_sync/cache.py +12 -4
  60. sqlmesh/core/state_sync/common.py +216 -111
  61. sqlmesh/core/state_sync/db/facade.py +30 -15
  62. sqlmesh/core/state_sync/db/interval.py +27 -7
  63. sqlmesh/core/state_sync/db/migrator.py +14 -8
  64. sqlmesh/core/state_sync/db/snapshot.py +119 -87
  65. sqlmesh/core/table_diff.py +2 -2
  66. sqlmesh/core/test/definition.py +14 -9
  67. sqlmesh/core/test/discovery.py +4 -0
  68. sqlmesh/dbt/adapter.py +20 -11
  69. sqlmesh/dbt/basemodel.py +52 -41
  70. sqlmesh/dbt/builtin.py +27 -11
  71. sqlmesh/dbt/column.py +17 -5
  72. sqlmesh/dbt/common.py +4 -2
  73. sqlmesh/dbt/context.py +14 -1
  74. sqlmesh/dbt/loader.py +60 -8
  75. sqlmesh/dbt/manifest.py +136 -8
  76. sqlmesh/dbt/model.py +105 -25
  77. sqlmesh/dbt/package.py +16 -1
  78. sqlmesh/dbt/profile.py +3 -3
  79. sqlmesh/dbt/project.py +12 -7
  80. sqlmesh/dbt/seed.py +1 -1
  81. sqlmesh/dbt/source.py +6 -1
  82. sqlmesh/dbt/target.py +25 -6
  83. sqlmesh/dbt/test.py +31 -1
  84. sqlmesh/integrations/github/cicd/controller.py +6 -2
  85. sqlmesh/lsp/context.py +4 -2
  86. sqlmesh/magics.py +1 -1
  87. sqlmesh/migrations/v0000_baseline.py +3 -6
  88. sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py +2 -5
  89. sqlmesh/migrations/v0062_add_model_gateway.py +2 -2
  90. sqlmesh/migrations/v0063_change_signals.py +2 -4
  91. sqlmesh/migrations/v0064_join_when_matched_strings.py +2 -4
  92. sqlmesh/migrations/v0065_add_model_optimize.py +2 -2
  93. sqlmesh/migrations/v0066_add_auto_restatements.py +2 -6
  94. sqlmesh/migrations/v0067_add_tsql_date_full_precision.py +2 -2
  95. sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py +2 -2
  96. sqlmesh/migrations/v0069_update_dev_table_suffix.py +2 -4
  97. sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py +2 -2
  98. sqlmesh/migrations/v0071_add_dev_version_to_intervals.py +2 -6
  99. sqlmesh/migrations/v0072_add_environment_statements.py +2 -4
  100. sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py +2 -4
  101. sqlmesh/migrations/v0074_add_partition_by_time_column_property.py +2 -2
  102. sqlmesh/migrations/v0075_remove_validate_query.py +2 -4
  103. sqlmesh/migrations/v0076_add_cron_tz.py +2 -2
  104. sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py +2 -2
  105. sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py +2 -4
  106. sqlmesh/migrations/v0079_add_gateway_managed_property.py +7 -9
  107. sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py +2 -2
  108. sqlmesh/migrations/v0081_update_partitioned_by.py +2 -4
  109. sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py +2 -4
  110. sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py +2 -2
  111. sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py +2 -2
  112. sqlmesh/migrations/v0085_deterministic_repr.py +2 -4
  113. sqlmesh/migrations/v0086_check_deterministic_bug.py +2 -4
  114. sqlmesh/migrations/v0087_normalize_blueprint_variables.py +2 -4
  115. sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py +2 -4
  116. sqlmesh/migrations/v0089_add_virtual_environment_mode.py +2 -2
  117. sqlmesh/migrations/v0090_add_forward_only_column.py +2 -6
  118. sqlmesh/migrations/v0091_on_additive_change.py +2 -2
  119. sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py +2 -4
  120. sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py +2 -2
  121. sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py +2 -6
  122. sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py +2 -4
  123. sqlmesh/migrations/v0096_remove_plan_dags_table.py +2 -4
  124. sqlmesh/migrations/v0097_add_dbt_name_in_node.py +2 -2
  125. sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py +103 -0
  126. sqlmesh/migrations/v0099_add_last_altered_to_intervals.py +25 -0
  127. sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py +9 -0
  128. sqlmesh/utils/__init__.py +8 -1
  129. sqlmesh/utils/cache.py +5 -1
  130. sqlmesh/utils/date.py +1 -1
  131. sqlmesh/utils/errors.py +4 -0
  132. sqlmesh/utils/git.py +3 -1
  133. sqlmesh/utils/jinja.py +25 -2
  134. sqlmesh/utils/pydantic.py +6 -6
  135. sqlmesh/utils/windows.py +13 -3
  136. {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/METADATA +5 -5
  137. {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/RECORD +188 -183
  138. sqlmesh_dbt/cli.py +70 -7
  139. sqlmesh_dbt/console.py +14 -6
  140. sqlmesh_dbt/operations.py +103 -24
  141. sqlmesh_dbt/selectors.py +39 -1
  142. web/client/dist/assets/{Audits-Ucsx1GzF.js → Audits-CBiYyyx-.js} +1 -1
  143. web/client/dist/assets/{Banner-BWDzvavM.js → Banner-DSRbUlO5.js} +1 -1
  144. web/client/dist/assets/{ChevronDownIcon-D2VL13Ah.js → ChevronDownIcon-MK_nrjD_.js} +1 -1
  145. web/client/dist/assets/{ChevronRightIcon-DWGYbf1l.js → ChevronRightIcon-CLWtT22Q.js} +1 -1
  146. web/client/dist/assets/{Content-DdHDZM3I.js → Content-BNuGZN5l.js} +1 -1
  147. web/client/dist/assets/{Content-Bikfy8fh.js → Content-CSHJyW0n.js} +1 -1
  148. web/client/dist/assets/{Data-CzAJH7rW.js → Data-C1oRDbLx.js} +1 -1
  149. web/client/dist/assets/{DataCatalog-BJF11g8f.js → DataCatalog-HXyX2-_j.js} +1 -1
  150. web/client/dist/assets/{Editor-s0SBpV2y.js → Editor-BDyfpUuw.js} +1 -1
  151. web/client/dist/assets/{Editor-DgLhgKnm.js → Editor-D0jNItwC.js} +1 -1
  152. web/client/dist/assets/{Errors-D0m0O1d3.js → Errors-BfuFLcPi.js} +1 -1
  153. web/client/dist/assets/{FileExplorer-CEv0vXkt.js → FileExplorer-BR9IE3he.js} +1 -1
  154. web/client/dist/assets/{Footer-BwzXn8Ew.js → Footer-CgBEtiAh.js} +1 -1
  155. web/client/dist/assets/{Header-6heDkEqG.js → Header-DSqR6nSO.js} +1 -1
  156. web/client/dist/assets/{Input-obuJsD6k.js → Input-B-oZ6fGO.js} +1 -1
  157. web/client/dist/assets/Lineage-DYQVwDbD.js +1 -0
  158. web/client/dist/assets/{ListboxShow-HM9_qyrt.js → ListboxShow-BE5-xevs.js} +1 -1
  159. web/client/dist/assets/{ModelLineage-zWdKo0U2.js → ModelLineage-DkIFAYo4.js} +1 -1
  160. web/client/dist/assets/{Models-Bcu66SRz.js → Models-D5dWr8RB.js} +1 -1
  161. web/client/dist/assets/{Page-BWEEQfIt.js → Page-C-XfU5BR.js} +1 -1
  162. web/client/dist/assets/{Plan-C4gXCqlf.js → Plan-ZEuTINBq.js} +1 -1
  163. web/client/dist/assets/{PlusCircleIcon-CVDO651q.js → PlusCircleIcon-DVXAHG8_.js} +1 -1
  164. web/client/dist/assets/{ReportErrors-BT6xFwAr.js → ReportErrors-B7FEPzMB.js} +1 -1
  165. web/client/dist/assets/{Root-ryJoBK4h.js → Root-8aZyhPxF.js} +1 -1
  166. web/client/dist/assets/{SearchList-DB04sPb9.js → SearchList-W_iT2G82.js} +1 -1
  167. web/client/dist/assets/{SelectEnvironment-CUYcXUu6.js → SelectEnvironment-C65jALmO.js} +1 -1
  168. web/client/dist/assets/{SourceList-Doo_9ZGp.js → SourceList-DSLO6nVJ.js} +1 -1
  169. web/client/dist/assets/{SourceListItem-D5Mj7Dly.js → SourceListItem-BHt8d9-I.js} +1 -1
  170. web/client/dist/assets/{SplitPane-qHmkD1qy.js → SplitPane-CViaZmw6.js} +1 -1
  171. web/client/dist/assets/{Tests-DH1Z74ML.js → Tests-DhaVt5t1.js} +1 -1
  172. web/client/dist/assets/{Welcome-DqUJUNMF.js → Welcome-DvpjH-_4.js} +1 -1
  173. web/client/dist/assets/context-BctCsyGb.js +71 -0
  174. web/client/dist/assets/{context-Dr54UHLi.js → context-DFNeGsFF.js} +1 -1
  175. web/client/dist/assets/{editor-DYIP1yQ4.js → editor-CcO28cqd.js} +1 -1
  176. web/client/dist/assets/{file-DarlIDVi.js → file-CvJN3aZO.js} +1 -1
  177. web/client/dist/assets/{floating-ui.react-dom-BH3TFvkM.js → floating-ui.react-dom-CjE-JNW1.js} +1 -1
  178. web/client/dist/assets/{help-Bl8wqaQc.js → help-DuPhjipa.js} +1 -1
  179. web/client/dist/assets/{index-D1sR7wpN.js → index-C-dJH7yZ.js} +1 -1
  180. web/client/dist/assets/{index-O3mjYpnE.js → index-Dj0i1-CA.js} +2 -2
  181. web/client/dist/assets/{plan-CehRrJUG.js → plan-BTRSbjKn.js} +1 -1
  182. web/client/dist/assets/{popover-CqgMRE0G.js → popover-_Sf0yvOI.js} +1 -1
  183. web/client/dist/assets/{project-6gxepOhm.js → project-BvSOI8MY.js} +1 -1
  184. web/client/dist/index.html +1 -1
  185. web/client/dist/assets/Lineage-D0Hgdz2v.js +0 -1
  186. web/client/dist/assets/context-DgX0fp2E.js +0 -68
  187. {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/WHEEL +0 -0
  188. {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/entry_points.txt +0 -0
  189. {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/licenses/LICENSE +0 -0
  190. {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/top_level.txt +0 -0
@@ -15,10 +15,10 @@ from sqlmesh.core.state_sync.db.utils import (
15
15
  from sqlmesh.core.snapshot import (
16
16
  SnapshotIntervals,
17
17
  SnapshotIdLike,
18
+ SnapshotIdAndVersionLike,
18
19
  SnapshotNameVersionLike,
19
20
  SnapshotTableCleanupTask,
20
21
  SnapshotNameVersion,
21
- SnapshotInfoLike,
22
22
  Snapshot,
23
23
  )
24
24
  from sqlmesh.core.snapshot.definition import Interval
@@ -60,6 +60,7 @@ class IntervalState:
60
60
  "is_removed": exp.DataType.build("boolean"),
61
61
  "is_compacted": exp.DataType.build("boolean"),
62
62
  "is_pending_restatement": exp.DataType.build("boolean"),
63
+ "last_altered_ts": exp.DataType.build("bigint"),
63
64
  }
64
65
 
65
66
  def add_snapshots_intervals(self, snapshots_intervals: t.Sequence[SnapshotIntervals]) -> None:
@@ -68,11 +69,11 @@ class IntervalState:
68
69
 
69
70
  def remove_intervals(
70
71
  self,
71
- snapshot_intervals: t.Sequence[t.Tuple[SnapshotInfoLike, Interval]],
72
+ snapshot_intervals: t.Sequence[t.Tuple[SnapshotIdAndVersionLike, Interval]],
72
73
  remove_shared_versions: bool = False,
73
74
  ) -> None:
74
75
  intervals_to_remove: t.Sequence[
75
- t.Tuple[t.Union[SnapshotInfoLike, SnapshotIntervals], Interval]
76
+ t.Tuple[t.Union[SnapshotIdAndVersionLike, SnapshotIntervals], Interval]
76
77
  ] = snapshot_intervals
77
78
  if remove_shared_versions:
78
79
  name_version_mapping = {s.name_version: interval for s, interval in snapshot_intervals}
@@ -215,13 +216,23 @@ class IntervalState:
215
216
  for start_ts, end_ts in snapshot.intervals:
216
217
  new_intervals.append(
217
218
  _interval_to_df(
218
- snapshot, start_ts, end_ts, is_dev=False, is_compacted=is_compacted
219
+ snapshot,
220
+ start_ts,
221
+ end_ts,
222
+ is_dev=False,
223
+ is_compacted=is_compacted,
224
+ last_altered_ts=snapshot.last_altered_ts,
219
225
  )
220
226
  )
221
227
  for start_ts, end_ts in snapshot.dev_intervals:
222
228
  new_intervals.append(
223
229
  _interval_to_df(
224
- snapshot, start_ts, end_ts, is_dev=True, is_compacted=is_compacted
230
+ snapshot,
231
+ start_ts,
232
+ end_ts,
233
+ is_dev=True,
234
+ is_compacted=is_compacted,
235
+ last_altered_ts=snapshot.dev_last_altered_ts,
225
236
  )
226
237
  )
227
238
 
@@ -236,6 +247,7 @@ class IntervalState:
236
247
  is_dev=False,
237
248
  is_compacted=is_compacted,
238
249
  is_pending_restatement=True,
250
+ last_altered_ts=snapshot.last_altered_ts,
239
251
  )
240
252
  )
241
253
 
@@ -284,6 +296,7 @@ class IntervalState:
284
296
  is_dev,
285
297
  is_removed,
286
298
  is_pending_restatement,
299
+ last_altered_ts,
287
300
  ) in rows:
288
301
  interval_ids.add(interval_id)
289
302
  merge_key = (name, version, dev_version, identifier)
@@ -318,8 +331,10 @@ class IntervalState:
318
331
  else:
319
332
  if is_dev:
320
333
  intervals[merge_key].add_dev_interval(start, end)
334
+ intervals[merge_key].update_dev_last_altered_ts(last_altered_ts)
321
335
  else:
322
336
  intervals[merge_key].add_interval(start, end)
337
+ intervals[merge_key].update_last_altered_ts(last_altered_ts)
323
338
  # Remove all pending restatement intervals recorded before the current interval has been added
324
339
  intervals[
325
340
  pending_restatement_interval_merge_key
@@ -340,6 +355,7 @@ class IntervalState:
340
355
  "is_dev",
341
356
  "is_removed",
342
357
  "is_pending_restatement",
358
+ "last_altered_ts",
343
359
  )
344
360
  .from_(exp.to_table(self.intervals_table).as_("intervals"))
345
361
  .order_by(
@@ -431,7 +447,9 @@ class IntervalState:
431
447
 
432
448
 
433
449
  def _intervals_to_df(
434
- snapshot_intervals: t.Sequence[t.Tuple[t.Union[SnapshotInfoLike, SnapshotIntervals], Interval]],
450
+ snapshot_intervals: t.Sequence[
451
+ t.Tuple[t.Union[SnapshotIdAndVersionLike, SnapshotIntervals], Interval]
452
+ ],
435
453
  is_dev: bool,
436
454
  is_removed: bool,
437
455
  ) -> pd.DataFrame:
@@ -451,13 +469,14 @@ def _intervals_to_df(
451
469
 
452
470
 
453
471
  def _interval_to_df(
454
- snapshot: t.Union[SnapshotInfoLike, SnapshotIntervals],
472
+ snapshot: t.Union[SnapshotIdAndVersionLike, SnapshotIntervals],
455
473
  start_ts: int,
456
474
  end_ts: int,
457
475
  is_dev: bool = False,
458
476
  is_removed: bool = False,
459
477
  is_compacted: bool = False,
460
478
  is_pending_restatement: bool = False,
479
+ last_altered_ts: t.Optional[int] = None,
461
480
  ) -> t.Dict[str, t.Any]:
462
481
  return {
463
482
  "id": random_id(),
@@ -472,4 +491,5 @@ def _interval_to_df(
472
491
  "is_removed": is_removed,
473
492
  "is_compacted": is_compacted,
474
493
  "is_pending_restatement": is_pending_restatement,
494
+ "last_altered_ts": last_altered_ts,
475
495
  }
@@ -30,7 +30,6 @@ from sqlmesh.core.state_sync.base import (
30
30
  MIN_SCHEMA_VERSION,
31
31
  MIN_SQLMESH_VERSION,
32
32
  )
33
- from sqlmesh.core.state_sync.base import StateSync
34
33
  from sqlmesh.core.state_sync.db.environment import EnvironmentState
35
34
  from sqlmesh.core.state_sync.db.interval import IntervalState
36
35
  from sqlmesh.core.state_sync.db.snapshot import SnapshotState
@@ -85,7 +84,7 @@ class StateMigrator:
85
84
 
86
85
  def migrate(
87
86
  self,
88
- state_sync: StateSync,
87
+ schema: t.Optional[str],
89
88
  skip_backup: bool = False,
90
89
  promoted_snapshots_only: bool = True,
91
90
  ) -> None:
@@ -94,7 +93,7 @@ class StateMigrator:
94
93
  migration_start_ts = time.perf_counter()
95
94
 
96
95
  try:
97
- migrate_rows = self._apply_migrations(state_sync, skip_backup)
96
+ migrate_rows = self._apply_migrations(schema, skip_backup)
98
97
 
99
98
  if not migrate_rows and major_minor(SQLMESH_VERSION) == versions.minor_sqlmesh_version:
100
99
  return
@@ -153,7 +152,7 @@ class StateMigrator:
153
152
 
154
153
  def _apply_migrations(
155
154
  self,
156
- state_sync: StateSync,
155
+ schema: t.Optional[str],
157
156
  skip_backup: bool,
158
157
  ) -> bool:
159
158
  versions = self.version_state.get_versions()
@@ -184,10 +183,10 @@ class StateMigrator:
184
183
 
185
184
  for migration in migrations:
186
185
  logger.info(f"Applying migration {migration}")
187
- migration.migrate_schemas(state_sync)
186
+ migration.migrate_schemas(engine_adapter=self.engine_adapter, schema=schema)
188
187
  if state_table_exist:
189
188
  # No need to run DML for the initial migration since all tables are empty
190
- migration.migrate_rows(state_sync)
189
+ migration.migrate_rows(engine_adapter=self.engine_adapter, schema=schema)
191
190
 
192
191
  snapshot_count_after = self.snapshot_state.count()
193
192
 
@@ -229,6 +228,7 @@ class StateMigrator:
229
228
  "updated_ts": updated_ts,
230
229
  "unpaused_ts": unpaused_ts,
231
230
  "unrestorable": unrestorable,
231
+ "forward_only": forward_only,
232
232
  }
233
233
  for where in (
234
234
  snapshot_id_filter(
@@ -237,10 +237,16 @@ class StateMigrator:
237
237
  if snapshots is not None
238
238
  else [None]
239
239
  )
240
- for name, identifier, raw_snapshot, updated_ts, unpaused_ts, unrestorable in fetchall(
240
+ for name, identifier, raw_snapshot, updated_ts, unpaused_ts, unrestorable, forward_only in fetchall(
241
241
  self.engine_adapter,
242
242
  exp.select(
243
- "name", "identifier", "snapshot", "updated_ts", "unpaused_ts", "unrestorable"
243
+ "name",
244
+ "identifier",
245
+ "snapshot",
246
+ "updated_ts",
247
+ "unpaused_ts",
248
+ "unrestorable",
249
+ "forward_only",
244
250
  )
245
251
  .from_(self.snapshot_state.snapshots_table)
246
252
  .where(where)
@@ -14,7 +14,6 @@ from sqlmesh.core.state_sync.db.utils import (
14
14
  snapshot_id_filter,
15
15
  fetchone,
16
16
  fetchall,
17
- create_batches,
18
17
  )
19
18
  from sqlmesh.core.environment import Environment
20
19
  from sqlmesh.core.model import SeedModel, ModelKindName
@@ -30,6 +29,12 @@ from sqlmesh.core.snapshot import (
30
29
  SnapshotId,
31
30
  SnapshotFingerprint,
32
31
  )
32
+ from sqlmesh.core.state_sync.common import (
33
+ RowBoundary,
34
+ ExpiredSnapshotBatch,
35
+ ExpiredBatchRange,
36
+ LimitBoundary,
37
+ )
33
38
  from sqlmesh.utils.migration import index_text_type, blob_text_type
34
39
  from sqlmesh.utils.date import now_timestamp, TimeLike, to_timestamp
35
40
  from sqlmesh.utils import unique
@@ -43,9 +48,6 @@ logger = logging.getLogger(__name__)
43
48
 
44
49
  class SnapshotState:
45
50
  SNAPSHOT_BATCH_SIZE = 1000
46
- # Use a smaller batch size for expired snapshots to account for fetching
47
- # of all snapshots that share the same version.
48
- EXPIRED_SNAPSHOT_BATCH_SIZE = 200
49
51
 
50
52
  def __init__(
51
53
  self,
@@ -166,53 +168,62 @@ class SnapshotState:
166
168
  self,
167
169
  environments: t.Iterable[Environment],
168
170
  current_ts: int,
169
- ignore_ttl: bool = False,
170
- ) -> t.List[SnapshotTableCleanupTask]:
171
- """Aggregates the id's of the expired snapshots and creates a list of table cleanup tasks.
172
-
173
- Expired snapshots are snapshots that have exceeded their time-to-live
174
- and are no longer in use within an environment.
175
-
176
- Returns:
177
- The set of expired snapshot ids.
178
- The list of table cleanup tasks.
179
- """
180
- all_cleanup_targets = []
181
- for _, cleanup_targets in self._get_expired_snapshots(
182
- environments=environments,
183
- current_ts=current_ts,
184
- ignore_ttl=ignore_ttl,
185
- ):
186
- all_cleanup_targets.extend(cleanup_targets)
187
- return all_cleanup_targets
188
-
189
- def _get_expired_snapshots(
190
- self,
191
- environments: t.Iterable[Environment],
192
- current_ts: int,
193
- ignore_ttl: bool = False,
194
- ) -> t.Iterator[t.Tuple[t.Set[SnapshotId], t.List[SnapshotTableCleanupTask]]]:
195
- expired_query = exp.select("name", "identifier", "version").from_(self.snapshots_table)
171
+ ignore_ttl: bool,
172
+ batch_range: ExpiredBatchRange,
173
+ ) -> t.Optional[ExpiredSnapshotBatch]:
174
+ expired_query = exp.select("name", "identifier", "version", "updated_ts").from_(
175
+ self.snapshots_table
176
+ )
196
177
 
197
178
  if not ignore_ttl:
198
179
  expired_query = expired_query.where(
199
180
  (exp.column("updated_ts") + exp.column("ttl_ms")) <= current_ts
200
181
  )
201
182
 
183
+ expired_query = expired_query.where(batch_range.where_filter)
184
+
185
+ promoted_snapshot_ids = {
186
+ snapshot.snapshot_id
187
+ for environment in environments
188
+ for snapshot in (
189
+ environment.snapshots
190
+ if environment.finalized_ts is not None
191
+ # If the environment is not finalized, check both the current snapshots and the previous finalized snapshots
192
+ else [*environment.snapshots, *(environment.previous_finalized_snapshots or [])]
193
+ )
194
+ }
195
+
196
+ if promoted_snapshot_ids:
197
+ not_in_conditions = [
198
+ exp.not_(condition)
199
+ for condition in snapshot_id_filter(
200
+ self.engine_adapter,
201
+ promoted_snapshot_ids,
202
+ batch_size=self.SNAPSHOT_BATCH_SIZE,
203
+ )
204
+ ]
205
+ expired_query = expired_query.where(exp.and_(*not_in_conditions))
206
+
207
+ expired_query = expired_query.order_by(
208
+ exp.column("updated_ts"), exp.column("name"), exp.column("identifier")
209
+ )
210
+
211
+ if isinstance(batch_range.end, LimitBoundary):
212
+ expired_query = expired_query.limit(batch_range.end.batch_size)
213
+
214
+ rows = fetchall(self.engine_adapter, expired_query)
215
+
216
+ if not rows:
217
+ return None
218
+
202
219
  expired_candidates = {
203
220
  SnapshotId(name=name, identifier=identifier): SnapshotNameVersion(
204
221
  name=name, version=version
205
222
  )
206
- for name, identifier, version in fetchall(self.engine_adapter, expired_query)
223
+ for name, identifier, version, _ in rows
207
224
  }
208
225
  if not expired_candidates:
209
- return
210
-
211
- promoted_snapshot_ids = {
212
- snapshot.snapshot_id
213
- for environment in environments
214
- for snapshot in environment.snapshots
215
- }
226
+ return None
216
227
 
217
228
  def _is_snapshot_used(snapshot: SnapshotIdAndVersion) -> bool:
218
229
  return (
@@ -220,57 +231,73 @@ class SnapshotState:
220
231
  or snapshot.snapshot_id not in expired_candidates
221
232
  )
222
233
 
223
- unique_expired_versions = unique(expired_candidates.values())
224
- version_batches = create_batches(
225
- unique_expired_versions, batch_size=self.EXPIRED_SNAPSHOT_BATCH_SIZE
234
+ # Extract cursor values from last row for pagination
235
+ last_row = rows[-1]
236
+ last_row_boundary = RowBoundary(
237
+ updated_ts=last_row[3],
238
+ name=last_row[0],
239
+ identifier=last_row[1],
226
240
  )
227
- for versions_batch in version_batches:
228
- snapshots = self._get_snapshots_with_same_version(versions_batch)
229
-
230
- snapshots_by_version = defaultdict(set)
231
- snapshots_by_dev_version = defaultdict(set)
232
- for s in snapshots:
233
- snapshots_by_version[(s.name, s.version)].add(s.snapshot_id)
234
- snapshots_by_dev_version[(s.name, s.dev_version)].add(s.snapshot_id)
235
-
236
- expired_snapshots = [s for s in snapshots if not _is_snapshot_used(s)]
237
- all_expired_snapshot_ids = {s.snapshot_id for s in expired_snapshots}
238
-
239
- cleanup_targets: t.List[t.Tuple[SnapshotId, bool]] = []
240
- for snapshot in expired_snapshots:
241
- shared_version_snapshots = snapshots_by_version[(snapshot.name, snapshot.version)]
242
- shared_version_snapshots.discard(snapshot.snapshot_id)
243
-
244
- shared_dev_version_snapshots = snapshots_by_dev_version[
245
- (snapshot.name, snapshot.dev_version)
246
- ]
247
- shared_dev_version_snapshots.discard(snapshot.snapshot_id)
248
-
249
- if not shared_dev_version_snapshots:
250
- dev_table_only = bool(shared_version_snapshots)
251
- cleanup_targets.append((snapshot.snapshot_id, dev_table_only))
252
-
253
- snapshot_ids_to_cleanup = [snapshot_id for snapshot_id, _ in cleanup_targets]
254
- for snapshot_id_batch in create_batches(
255
- snapshot_ids_to_cleanup, batch_size=self.SNAPSHOT_BATCH_SIZE
256
- ):
257
- snapshot_id_batch_set = set(snapshot_id_batch)
258
- full_snapshots = self._get_snapshots(snapshot_id_batch_set)
259
- cleanup_tasks = [
241
+ # The returned batch_range represents the actual range of rows in this batch
242
+ result_batch_range = ExpiredBatchRange(
243
+ start=batch_range.start,
244
+ end=last_row_boundary,
245
+ )
246
+
247
+ unique_expired_versions = unique(expired_candidates.values())
248
+ expired_snapshot_ids: t.Set[SnapshotId] = set()
249
+ cleanup_tasks: t.List[SnapshotTableCleanupTask] = []
250
+
251
+ snapshots = self._get_snapshots_with_same_version(unique_expired_versions)
252
+
253
+ snapshots_by_version = defaultdict(set)
254
+ snapshots_by_dev_version = defaultdict(set)
255
+ for s in snapshots:
256
+ snapshots_by_version[(s.name, s.version)].add(s.snapshot_id)
257
+ snapshots_by_dev_version[(s.name, s.dev_version)].add(s.snapshot_id)
258
+
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]] = []
263
+ 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:
280
+ cleanup_tasks.append(
260
281
  SnapshotTableCleanupTask(
261
282
  snapshot=full_snapshots[snapshot_id].table_info,
262
283
  dev_table_only=dev_table_only,
263
284
  )
264
- for snapshot_id, dev_table_only in cleanup_targets
265
- if snapshot_id in full_snapshots
266
- ]
267
- all_expired_snapshot_ids -= snapshot_id_batch_set
268
- yield snapshot_id_batch_set, cleanup_tasks
285
+ )
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)
292
+
293
+ if expired_snapshot_ids or cleanup_tasks:
294
+ return ExpiredSnapshotBatch(
295
+ expired_snapshot_ids=expired_snapshot_ids,
296
+ cleanup_tasks=cleanup_tasks,
297
+ batch_range=result_batch_range,
298
+ )
269
299
 
270
- if all_expired_snapshot_ids:
271
- # Remaining expired snapshots for which there are no tables
272
- # to cleanup
273
- yield all_expired_snapshot_ids, []
300
+ return None
274
301
 
275
302
  def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None:
276
303
  """Deletes snapshots.
@@ -337,6 +364,7 @@ class SnapshotState:
337
364
  name=name,
338
365
  identifier=identifier,
339
366
  version=version,
367
+ kind_name=kind_name or None,
340
368
  dev_version=dev_version,
341
369
  fingerprint=fingerprint,
342
370
  )
@@ -344,9 +372,11 @@ class SnapshotState:
344
372
  snapshot_names=snapshot_names,
345
373
  batch_size=self.SNAPSHOT_BATCH_SIZE,
346
374
  )
347
- for name, identifier, version, dev_version, fingerprint in fetchall(
375
+ for name, identifier, version, kind_name, dev_version, fingerprint in fetchall(
348
376
  self.engine_adapter,
349
- exp.select("name", "identifier", "version", "dev_version", "fingerprint")
377
+ exp.select(
378
+ "name", "identifier", "version", "kind_name", "dev_version", "fingerprint"
379
+ )
350
380
  .from_(self.snapshots_table)
351
381
  .where(where)
352
382
  .and_(unexpired_expr),
@@ -661,6 +691,7 @@ class SnapshotState:
661
691
  "name",
662
692
  "identifier",
663
693
  "version",
694
+ "kind_name",
664
695
  "dev_version",
665
696
  "fingerprint",
666
697
  )
@@ -677,10 +708,11 @@ class SnapshotState:
677
708
  name=name,
678
709
  identifier=identifier,
679
710
  version=version,
711
+ kind_name=kind_name or None,
680
712
  dev_version=dev_version,
681
713
  fingerprint=SnapshotFingerprint.parse_raw(fingerprint),
682
714
  )
683
- for name, identifier, version, dev_version, fingerprint in snapshot_rows
715
+ for name, identifier, version, kind_name, dev_version, fingerprint in snapshot_rows
684
716
  ]
685
717
 
686
718
 
@@ -367,8 +367,8 @@ class TableDiff:
367
367
  column_type = matched_columns[name]
368
368
  qualified_column = exp.column(name, table)
369
369
 
370
- if column_type.is_type(*exp.DataType.FLOAT_TYPES):
371
- return exp.func("ROUND", qualified_column, exp.Literal.number(self.decimals))
370
+ if column_type.is_type(*exp.DataType.REAL_TYPES):
371
+ return self.adapter._normalize_decimal_value(qualified_column, self.decimals)
372
372
  if column_type.is_type(*exp.DataType.NESTED_TYPES):
373
373
  return self.adapter._normalize_nested_value(qualified_column)
374
374
 
@@ -100,8 +100,11 @@ class ModelTest(unittest.TestCase):
100
100
  self._validate_and_normalize_test()
101
101
 
102
102
  if self.engine_adapter.default_catalog:
103
- self._fixture_catalog: t.Optional[exp.Identifier] = exp.parse_identifier(
104
- self.engine_adapter.default_catalog, dialect=self._test_adapter_dialect
103
+ self._fixture_catalog: t.Optional[exp.Identifier] = normalize_identifiers(
104
+ exp.parse_identifier(
105
+ self.engine_adapter.default_catalog, dialect=self._test_adapter_dialect
106
+ ),
107
+ dialect=self._test_adapter_dialect,
105
108
  )
106
109
  else:
107
110
  self._fixture_catalog = None
@@ -451,6 +454,9 @@ class ModelTest(unittest.TestCase):
451
454
  query = outputs.get("query")
452
455
  partial = outputs.pop("partial", None)
453
456
 
457
+ if ctes is None and query is None:
458
+ _raise_error("Incomplete test, outputs must contain 'query' or 'ctes'", self.path)
459
+
454
460
  def _normalize_rows(
455
461
  values: t.List[Row] | t.Dict,
456
462
  name: str,
@@ -641,16 +647,16 @@ class ModelTest(unittest.TestCase):
641
647
  return self._execute(query)
642
648
 
643
649
  rows = values["rows"]
650
+ columns_str: t.Optional[t.List[str]] = None
644
651
  if columns:
652
+ columns_str = [str(c) for c in columns]
645
653
  referenced_columns = list(dict.fromkeys(col for row in rows for col in row))
646
654
  _raise_if_unexpected_columns(columns, referenced_columns)
647
655
 
648
656
  if partial:
649
- columns = referenced_columns
657
+ columns_str = [c for c in columns_str if c in referenced_columns]
650
658
 
651
- return pd.DataFrame.from_records(
652
- rows, columns=[str(c) for c in columns] if columns else None
653
- )
659
+ return pd.DataFrame.from_records(rows, columns=columns_str)
654
660
 
655
661
  def _add_missing_columns(
656
662
  self, query: exp.Query, all_columns: t.Optional[t.Collection[str]] = None
@@ -801,7 +807,7 @@ class PythonModelTest(ModelTest):
801
807
  actual_df.reset_index(drop=True, inplace=True)
802
808
  expected = self._create_df(values, columns=self.model.columns_to_types, partial=partial)
803
809
 
804
- self.assert_equal(expected, actual_df, sort=False, partial=partial)
810
+ self.assert_equal(expected, actual_df, sort=True, partial=partial)
805
811
 
806
812
  def _execute_model(self) -> pd.DataFrame:
807
813
  """Executes the python model and returns a DataFrame."""
@@ -919,8 +925,7 @@ def generate_test(
919
925
  cte_output = test._execute(cte_query)
920
926
  ctes[cte.alias] = (
921
927
  pandas_timestamp_to_pydatetime(
922
- cte_output.apply(lambda col: col.map(_normalize_df_value)),
923
- cte_query.named_selects,
928
+ df=cte_output.apply(lambda col: col.map(_normalize_df_value)),
924
929
  )
925
930
  .replace({np.nan: None})
926
931
  .to_dict(orient="records")
@@ -20,6 +20,10 @@ class ModelTestMetadata(PydanticModel):
20
20
  def fully_qualified_test_name(self) -> str:
21
21
  return f"{self.path}::{self.test_name}"
22
22
 
23
+ @property
24
+ def model_name(self) -> str:
25
+ return self.body.get("model", "")
26
+
23
27
  def __hash__(self) -> int:
24
28
  return self.fully_qualified_test_name.__hash__()
25
29
 
sqlmesh/dbt/adapter.py CHANGED
@@ -115,30 +115,39 @@ class BaseAdapter(abc.ABC):
115
115
  """Returns the value quoted according to the quote policy."""
116
116
  return self.quote(value) if getattr(self.quote_policy, component_type, False) else value
117
117
 
118
- def dispatch(self, name: str, package: t.Optional[str] = None) -> t.Callable:
118
+ def dispatch(
119
+ self,
120
+ macro_name: str,
121
+ macro_namespace: t.Optional[str] = None,
122
+ ) -> t.Callable:
119
123
  """Returns a dialect-specific version of a macro with the given name."""
120
124
  target_type = self.jinja_globals["target"]["type"]
121
- macro_suffix = f"__{name}"
125
+ macro_suffix = f"__{macro_name}"
122
126
 
123
127
  def _relevance(package_name_pair: t.Tuple[t.Optional[str], str]) -> t.Tuple[int, int]:
124
128
  """Lower scores more relevant."""
125
- macro_package, macro_name = package_name_pair
129
+ macro_package, name = package_name_pair
126
130
 
127
- package_score = 0 if macro_package == package else 1
131
+ package_score = 0 if macro_package == macro_namespace else 1
128
132
  name_score = 1
129
133
 
130
- if macro_name.startswith("default"):
134
+ if name.startswith("default"):
131
135
  name_score = 2
132
- elif macro_name.startswith(target_type):
136
+ elif name.startswith(target_type):
133
137
  name_score = 0
134
138
 
135
139
  return name_score, package_score
136
140
 
137
141
  jinja_env = self.jinja_macros.build_environment(**self.jinja_globals).globals
138
- packages_to_check: t.List[t.Optional[str]] = [
139
- package,
140
- *(k for k in jinja_env if k.startswith("dbt")),
141
- ]
142
+
143
+ packages_to_check: t.List[t.Optional[str]] = [None]
144
+ if macro_namespace is not None:
145
+ if macro_namespace in jinja_env:
146
+ packages_to_check = [self.jinja_macros.root_package_name, macro_namespace]
147
+
148
+ # Add dbt packages as fallback
149
+ packages_to_check.extend(k for k in jinja_env if k.startswith("dbt"))
150
+
142
151
  candidates = {}
143
152
  for macro_package in packages_to_check:
144
153
  macros = jinja_env.get(macro_package, {}) if macro_package else jinja_env
@@ -156,7 +165,7 @@ class BaseAdapter(abc.ABC):
156
165
  sorted_candidates = sorted(candidates, key=_relevance)
157
166
  return candidates[sorted_candidates[0]]
158
167
 
159
- raise ConfigError(f"Macro '{name}', package '{package}' was not found.")
168
+ raise ConfigError(f"Macro '{macro_name}', package '{macro_namespace}' was not found.")
160
169
 
161
170
  def type(self) -> str:
162
171
  return self.project_dialect or ""