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
@@ -6,7 +6,6 @@ import logging
6
6
  from pathlib import Path
7
7
  from collections import defaultdict
8
8
  from sqlglot import exp
9
- from pydantic import Field
10
9
 
11
10
  from sqlmesh.core.engine_adapter import EngineAdapter
12
11
  from sqlmesh.core.state_sync.db.utils import (
@@ -15,9 +14,7 @@ from sqlmesh.core.state_sync.db.utils import (
15
14
  snapshot_id_filter,
16
15
  fetchone,
17
16
  fetchall,
18
- create_batches,
19
17
  )
20
- from sqlmesh.core.node import IntervalUnit
21
18
  from sqlmesh.core.environment import Environment
22
19
  from sqlmesh.core.model import SeedModel, ModelKindName
23
20
  from sqlmesh.core.snapshot.cache import SnapshotCache
@@ -28,13 +25,18 @@ from sqlmesh.core.snapshot import (
28
25
  SnapshotNameVersion,
29
26
  SnapshotInfoLike,
30
27
  Snapshot,
28
+ SnapshotIdAndVersion,
31
29
  SnapshotId,
32
30
  SnapshotFingerprint,
33
- SnapshotChangeCategory,
31
+ )
32
+ from sqlmesh.core.state_sync.common import (
33
+ RowBoundary,
34
+ ExpiredSnapshotBatch,
35
+ ExpiredBatchRange,
36
+ LimitBoundary,
34
37
  )
35
38
  from sqlmesh.utils.migration import index_text_type, blob_text_type
36
39
  from sqlmesh.utils.date import now_timestamp, TimeLike, to_timestamp
37
- from sqlmesh.utils.pydantic import PydanticModel
38
40
  from sqlmesh.utils import unique
39
41
 
40
42
  if t.TYPE_CHECKING:
@@ -63,6 +65,7 @@ class SnapshotState:
63
65
  "name": exp.DataType.build(index_type),
64
66
  "identifier": exp.DataType.build(index_type),
65
67
  "version": exp.DataType.build(index_type),
68
+ "dev_version": exp.DataType.build(index_type),
66
69
  "snapshot": exp.DataType.build(blob_type),
67
70
  "kind_name": exp.DataType.build("text"),
68
71
  "updated_ts": exp.DataType.build("bigint"),
@@ -70,6 +73,7 @@ class SnapshotState:
70
73
  "ttl_ms": exp.DataType.build("bigint"),
71
74
  "unrestorable": exp.DataType.build("boolean"),
72
75
  "forward_only": exp.DataType.build("boolean"),
76
+ "fingerprint": exp.DataType.build(blob_type),
73
77
  }
74
78
 
75
79
  self._auto_restatement_columns_to_types = {
@@ -164,94 +168,136 @@ class SnapshotState:
164
168
  self,
165
169
  environments: t.Iterable[Environment],
166
170
  current_ts: int,
167
- ignore_ttl: bool = False,
168
- ) -> t.List[SnapshotTableCleanupTask]:
169
- """Aggregates the id's of the expired snapshots and creates a list of table cleanup tasks.
170
-
171
- Expired snapshots are snapshots that have exceeded their time-to-live
172
- and are no longer in use within an environment.
173
-
174
- Returns:
175
- The set of expired snapshot ids.
176
- The list of table cleanup tasks.
177
- """
178
- _, cleanup_targets = self._get_expired_snapshots(
179
- environments=environments,
180
- current_ts=current_ts,
181
- ignore_ttl=ignore_ttl,
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
182
176
  )
183
- return cleanup_targets
184
-
185
- def _get_expired_snapshots(
186
- self,
187
- environments: t.Iterable[Environment],
188
- current_ts: int,
189
- ignore_ttl: bool = False,
190
- ) -> t.Tuple[t.Set[SnapshotId], t.List[SnapshotTableCleanupTask]]:
191
- expired_query = exp.select("name", "identifier", "version").from_(self.snapshots_table)
192
177
 
193
178
  if not ignore_ttl:
194
179
  expired_query = expired_query.where(
195
180
  (exp.column("updated_ts") + exp.column("ttl_ms")) <= current_ts
196
181
  )
197
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
+
198
219
  expired_candidates = {
199
220
  SnapshotId(name=name, identifier=identifier): SnapshotNameVersion(
200
221
  name=name, version=version
201
222
  )
202
- for name, identifier, version in fetchall(self.engine_adapter, expired_query)
223
+ for name, identifier, version, _ in rows
203
224
  }
204
225
  if not expired_candidates:
205
- return set(), []
226
+ return None
206
227
 
207
- promoted_snapshot_ids = {
208
- snapshot.snapshot_id
209
- for environment in environments
210
- for snapshot in environment.snapshots
211
- }
212
-
213
- def _is_snapshot_used(snapshot: SharedVersionSnapshot) -> bool:
228
+ def _is_snapshot_used(snapshot: SnapshotIdAndVersion) -> bool:
214
229
  return (
215
230
  snapshot.snapshot_id in promoted_snapshot_ids
216
231
  or snapshot.snapshot_id not in expired_candidates
217
232
  )
218
233
 
219
- unique_expired_versions = unique(expired_candidates.values())
220
- version_batches = create_batches(
221
- unique_expired_versions, batch_size=self.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],
222
240
  )
223
- cleanup_targets = []
224
- expired_snapshot_ids = set()
225
- for versions_batch in version_batches:
226
- snapshots = self._get_snapshots_with_same_version(versions_batch)
227
-
228
- snapshots_by_version = defaultdict(set)
229
- snapshots_by_dev_version = defaultdict(set)
230
- for s in snapshots:
231
- snapshots_by_version[(s.name, s.version)].add(s.snapshot_id)
232
- snapshots_by_dev_version[(s.name, s.dev_version)].add(s.snapshot_id)
233
-
234
- expired_snapshots = [s for s in snapshots if not _is_snapshot_used(s)]
235
- expired_snapshot_ids.update([s.snapshot_id for s in expired_snapshots])
236
-
237
- for snapshot in expired_snapshots:
238
- shared_version_snapshots = snapshots_by_version[(snapshot.name, snapshot.version)]
239
- shared_version_snapshots.discard(snapshot.snapshot_id)
240
-
241
- shared_dev_version_snapshots = snapshots_by_dev_version[
242
- (snapshot.name, snapshot.dev_version)
243
- ]
244
- shared_dev_version_snapshots.discard(snapshot.snapshot_id)
245
-
246
- if not shared_dev_version_snapshots:
247
- cleanup_targets.append(
248
- SnapshotTableCleanupTask(
249
- snapshot=snapshot.full_snapshot.table_info,
250
- dev_table_only=bool(shared_version_snapshots),
251
- )
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(
281
+ SnapshotTableCleanupTask(
282
+ snapshot=full_snapshots[snapshot_id].table_info,
283
+ dev_table_only=dev_table_only,
252
284
  )
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
+ )
253
299
 
254
- return expired_snapshot_ids, cleanup_targets
300
+ return None
255
301
 
256
302
  def delete_snapshots(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> None:
257
303
  """Deletes snapshots.
@@ -288,6 +334,55 @@ class SnapshotState:
288
334
  """
289
335
  return self._get_snapshots(snapshot_ids)
290
336
 
337
+ def get_snapshots_by_names(
338
+ self,
339
+ snapshot_names: t.Iterable[str],
340
+ current_ts: t.Optional[int] = None,
341
+ exclude_expired: bool = True,
342
+ ) -> t.Set[SnapshotIdAndVersion]:
343
+ """Return the snapshot records for all versions of the specified snapshot names.
344
+
345
+ Args:
346
+ snapshot_names: Iterable of snapshot names to fetch all snapshot records for
347
+ current_ts: Sets the current time for identifying which snapshots have expired so they can be excluded (only relevant if :exclude_expired=True)
348
+ exclude_expired: Whether or not to return the snapshot id's of expired snapshots in the result
349
+
350
+ Returns:
351
+ A set containing all the matched snapshot records. To fetch full snapshots, pass it into StateSync.get_snapshots()
352
+ """
353
+ if not snapshot_names:
354
+ return set()
355
+
356
+ if exclude_expired:
357
+ current_ts = current_ts or now_timestamp()
358
+ unexpired_expr = (exp.column("updated_ts") + exp.column("ttl_ms")) > current_ts
359
+ else:
360
+ unexpired_expr = None
361
+
362
+ return {
363
+ SnapshotIdAndVersion(
364
+ name=name,
365
+ identifier=identifier,
366
+ version=version,
367
+ kind_name=kind_name or None,
368
+ dev_version=dev_version,
369
+ fingerprint=fingerprint,
370
+ )
371
+ for where in snapshot_name_filter(
372
+ snapshot_names=snapshot_names,
373
+ batch_size=self.SNAPSHOT_BATCH_SIZE,
374
+ )
375
+ for name, identifier, version, kind_name, dev_version, fingerprint in fetchall(
376
+ self.engine_adapter,
377
+ exp.select(
378
+ "name", "identifier", "version", "kind_name", "dev_version", "fingerprint"
379
+ )
380
+ .from_(self.snapshots_table)
381
+ .where(where)
382
+ .and_(unexpired_expr),
383
+ )
384
+ }
385
+
291
386
  def snapshots_exist(self, snapshot_ids: t.Iterable[SnapshotIdLike]) -> t.Set[SnapshotId]:
292
387
  """Checks if snapshots exist.
293
388
 
@@ -571,7 +666,7 @@ class SnapshotState:
571
666
  self,
572
667
  snapshots: t.Collection[SnapshotNameVersionLike],
573
668
  lock_for_update: bool = False,
574
- ) -> t.List[SharedVersionSnapshot]:
669
+ ) -> t.List[SnapshotIdAndVersion]:
575
670
  """Fetches all snapshots that share the same version as the snapshots.
576
671
 
577
672
  The output includes the snapshots with the specified identifiers.
@@ -593,14 +688,12 @@ class SnapshotState:
593
688
  ):
594
689
  query = (
595
690
  exp.select(
596
- "snapshot",
597
691
  "name",
598
692
  "identifier",
599
693
  "version",
600
- "updated_ts",
601
- "unpaused_ts",
602
- "unrestorable",
603
- "forward_only",
694
+ "kind_name",
695
+ "dev_version",
696
+ "fingerprint",
604
697
  )
605
698
  .from_(exp.to_table(self.snapshots_table).as_("snapshots"))
606
699
  .where(where)
@@ -611,17 +704,15 @@ class SnapshotState:
611
704
  snapshot_rows.extend(fetchall(self.engine_adapter, query))
612
705
 
613
706
  return [
614
- SharedVersionSnapshot.from_snapshot_record(
707
+ SnapshotIdAndVersion(
615
708
  name=name,
616
709
  identifier=identifier,
617
710
  version=version,
618
- updated_ts=updated_ts,
619
- unpaused_ts=unpaused_ts,
620
- unrestorable=unrestorable,
621
- forward_only=forward_only,
622
- snapshot=snapshot,
711
+ kind_name=kind_name or None,
712
+ dev_version=dev_version,
713
+ fingerprint=SnapshotFingerprint.parse_raw(fingerprint),
623
714
  )
624
- for snapshot, name, identifier, version, updated_ts, unpaused_ts, unrestorable, forward_only in snapshot_rows
715
+ for name, identifier, version, kind_name, dev_version, fingerprint in snapshot_rows
625
716
  ]
626
717
 
627
718
 
@@ -676,6 +767,8 @@ def _snapshots_to_df(snapshots: t.Iterable[Snapshot]) -> pd.DataFrame:
676
767
  "ttl_ms": snapshot.ttl_ms,
677
768
  "unrestorable": snapshot.unrestorable,
678
769
  "forward_only": snapshot.forward_only,
770
+ "dev_version": snapshot.dev_version,
771
+ "fingerprint": snapshot.fingerprint.json(),
679
772
  }
680
773
  for snapshot in snapshots
681
774
  ]
@@ -695,88 +788,3 @@ def _auto_restatements_to_df(auto_restatements: t.Dict[SnapshotNameVersion, int]
695
788
  for name_version, ts in auto_restatements.items()
696
789
  ]
697
790
  )
698
-
699
-
700
- class SharedVersionSnapshot(PydanticModel):
701
- """A stripped down version of a snapshot that is used for fetching snapshots that share the same version
702
- with a significantly reduced parsing overhead.
703
- """
704
-
705
- name: str
706
- version: str
707
- dev_version_: t.Optional[str] = Field(alias="dev_version")
708
- identifier: str
709
- fingerprint: SnapshotFingerprint
710
- interval_unit: IntervalUnit
711
- change_category: SnapshotChangeCategory
712
- updated_ts: int
713
- unpaused_ts: t.Optional[int]
714
- unrestorable: bool
715
- disable_restatement: bool
716
- effective_from: t.Optional[TimeLike]
717
- raw_snapshot: t.Dict[str, t.Any]
718
- forward_only: bool
719
-
720
- @property
721
- def snapshot_id(self) -> SnapshotId:
722
- return SnapshotId(name=self.name, identifier=self.identifier)
723
-
724
- @property
725
- def is_forward_only(self) -> bool:
726
- return self.forward_only or self.change_category == SnapshotChangeCategory.FORWARD_ONLY
727
-
728
- @property
729
- def normalized_effective_from_ts(self) -> t.Optional[int]:
730
- return (
731
- to_timestamp(self.interval_unit.cron_floor(self.effective_from))
732
- if self.effective_from
733
- else None
734
- )
735
-
736
- @property
737
- def dev_version(self) -> str:
738
- return self.dev_version_ or self.fingerprint.to_version()
739
-
740
- @property
741
- def full_snapshot(self) -> Snapshot:
742
- return Snapshot(
743
- **{
744
- **self.raw_snapshot,
745
- "updated_ts": self.updated_ts,
746
- "unpaused_ts": self.unpaused_ts,
747
- "unrestorable": self.unrestorable,
748
- "forward_only": self.forward_only,
749
- }
750
- )
751
-
752
- @classmethod
753
- def from_snapshot_record(
754
- cls,
755
- *,
756
- name: str,
757
- identifier: str,
758
- version: str,
759
- updated_ts: int,
760
- unpaused_ts: t.Optional[int],
761
- unrestorable: bool,
762
- forward_only: bool,
763
- snapshot: str,
764
- ) -> SharedVersionSnapshot:
765
- raw_snapshot = json.loads(snapshot)
766
- raw_node = raw_snapshot["node"]
767
- return SharedVersionSnapshot(
768
- name=name,
769
- version=version,
770
- dev_version=raw_snapshot.get("dev_version"),
771
- identifier=identifier,
772
- fingerprint=raw_snapshot["fingerprint"],
773
- interval_unit=raw_node.get("interval_unit", IntervalUnit.from_cron(raw_node["cron"])),
774
- change_category=raw_snapshot["change_category"],
775
- updated_ts=updated_ts,
776
- unpaused_ts=unpaused_ts,
777
- unrestorable=unrestorable,
778
- disable_restatement=raw_node.get("kind", {}).get("disable_restatement", False),
779
- effective_from=raw_snapshot.get("effective_from"),
780
- raw_snapshot=raw_snapshot,
781
- forward_only=forward_only,
782
- )
@@ -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
 
@@ -18,6 +18,8 @@ class TestExecutionContext(ExecutionContext):
18
18
  models: All upstream models to use for expansion and mapping of physical locations.
19
19
  """
20
20
 
21
+ __test__ = False # prevent pytest trying to collect this as a test class
22
+
21
23
  def __init__(
22
24
  self,
23
25
  engine_adapter: EngineAdapter,
@@ -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")
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 ""
@@ -168,7 +177,8 @@ class BaseAdapter(abc.ABC):
168
177
 
169
178
  @property
170
179
  def graph(self) -> t.Any:
171
- return AttributeDict(
180
+ flat_graph = self.jinja_globals.get("flat_graph", None)
181
+ return flat_graph or AttributeDict(
172
182
  {
173
183
  "exposures": {},
174
184
  "groups": {},
@@ -276,10 +286,6 @@ class RuntimeAdapter(BaseAdapter):
276
286
  **table_mapping,
277
287
  }
278
288
 
279
- @property
280
- def graph(self) -> t.Any:
281
- return self.jinja_globals.get("flat_graph", super().graph)
282
-
283
289
  def get_relation(
284
290
  self, database: t.Optional[str], schema: str, identifier: str
285
291
  ) -> t.Optional[BaseRelation]: