sqlmesh 0.213.1.dev1__py3-none-any.whl → 0.227.2.dev4__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 (252) hide show
  1. sqlmesh/__init__.py +12 -2
  2. sqlmesh/_version.py +2 -2
  3. sqlmesh/cli/main.py +0 -44
  4. sqlmesh/cli/project_init.py +11 -2
  5. sqlmesh/core/_typing.py +1 -0
  6. sqlmesh/core/audit/definition.py +8 -2
  7. sqlmesh/core/config/__init__.py +1 -1
  8. sqlmesh/core/config/connection.py +17 -5
  9. sqlmesh/core/config/dbt.py +13 -0
  10. sqlmesh/core/config/janitor.py +12 -0
  11. sqlmesh/core/config/loader.py +7 -0
  12. sqlmesh/core/config/model.py +2 -0
  13. sqlmesh/core/config/root.py +3 -0
  14. sqlmesh/core/console.py +81 -3
  15. sqlmesh/core/constants.py +1 -1
  16. sqlmesh/core/context.py +69 -26
  17. sqlmesh/core/dialect.py +3 -0
  18. sqlmesh/core/engine_adapter/_typing.py +2 -0
  19. sqlmesh/core/engine_adapter/base.py +322 -22
  20. sqlmesh/core/engine_adapter/base_postgres.py +17 -1
  21. sqlmesh/core/engine_adapter/bigquery.py +146 -7
  22. sqlmesh/core/engine_adapter/clickhouse.py +17 -13
  23. sqlmesh/core/engine_adapter/databricks.py +33 -2
  24. sqlmesh/core/engine_adapter/fabric.py +10 -29
  25. sqlmesh/core/engine_adapter/mixins.py +142 -48
  26. sqlmesh/core/engine_adapter/mssql.py +15 -4
  27. sqlmesh/core/engine_adapter/mysql.py +2 -2
  28. sqlmesh/core/engine_adapter/postgres.py +9 -3
  29. sqlmesh/core/engine_adapter/redshift.py +4 -0
  30. sqlmesh/core/engine_adapter/risingwave.py +1 -0
  31. sqlmesh/core/engine_adapter/shared.py +6 -0
  32. sqlmesh/core/engine_adapter/snowflake.py +82 -11
  33. sqlmesh/core/engine_adapter/spark.py +14 -10
  34. sqlmesh/core/engine_adapter/trino.py +4 -2
  35. sqlmesh/core/environment.py +2 -0
  36. sqlmesh/core/janitor.py +181 -0
  37. sqlmesh/core/lineage.py +1 -0
  38. sqlmesh/core/linter/definition.py +13 -13
  39. sqlmesh/core/linter/rules/builtin.py +29 -0
  40. sqlmesh/core/macros.py +35 -13
  41. sqlmesh/core/model/common.py +2 -0
  42. sqlmesh/core/model/definition.py +82 -28
  43. sqlmesh/core/model/kind.py +66 -2
  44. sqlmesh/core/model/meta.py +108 -4
  45. sqlmesh/core/node.py +101 -1
  46. sqlmesh/core/plan/builder.py +18 -10
  47. sqlmesh/core/plan/common.py +199 -2
  48. sqlmesh/core/plan/definition.py +25 -6
  49. sqlmesh/core/plan/evaluator.py +75 -113
  50. sqlmesh/core/plan/explainer.py +90 -8
  51. sqlmesh/core/plan/stages.py +42 -21
  52. sqlmesh/core/renderer.py +78 -32
  53. sqlmesh/core/scheduler.py +102 -22
  54. sqlmesh/core/selector.py +137 -9
  55. sqlmesh/core/signal.py +64 -1
  56. sqlmesh/core/snapshot/__init__.py +2 -0
  57. sqlmesh/core/snapshot/definition.py +146 -34
  58. sqlmesh/core/snapshot/evaluator.py +689 -124
  59. sqlmesh/core/state_sync/__init__.py +0 -1
  60. sqlmesh/core/state_sync/base.py +55 -33
  61. sqlmesh/core/state_sync/cache.py +12 -7
  62. sqlmesh/core/state_sync/common.py +216 -111
  63. sqlmesh/core/state_sync/db/environment.py +6 -4
  64. sqlmesh/core/state_sync/db/facade.py +42 -24
  65. sqlmesh/core/state_sync/db/interval.py +27 -7
  66. sqlmesh/core/state_sync/db/migrator.py +34 -16
  67. sqlmesh/core/state_sync/db/snapshot.py +177 -169
  68. sqlmesh/core/table_diff.py +2 -2
  69. sqlmesh/core/test/context.py +2 -0
  70. sqlmesh/core/test/definition.py +14 -9
  71. sqlmesh/dbt/adapter.py +22 -16
  72. sqlmesh/dbt/basemodel.py +75 -56
  73. sqlmesh/dbt/builtin.py +116 -12
  74. sqlmesh/dbt/column.py +17 -5
  75. sqlmesh/dbt/common.py +19 -5
  76. sqlmesh/dbt/context.py +14 -1
  77. sqlmesh/dbt/loader.py +61 -9
  78. sqlmesh/dbt/manifest.py +174 -16
  79. sqlmesh/dbt/model.py +183 -85
  80. sqlmesh/dbt/package.py +16 -1
  81. sqlmesh/dbt/profile.py +3 -3
  82. sqlmesh/dbt/project.py +12 -7
  83. sqlmesh/dbt/seed.py +6 -1
  84. sqlmesh/dbt/source.py +13 -1
  85. sqlmesh/dbt/target.py +25 -6
  86. sqlmesh/dbt/test.py +36 -5
  87. sqlmesh/migrations/v0000_baseline.py +95 -0
  88. sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py +5 -7
  89. sqlmesh/migrations/v0062_add_model_gateway.py +5 -1
  90. sqlmesh/migrations/v0063_change_signals.py +5 -3
  91. sqlmesh/migrations/v0064_join_when_matched_strings.py +5 -3
  92. sqlmesh/migrations/v0065_add_model_optimize.py +5 -1
  93. sqlmesh/migrations/v0066_add_auto_restatements.py +8 -3
  94. sqlmesh/migrations/v0067_add_tsql_date_full_precision.py +5 -1
  95. sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py +5 -1
  96. sqlmesh/migrations/v0069_update_dev_table_suffix.py +5 -3
  97. sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py +5 -1
  98. sqlmesh/migrations/v0071_add_dev_version_to_intervals.py +9 -5
  99. sqlmesh/migrations/v0072_add_environment_statements.py +5 -3
  100. sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py +5 -3
  101. sqlmesh/migrations/v0074_add_partition_by_time_column_property.py +5 -1
  102. sqlmesh/migrations/v0075_remove_validate_query.py +5 -3
  103. sqlmesh/migrations/v0076_add_cron_tz.py +5 -1
  104. sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py +5 -1
  105. sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py +5 -3
  106. sqlmesh/migrations/v0079_add_gateway_managed_property.py +10 -5
  107. sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py +5 -1
  108. sqlmesh/migrations/v0081_update_partitioned_by.py +5 -3
  109. sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py +5 -3
  110. sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py +5 -1
  111. sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py +5 -1
  112. sqlmesh/migrations/v0085_deterministic_repr.py +5 -3
  113. sqlmesh/migrations/v0086_check_deterministic_bug.py +5 -3
  114. sqlmesh/migrations/v0087_normalize_blueprint_variables.py +5 -3
  115. sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py +5 -3
  116. sqlmesh/migrations/v0089_add_virtual_environment_mode.py +5 -1
  117. sqlmesh/migrations/v0090_add_forward_only_column.py +9 -5
  118. sqlmesh/migrations/v0091_on_additive_change.py +5 -1
  119. sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py +5 -3
  120. sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py +5 -1
  121. sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py +123 -0
  122. sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py +49 -0
  123. sqlmesh/migrations/v0096_remove_plan_dags_table.py +13 -0
  124. sqlmesh/migrations/v0097_add_dbt_name_in_node.py +9 -0
  125. sqlmesh/migrations/{v0060_move_audits_to_model.py → v0098_add_dbt_node_info_in_node.py} +33 -16
  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/connection_pool.py +2 -1
  131. sqlmesh/utils/dag.py +65 -10
  132. sqlmesh/utils/date.py +8 -1
  133. sqlmesh/utils/errors.py +8 -0
  134. sqlmesh/utils/jinja.py +54 -4
  135. sqlmesh/utils/pydantic.py +6 -6
  136. sqlmesh/utils/windows.py +13 -3
  137. {sqlmesh-0.213.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/METADATA +7 -10
  138. sqlmesh-0.227.2.dev4.dist-info/RECORD +370 -0
  139. sqlmesh_dbt/cli.py +70 -7
  140. sqlmesh_dbt/console.py +14 -6
  141. sqlmesh_dbt/operations.py +103 -24
  142. sqlmesh_dbt/selectors.py +39 -1
  143. web/client/dist/assets/{Audits-Ucsx1GzF.js → Audits-CBiYyyx-.js} +1 -1
  144. web/client/dist/assets/{Banner-BWDzvavM.js → Banner-DSRbUlO5.js} +1 -1
  145. web/client/dist/assets/{ChevronDownIcon-D2VL13Ah.js → ChevronDownIcon-MK_nrjD_.js} +1 -1
  146. web/client/dist/assets/{ChevronRightIcon-DWGYbf1l.js → ChevronRightIcon-CLWtT22Q.js} +1 -1
  147. web/client/dist/assets/{Content-DdHDZM3I.js → Content-BNuGZN5l.js} +1 -1
  148. web/client/dist/assets/{Content-Bikfy8fh.js → Content-CSHJyW0n.js} +1 -1
  149. web/client/dist/assets/{Data-CzAJH7rW.js → Data-C1oRDbLx.js} +1 -1
  150. web/client/dist/assets/{DataCatalog-BJF11g8f.js → DataCatalog-HXyX2-_j.js} +1 -1
  151. web/client/dist/assets/{Editor-s0SBpV2y.js → Editor-BDyfpUuw.js} +1 -1
  152. web/client/dist/assets/{Editor-DgLhgKnm.js → Editor-D0jNItwC.js} +1 -1
  153. web/client/dist/assets/{Errors-D0m0O1d3.js → Errors-BfuFLcPi.js} +1 -1
  154. web/client/dist/assets/{FileExplorer-CEv0vXkt.js → FileExplorer-BR9IE3he.js} +1 -1
  155. web/client/dist/assets/{Footer-BwzXn8Ew.js → Footer-CgBEtiAh.js} +1 -1
  156. web/client/dist/assets/{Header-6heDkEqG.js → Header-DSqR6nSO.js} +1 -1
  157. web/client/dist/assets/{Input-obuJsD6k.js → Input-B-oZ6fGO.js} +1 -1
  158. web/client/dist/assets/Lineage-DYQVwDbD.js +1 -0
  159. web/client/dist/assets/{ListboxShow-HM9_qyrt.js → ListboxShow-BE5-xevs.js} +1 -1
  160. web/client/dist/assets/{ModelLineage-zWdKo0U2.js → ModelLineage-DkIFAYo4.js} +1 -1
  161. web/client/dist/assets/{Models-Bcu66SRz.js → Models-D5dWr8RB.js} +1 -1
  162. web/client/dist/assets/{Page-BWEEQfIt.js → Page-C-XfU5BR.js} +1 -1
  163. web/client/dist/assets/{Plan-C4gXCqlf.js → Plan-ZEuTINBq.js} +1 -1
  164. web/client/dist/assets/{PlusCircleIcon-CVDO651q.js → PlusCircleIcon-DVXAHG8_.js} +1 -1
  165. web/client/dist/assets/{ReportErrors-BT6xFwAr.js → ReportErrors-B7FEPzMB.js} +1 -1
  166. web/client/dist/assets/{Root-ryJoBK4h.js → Root-8aZyhPxF.js} +1 -1
  167. web/client/dist/assets/{SearchList-DB04sPb9.js → SearchList-W_iT2G82.js} +1 -1
  168. web/client/dist/assets/{SelectEnvironment-CUYcXUu6.js → SelectEnvironment-C65jALmO.js} +1 -1
  169. web/client/dist/assets/{SourceList-Doo_9ZGp.js → SourceList-DSLO6nVJ.js} +1 -1
  170. web/client/dist/assets/{SourceListItem-D5Mj7Dly.js → SourceListItem-BHt8d9-I.js} +1 -1
  171. web/client/dist/assets/{SplitPane-qHmkD1qy.js → SplitPane-CViaZmw6.js} +1 -1
  172. web/client/dist/assets/{Tests-DH1Z74ML.js → Tests-DhaVt5t1.js} +1 -1
  173. web/client/dist/assets/{Welcome-DqUJUNMF.js → Welcome-DvpjH-_4.js} +1 -1
  174. web/client/dist/assets/context-BctCsyGb.js +71 -0
  175. web/client/dist/assets/{context-Dr54UHLi.js → context-DFNeGsFF.js} +1 -1
  176. web/client/dist/assets/{editor-DYIP1yQ4.js → editor-CcO28cqd.js} +1 -1
  177. web/client/dist/assets/{file-DarlIDVi.js → file-CvJN3aZO.js} +1 -1
  178. web/client/dist/assets/{floating-ui.react-dom-BH3TFvkM.js → floating-ui.react-dom-CjE-JNW1.js} +1 -1
  179. web/client/dist/assets/{help-Bl8wqaQc.js → help-DuPhjipa.js} +1 -1
  180. web/client/dist/assets/{index-D1sR7wpN.js → index-C-dJH7yZ.js} +1 -1
  181. web/client/dist/assets/{index-O3mjYpnE.js → index-Dj0i1-CA.js} +2 -2
  182. web/client/dist/assets/{plan-CehRrJUG.js → plan-BTRSbjKn.js} +1 -1
  183. web/client/dist/assets/{popover-CqgMRE0G.js → popover-_Sf0yvOI.js} +1 -1
  184. web/client/dist/assets/{project-6gxepOhm.js → project-BvSOI8MY.js} +1 -1
  185. web/client/dist/index.html +1 -1
  186. sqlmesh/integrations/llm.py +0 -56
  187. sqlmesh/migrations/v0001_init.py +0 -60
  188. sqlmesh/migrations/v0002_remove_identify.py +0 -5
  189. sqlmesh/migrations/v0003_move_batch_size.py +0 -34
  190. sqlmesh/migrations/v0004_environmnent_add_finalized_at.py +0 -23
  191. sqlmesh/migrations/v0005_create_seed_table.py +0 -24
  192. sqlmesh/migrations/v0006_change_seed_hash.py +0 -5
  193. sqlmesh/migrations/v0007_env_table_info_to_kind.py +0 -99
  194. sqlmesh/migrations/v0008_create_intervals_table.py +0 -38
  195. sqlmesh/migrations/v0009_remove_pre_post_hooks.py +0 -62
  196. sqlmesh/migrations/v0010_seed_hash_batch_size.py +0 -5
  197. sqlmesh/migrations/v0011_add_model_kind_name.py +0 -63
  198. sqlmesh/migrations/v0012_update_jinja_expressions.py +0 -86
  199. sqlmesh/migrations/v0013_serde_using_model_dialects.py +0 -87
  200. sqlmesh/migrations/v0014_fix_dev_intervals.py +0 -14
  201. sqlmesh/migrations/v0015_environment_add_promoted_snapshot_ids.py +0 -26
  202. sqlmesh/migrations/v0016_fix_windows_path.py +0 -59
  203. sqlmesh/migrations/v0017_fix_windows_seed_path.py +0 -55
  204. sqlmesh/migrations/v0018_rename_snapshot_model_to_node.py +0 -53
  205. sqlmesh/migrations/v0019_add_env_suffix_target.py +0 -28
  206. sqlmesh/migrations/v0020_remove_redundant_attributes_from_dbt_models.py +0 -80
  207. sqlmesh/migrations/v0021_fix_table_properties.py +0 -62
  208. sqlmesh/migrations/v0022_move_project_to_model.py +0 -54
  209. sqlmesh/migrations/v0023_fix_added_models_with_forward_only_parents.py +0 -65
  210. sqlmesh/migrations/v0024_replace_model_kind_name_enum_with_value.py +0 -55
  211. sqlmesh/migrations/v0025_fix_intervals_and_missing_change_category.py +0 -117
  212. sqlmesh/migrations/v0026_remove_dialect_from_seed.py +0 -55
  213. sqlmesh/migrations/v0027_minute_interval_to_five.py +0 -57
  214. sqlmesh/migrations/v0028_add_plan_dags_table.py +0 -29
  215. sqlmesh/migrations/v0029_generate_schema_types_using_dialect.py +0 -69
  216. sqlmesh/migrations/v0030_update_unrestorable_snapshots.py +0 -65
  217. sqlmesh/migrations/v0031_remove_dbt_target_fields.py +0 -65
  218. sqlmesh/migrations/v0032_add_sqlmesh_version.py +0 -25
  219. sqlmesh/migrations/v0033_mysql_fix_blob_text_type.py +0 -45
  220. sqlmesh/migrations/v0034_add_default_catalog.py +0 -367
  221. sqlmesh/migrations/v0035_add_catalog_name_override.py +0 -22
  222. sqlmesh/migrations/v0036_delete_plan_dags_bug_fix.py +0 -14
  223. sqlmesh/migrations/v0037_remove_dbt_is_incremental_macro.py +0 -61
  224. sqlmesh/migrations/v0038_add_expiration_ts_to_snapshot.py +0 -73
  225. sqlmesh/migrations/v0039_include_environment_in_plan_dag_spec.py +0 -68
  226. sqlmesh/migrations/v0040_add_previous_finalized_snapshots.py +0 -26
  227. sqlmesh/migrations/v0041_remove_hash_raw_query_attribute.py +0 -59
  228. sqlmesh/migrations/v0042_trim_indirect_versions.py +0 -66
  229. sqlmesh/migrations/v0043_fix_remove_obsolete_attributes_in_plan_dags.py +0 -61
  230. sqlmesh/migrations/v0044_quote_identifiers_in_model_attributes.py +0 -5
  231. sqlmesh/migrations/v0045_move_gateway_variable.py +0 -70
  232. sqlmesh/migrations/v0046_add_batch_concurrency.py +0 -8
  233. sqlmesh/migrations/v0047_change_scd_string_to_column.py +0 -5
  234. sqlmesh/migrations/v0048_drop_indirect_versions.py +0 -59
  235. sqlmesh/migrations/v0049_replace_identifier_with_version_in_seeds_table.py +0 -57
  236. sqlmesh/migrations/v0050_drop_seeds_table.py +0 -11
  237. sqlmesh/migrations/v0051_rename_column_descriptions.py +0 -65
  238. sqlmesh/migrations/v0052_add_normalize_name_in_environment_naming_info.py +0 -28
  239. sqlmesh/migrations/v0053_custom_model_kind_extra_attributes.py +0 -5
  240. sqlmesh/migrations/v0054_fix_trailing_comments.py +0 -5
  241. sqlmesh/migrations/v0055_add_updated_ts_unpaused_ts_ttl_ms_unrestorable_to_snapshot.py +0 -132
  242. sqlmesh/migrations/v0056_restore_table_indexes.py +0 -118
  243. sqlmesh/migrations/v0057_add_table_format.py +0 -5
  244. sqlmesh/migrations/v0058_add_requirements.py +0 -26
  245. sqlmesh/migrations/v0059_add_physical_version.py +0 -5
  246. sqlmesh-0.213.1.dev1.dist-info/RECORD +0 -421
  247. web/client/dist/assets/Lineage-D0Hgdz2v.js +0 -1
  248. web/client/dist/assets/context-DgX0fp2E.js +0 -68
  249. {sqlmesh-0.213.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/WHEEL +0 -0
  250. {sqlmesh-0.213.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/entry_points.txt +0 -0
  251. {sqlmesh-0.213.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/licenses/LICENSE +0 -0
  252. {sqlmesh-0.213.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/top_level.txt +0 -0
sqlmesh/core/signal.py CHANGED
@@ -1,7 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
-
3
+ import typing as t
4
4
  from sqlmesh.utils import UniqueKeyDict, registry_decorator
5
+ from sqlmesh.utils.errors import MissingSourceError
6
+
7
+ if t.TYPE_CHECKING:
8
+ from sqlmesh.core.context import ExecutionContext
9
+ from sqlmesh.core.snapshot.definition import Snapshot
10
+ from sqlmesh.utils.date import DatetimeRanges
11
+ from sqlmesh.core.snapshot.definition import DeployabilityIndex
5
12
 
6
13
 
7
14
  class signal(registry_decorator):
@@ -33,3 +40,59 @@ class signal(registry_decorator):
33
40
 
34
41
 
35
42
  SignalRegistry = UniqueKeyDict[str, signal]
43
+
44
+
45
+ @signal()
46
+ def freshness(
47
+ batch: DatetimeRanges,
48
+ snapshot: Snapshot,
49
+ context: ExecutionContext,
50
+ ) -> bool:
51
+ """
52
+ Implements model freshness as a signal, i.e it considers this model to be fresh if:
53
+ - Any upstream SQLMesh model has available intervals to compute i.e is fresh
54
+ - Any upstream external model has been altered since the last time the model was evaluated
55
+ """
56
+ adapter = context.engine_adapter
57
+ if context.is_restatement or not adapter.SUPPORTS_METADATA_TABLE_LAST_MODIFIED_TS:
58
+ return True
59
+
60
+ deployability_index = context.deployability_index or DeployabilityIndex.all_deployable()
61
+
62
+ last_altered_ts = (
63
+ snapshot.last_altered_ts
64
+ if deployability_index.is_deployable(snapshot)
65
+ else snapshot.dev_last_altered_ts
66
+ )
67
+
68
+ if not last_altered_ts:
69
+ return True
70
+
71
+ parent_snapshots = {context.snapshots[p.name] for p in snapshot.parents}
72
+
73
+ upstream_parent_snapshots = {p for p in parent_snapshots if not p.is_external}
74
+ external_parents = snapshot.node.depends_on - {p.name for p in upstream_parent_snapshots}
75
+
76
+ if context.parent_intervals:
77
+ # At least one upstream sqlmesh model has intervals to compute (i.e is fresh),
78
+ # so the current model is considered fresh too
79
+ return True
80
+
81
+ if external_parents:
82
+ external_last_altered_timestamps = adapter.get_table_last_modified_ts(
83
+ list(external_parents)
84
+ )
85
+
86
+ if len(external_last_altered_timestamps) != len(external_parents):
87
+ raise MissingSourceError(
88
+ f"Expected {len(external_parents)} sources to be present, but got {len(external_last_altered_timestamps)}."
89
+ )
90
+
91
+ # Finding new data means that the upstream depedencies have been altered
92
+ # since the last time the model was evaluated
93
+ return any(
94
+ external_last_altered_ts > last_altered_ts
95
+ for external_last_altered_ts in external_last_altered_timestamps
96
+ )
97
+
98
+ return False
@@ -4,12 +4,14 @@ from sqlmesh.core.snapshot.definition import (
4
4
  Node as Node,
5
5
  QualifiedViewName as QualifiedViewName,
6
6
  Snapshot as Snapshot,
7
+ SnapshotIdAndVersion as SnapshotIdAndVersion,
7
8
  SnapshotChangeCategory as SnapshotChangeCategory,
8
9
  SnapshotDataVersion as SnapshotDataVersion,
9
10
  SnapshotFingerprint as SnapshotFingerprint,
10
11
  SnapshotId as SnapshotId,
11
12
  SnapshotIdBatch as SnapshotIdBatch,
12
13
  SnapshotIdLike as SnapshotIdLike,
14
+ SnapshotIdAndVersionLike as SnapshotIdAndVersionLike,
13
15
  SnapshotInfoLike as SnapshotInfoLike,
14
16
  SnapshotIntervals as SnapshotIntervals,
15
17
  SnapshotNameVersion as SnapshotNameVersion,
@@ -185,6 +185,8 @@ class SnapshotIntervals(PydanticModel):
185
185
  intervals: Intervals = []
186
186
  dev_intervals: Intervals = []
187
187
  pending_restatement_intervals: Intervals = []
188
+ last_altered_ts: t.Optional[int] = None
189
+ dev_last_altered_ts: t.Optional[int] = None
188
190
 
189
191
  @property
190
192
  def snapshot_id(self) -> t.Optional[SnapshotId]:
@@ -205,6 +207,12 @@ class SnapshotIntervals(PydanticModel):
205
207
  def add_pending_restatement_interval(self, start: int, end: int) -> None:
206
208
  self._add_interval(start, end, "pending_restatement_intervals")
207
209
 
210
+ def update_last_altered_ts(self, last_altered_ts: t.Optional[int]) -> None:
211
+ self._update_last_altered_ts(last_altered_ts, "last_altered_ts")
212
+
213
+ def update_dev_last_altered_ts(self, last_altered_ts: t.Optional[int]) -> None:
214
+ self._update_last_altered_ts(last_altered_ts, "dev_last_altered_ts")
215
+
208
216
  def remove_interval(self, start: int, end: int) -> None:
209
217
  self._remove_interval(start, end, "intervals")
210
218
 
@@ -224,6 +232,13 @@ class SnapshotIntervals(PydanticModel):
224
232
  target_intervals = merge_intervals([*target_intervals, (start, end)])
225
233
  setattr(self, interval_attr, target_intervals)
226
234
 
235
+ def _update_last_altered_ts(
236
+ self, last_altered_ts: t.Optional[int], last_altered_attr: str
237
+ ) -> None:
238
+ if last_altered_ts:
239
+ existing_last_altered_ts = getattr(self, last_altered_attr)
240
+ setattr(self, last_altered_attr, max(existing_last_altered_ts or 0, last_altered_ts))
241
+
227
242
  def _remove_interval(self, start: int, end: int, interval_attr: str) -> None:
228
243
  target_intervals = getattr(self, interval_attr)
229
244
  target_intervals = remove_interval(target_intervals, start, end)
@@ -587,6 +602,67 @@ class SnapshotTableInfo(PydanticModel, SnapshotInfoMixin, frozen=True):
587
602
  """Returns the name and version of the snapshot."""
588
603
  return SnapshotNameVersion(name=self.name, version=self.version)
589
604
 
605
+ @property
606
+ def id_and_version(self) -> SnapshotIdAndVersion:
607
+ return SnapshotIdAndVersion(
608
+ name=self.name,
609
+ kind_name=self.kind_name,
610
+ identifier=self.identifier,
611
+ version=self.version,
612
+ dev_version=self.dev_version,
613
+ fingerprint=self.fingerprint,
614
+ )
615
+
616
+
617
+ class SnapshotIdAndVersion(PydanticModel, ModelKindMixin):
618
+ """A stripped down version of a snapshot that is used in situations where we want to fetch the main fields of the snapshots table
619
+ without the overhead of parsing the full snapshot payload and fetching intervals.
620
+ """
621
+
622
+ name: str
623
+ version: str
624
+ kind_name_: t.Optional[ModelKindName] = Field(default=None, alias="kind_name")
625
+ dev_version_: t.Optional[str] = Field(alias="dev_version")
626
+ identifier: str
627
+ fingerprint_: t.Union[str, SnapshotFingerprint] = Field(alias="fingerprint")
628
+
629
+ @property
630
+ def snapshot_id(self) -> SnapshotId:
631
+ return SnapshotId(name=self.name, identifier=self.identifier)
632
+
633
+ @property
634
+ def id_and_version(self) -> SnapshotIdAndVersion:
635
+ return self
636
+
637
+ @property
638
+ def name_version(self) -> SnapshotNameVersion:
639
+ return SnapshotNameVersion(name=self.name, version=self.version)
640
+
641
+ @property
642
+ def fingerprint(self) -> SnapshotFingerprint:
643
+ value = self.fingerprint_
644
+ if isinstance(value, str):
645
+ self.fingerprint_ = value = SnapshotFingerprint.parse_raw(value)
646
+ return value
647
+
648
+ @property
649
+ def dev_version(self) -> str:
650
+ return self.dev_version_ or self.fingerprint.to_version()
651
+
652
+ @property
653
+ def model_kind_name(self) -> t.Optional[ModelKindName]:
654
+ return self.kind_name_
655
+
656
+ def display_name(
657
+ self,
658
+ environment_naming_info: EnvironmentNamingInfo,
659
+ default_catalog: t.Optional[str],
660
+ dialect: DialectType = None,
661
+ ) -> str:
662
+ return model_display_name(
663
+ self.name, environment_naming_info, default_catalog, dialect=dialect
664
+ )
665
+
590
666
 
591
667
  class Snapshot(PydanticModel, SnapshotInfoMixin):
592
668
  """A snapshot represents a node at a certain point in time.
@@ -652,6 +728,10 @@ class Snapshot(PydanticModel, SnapshotInfoMixin):
652
728
  dev_table_suffix: str = "dev"
653
729
  table_naming_convention: TableNamingConvention = TableNamingConvention.default
654
730
  forward_only: bool = False
731
+ # Physical table last modified timestamp, not to be confused with the "updated_ts" field
732
+ # which is for the snapshot record itself
733
+ last_altered_ts: t.Optional[int] = None
734
+ dev_last_altered_ts: t.Optional[int] = None
655
735
 
656
736
  @field_validator("ttl")
657
737
  @classmethod
@@ -690,6 +770,7 @@ class Snapshot(PydanticModel, SnapshotInfoMixin):
690
770
  )
691
771
  for interval in snapshot_intervals:
692
772
  snapshot.merge_intervals(interval)
773
+
693
774
  result.append(snapshot)
694
775
 
695
776
  return result
@@ -896,12 +977,20 @@ class Snapshot(PydanticModel, SnapshotInfoMixin):
896
977
  if not apply_effective_from or end <= effective_from_ts:
897
978
  self.add_interval(start, end)
898
979
 
980
+ if other.last_altered_ts:
981
+ self.last_altered_ts = max(self.last_altered_ts or 0, other.last_altered_ts)
982
+
899
983
  if self.dev_version == other.dev_version:
900
984
  # Merge dev intervals if the dev versions match which would mean
901
985
  # that this and the other snapshot are pointing to the same dev table.
902
986
  for start, end in other.dev_intervals:
903
987
  self.add_interval(start, end, is_dev=True)
904
988
 
989
+ if other.dev_last_altered_ts:
990
+ self.dev_last_altered_ts = max(
991
+ self.dev_last_altered_ts or 0, other.dev_last_altered_ts
992
+ )
993
+
905
994
  self.pending_restatement_intervals = merge_intervals(
906
995
  [*self.pending_restatement_intervals, *other.pending_restatement_intervals]
907
996
  )
@@ -1020,6 +1109,7 @@ class Snapshot(PydanticModel, SnapshotInfoMixin):
1020
1109
  python_env=signals.python_env,
1021
1110
  dialect=self.model.dialect,
1022
1111
  path=self.model._path,
1112
+ snapshot=self,
1023
1113
  kwargs=kwargs,
1024
1114
  )
1025
1115
  except SQLMeshError as e:
@@ -1393,6 +1483,10 @@ class Snapshot(PydanticModel, SnapshotInfoMixin):
1393
1483
  """Returns the name and version of the snapshot."""
1394
1484
  return SnapshotNameVersion(name=self.name, version=self.version)
1395
1485
 
1486
+ @property
1487
+ def id_and_version(self) -> SnapshotIdAndVersion:
1488
+ return self.table_info.id_and_version
1489
+
1396
1490
  @property
1397
1491
  def disable_restatement(self) -> bool:
1398
1492
  """Is restatement disabled for the node"""
@@ -1412,19 +1506,19 @@ class Snapshot(PydanticModel, SnapshotInfoMixin):
1412
1506
  check_categorical_relative_expression=False,
1413
1507
  )
1414
1508
 
1509
+ @property
1510
+ def supports_schema_migration_in_prod(self) -> bool:
1511
+ """Returns whether or not this snapshot supports schema migration when deployed to production."""
1512
+ return self.is_paused and self.is_model and not self.is_symbolic and not self.is_seed
1513
+
1415
1514
  @property
1416
1515
  def requires_schema_migration_in_prod(self) -> bool:
1417
1516
  """Returns whether or not this snapshot requires a schema migration when deployed to production."""
1418
- return (
1419
- self.is_paused
1420
- and self.is_model
1421
- and self.is_materialized
1422
- and (
1423
- (self.previous_version and self.previous_version.version == self.version)
1424
- or self.model.forward_only
1425
- or bool(self.model.physical_version)
1426
- or not self.virtual_environment_mode.is_full
1427
- )
1517
+ return self.supports_schema_migration_in_prod and (
1518
+ (self.previous_version and self.previous_version.version == self.version)
1519
+ or self.model.forward_only
1520
+ or bool(self.model.physical_version)
1521
+ or not self.virtual_environment_mode.is_full
1428
1522
  )
1429
1523
 
1430
1524
  @property
@@ -1463,9 +1557,12 @@ class SnapshotTableCleanupTask(PydanticModel):
1463
1557
  dev_table_only: bool
1464
1558
 
1465
1559
 
1466
- SnapshotIdLike = t.Union[SnapshotId, SnapshotTableInfo, Snapshot]
1560
+ SnapshotIdLike = t.Union[SnapshotId, SnapshotIdAndVersion, SnapshotTableInfo, Snapshot]
1561
+ SnapshotIdAndVersionLike = t.Union[SnapshotIdAndVersion, SnapshotTableInfo, Snapshot]
1467
1562
  SnapshotInfoLike = t.Union[SnapshotTableInfo, Snapshot]
1468
- SnapshotNameVersionLike = t.Union[SnapshotNameVersion, SnapshotTableInfo, Snapshot]
1563
+ SnapshotNameVersionLike = t.Union[
1564
+ SnapshotNameVersion, SnapshotTableInfo, SnapshotIdAndVersion, Snapshot
1565
+ ]
1469
1566
 
1470
1567
 
1471
1568
  class DeployabilityIndex(PydanticModel, frozen=True):
@@ -1611,6 +1708,7 @@ class DeployabilityIndex(PydanticModel, frozen=True):
1611
1708
  snapshot.is_valid_start(start, snapshot_start) if start is not None else True
1612
1709
  )
1613
1710
 
1711
+ children_deployable = is_valid_start and not has_auto_restatement
1614
1712
  if (
1615
1713
  snapshot.is_forward_only
1616
1714
  or snapshot.is_indirect_non_breaking
@@ -1627,15 +1725,9 @@ class DeployabilityIndex(PydanticModel, frozen=True):
1627
1725
  ):
1628
1726
  # This snapshot represents what's currently deployed in prod.
1629
1727
  representative_shared_version_ids.add(node)
1630
-
1631
- # A child can still be deployable even if its parent is not
1632
- children_deployable = (
1633
- is_valid_start
1634
- and not (
1635
- snapshot.is_paused and (snapshot.is_forward_only or is_forward_only_model)
1636
- )
1637
- and not has_auto_restatement
1638
- )
1728
+ else:
1729
+ # If the parent is not representative then its children can't be deployable.
1730
+ children_deployable = False
1639
1731
  else:
1640
1732
  children_deployable = False
1641
1733
  if not snapshots[node].is_paused:
@@ -1735,7 +1827,19 @@ def display_name(
1735
1827
  """
1736
1828
  if snapshot_info_like.is_audit:
1737
1829
  return snapshot_info_like.name
1738
- view_name = exp.to_table(snapshot_info_like.name)
1830
+
1831
+ return model_display_name(
1832
+ snapshot_info_like.name, environment_naming_info, default_catalog, dialect
1833
+ )
1834
+
1835
+
1836
+ def model_display_name(
1837
+ node_name: str,
1838
+ environment_naming_info: EnvironmentNamingInfo,
1839
+ default_catalog: t.Optional[str],
1840
+ dialect: DialectType = None,
1841
+ ) -> str:
1842
+ view_name = exp.to_table(node_name)
1739
1843
 
1740
1844
  catalog = (
1741
1845
  None
@@ -1977,16 +2081,20 @@ def missing_intervals(
1977
2081
  continue
1978
2082
  snapshot_end_date = existing_interval_end
1979
2083
 
2084
+ snapshot_start_date = max(
2085
+ to_datetime(snapshot_start_date),
2086
+ to_datetime(start_date(snapshot, snapshots, cache, relative_to=snapshot_end_date)),
2087
+ )
2088
+ if snapshot_start_date > to_datetime(snapshot_end_date):
2089
+ continue
2090
+
1980
2091
  missing_interval_end_date = snapshot_end_date
1981
2092
  node_end_date = snapshot.node.end
1982
2093
  if node_end_date and (to_datetime(node_end_date) < to_datetime(snapshot_end_date)):
1983
2094
  missing_interval_end_date = node_end_date
1984
2095
 
1985
2096
  intervals = snapshot.missing_intervals(
1986
- max(
1987
- to_datetime(snapshot_start_date),
1988
- to_datetime(start_date(snapshot, snapshots, cache, relative_to=snapshot_end_date)),
1989
- ),
2097
+ snapshot_start_date,
1990
2098
  missing_interval_end_date,
1991
2099
  execution_time=execution_time,
1992
2100
  deployability_index=deployability_index,
@@ -2191,14 +2299,16 @@ def start_date(
2191
2299
  if not isinstance(snapshots, dict):
2192
2300
  snapshots = {snapshot.snapshot_id: snapshot for snapshot in snapshots}
2193
2301
 
2194
- earliest = snapshot.node.cron_prev(snapshot.node.cron_floor(relative_to or now()))
2195
-
2196
- for parent in snapshot.parents:
2197
- if parent in snapshots:
2198
- earliest = min(
2199
- earliest,
2200
- start_date(snapshots[parent], snapshots, cache=cache, relative_to=relative_to),
2201
- )
2302
+ parent_starts = [
2303
+ start_date(snapshots[parent], snapshots, cache=cache, relative_to=relative_to)
2304
+ for parent in snapshot.parents
2305
+ if parent in snapshots
2306
+ ]
2307
+ earliest = (
2308
+ min(parent_starts)
2309
+ if parent_starts
2310
+ else snapshot.node.cron_prev(snapshot.node.cron_floor(relative_to or now()))
2311
+ )
2202
2312
 
2203
2313
  cache[key] = earliest
2204
2314
  return earliest
@@ -2346,6 +2456,7 @@ def check_ready_intervals(
2346
2456
  python_env: t.Dict[str, Executable],
2347
2457
  dialect: DialectType = None,
2348
2458
  path: t.Optional[Path] = None,
2459
+ snapshot: t.Optional[Snapshot] = None,
2349
2460
  kwargs: t.Optional[t.Dict] = None,
2350
2461
  ) -> Intervals:
2351
2462
  checked_intervals: Intervals = []
@@ -2361,6 +2472,7 @@ def check_ready_intervals(
2361
2472
  provided_args=(batch,),
2362
2473
  provided_kwargs=(kwargs or {}),
2363
2474
  context=context,
2475
+ snapshot=snapshot,
2364
2476
  )
2365
2477
  except Exception as ex:
2366
2478
  raise SignalEvalError(format_evaluated_code_exception(ex, python_env))