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
sqlmesh/dbt/manifest.py CHANGED
@@ -11,7 +11,7 @@ from collections import defaultdict
11
11
  from functools import cached_property
12
12
  from pathlib import Path
13
13
 
14
- from dbt import constants as dbt_constants, flags
14
+ from dbt import flags
15
15
 
16
16
  from sqlmesh.dbt.util import DBT_VERSION
17
17
  from sqlmesh.utils.conversions import make_serializable
@@ -19,6 +19,8 @@ from sqlmesh.utils.conversions import make_serializable
19
19
  # Override the file name to prevent dbt commands from invalidating the cache.
20
20
 
21
21
  if DBT_VERSION >= (1, 6, 0):
22
+ from dbt import constants as dbt_constants
23
+
22
24
  dbt_constants.PARTIAL_PARSE_FILE_NAME = "sqlmesh_partial_parse.msgpack" # type: ignore
23
25
  else:
24
26
  from dbt.parser import manifest as dbt_manifest # type: ignore
@@ -47,7 +49,7 @@ from sqlmesh.core.config import ModelDefaultsConfig
47
49
  from sqlmesh.dbt.builtin import BUILTIN_FILTERS, BUILTIN_GLOBALS, OVERRIDDEN_MACROS
48
50
  from sqlmesh.dbt.common import Dependencies
49
51
  from sqlmesh.dbt.model import ModelConfig
50
- from sqlmesh.dbt.package import HookConfig, MacroConfig
52
+ from sqlmesh.dbt.package import HookConfig, MacroConfig, MaterializationConfig
51
53
  from sqlmesh.dbt.seed import SeedConfig
52
54
  from sqlmesh.dbt.source import SourceConfig
53
55
  from sqlmesh.dbt.target import TargetConfig
@@ -61,6 +63,7 @@ from sqlmesh.utils.jinja import (
61
63
  extract_call_names,
62
64
  jinja_call_arg_name,
63
65
  )
66
+ from sqlglot.helper import ensure_list
64
67
 
65
68
  if t.TYPE_CHECKING:
66
69
  from dbt.contracts.graph.manifest import Macro, Manifest
@@ -75,6 +78,7 @@ SeedConfigs = t.Dict[str, SeedConfig]
75
78
  SourceConfigs = t.Dict[str, SourceConfig]
76
79
  MacroConfigs = t.Dict[str, MacroConfig]
77
80
  HookConfigs = t.Dict[str, HookConfig]
81
+ MaterializationConfigs = t.Dict[str, MaterializationConfig]
78
82
 
79
83
 
80
84
  IGNORED_PACKAGES = {"elementary"}
@@ -135,6 +139,7 @@ class ManifestHelper:
135
139
 
136
140
  self._on_run_start_per_package: t.Dict[str, HookConfigs] = defaultdict(dict)
137
141
  self._on_run_end_per_package: t.Dict[str, HookConfigs] = defaultdict(dict)
142
+ self._materializations: MaterializationConfigs = {}
138
143
 
139
144
  def tests(self, package_name: t.Optional[str] = None) -> TestConfigs:
140
145
  self._load_all()
@@ -164,6 +169,10 @@ class ManifestHelper:
164
169
  self._load_all()
165
170
  return self._on_run_end_per_package[package_name or self._project_name]
166
171
 
172
+ def materializations(self) -> MaterializationConfigs:
173
+ self._load_all()
174
+ return self._materializations
175
+
167
176
  @property
168
177
  def all_macros(self) -> t.Dict[str, t.Dict[str, MacroInfo]]:
169
178
  self._load_all()
@@ -213,6 +222,7 @@ class ManifestHelper:
213
222
  self._calls = {k: (v, False) for k, v in (self._call_cache.get("") or {}).items()}
214
223
 
215
224
  self._load_macros()
225
+ self._load_materializations()
216
226
  self._load_sources()
217
227
  self._load_tests()
218
228
  self._load_models_and_seeds()
@@ -250,11 +260,14 @@ class ManifestHelper:
250
260
 
251
261
  def _load_macros(self) -> None:
252
262
  for macro in self._manifest.macros.values():
263
+ if macro.name.startswith("materialization_"):
264
+ continue
265
+
253
266
  if macro.name.startswith("test_"):
254
267
  macro.macro_sql = _convert_jinja_test_to_macro(macro.macro_sql)
255
268
 
256
269
  dependencies = Dependencies(macros=_macro_references(self._manifest, macro))
257
- if not macro.name.startswith("materialization_") and not macro.name.startswith("test_"):
270
+ if not macro.name.startswith("test_"):
258
271
  dependencies = dependencies.union(
259
272
  self._extra_dependencies(macro.macro_sql, macro.package_name)
260
273
  )
@@ -281,6 +294,32 @@ class ManifestHelper:
281
294
  if pos > 0 and name[pos + 2 :] in adapter_macro_names:
282
295
  macro_config.info.is_top_level = True
283
296
 
297
+ def _load_materializations(self) -> None:
298
+ for macro in self._manifest.macros.values():
299
+ if macro.name.startswith("materialization_"):
300
+ # Extract name and adapter ( "materialization_{name}_{adapter}" or "materialization_{name}_default")
301
+ name_parts = macro.name.split("_")
302
+ if len(name_parts) >= 3:
303
+ mat_name = "_".join(name_parts[1:-1])
304
+ adapter = name_parts[-1]
305
+
306
+ dependencies = Dependencies(macros=_macro_references(self._manifest, macro))
307
+ macro.macro_sql = _strip_jinja_materialization_tags(macro.macro_sql)
308
+ dependencies = dependencies.union(
309
+ self._extra_dependencies(macro.macro_sql, macro.package_name)
310
+ )
311
+
312
+ materialization_config = MaterializationConfig(
313
+ name=mat_name,
314
+ adapter=adapter,
315
+ definition=macro.macro_sql,
316
+ dependencies=dependencies,
317
+ path=Path(macro.original_file_path),
318
+ )
319
+
320
+ key = f"{mat_name}_{adapter}"
321
+ self._materializations[key] = materialization_config
322
+
284
323
  def _load_tests(self) -> None:
285
324
  for node in self._manifest.nodes.values():
286
325
  if node.resource_type != "test":
@@ -317,15 +356,17 @@ class ManifestHelper:
317
356
  )
318
357
 
319
358
  test_model = _test_model(node)
359
+ node_config = _node_base_config(node)
360
+ node_config["name"] = _build_test_name(node, dependencies)
320
361
 
321
362
  test = TestConfig(
322
363
  sql=sql,
323
364
  model_name=test_model,
324
365
  test_kwargs=node.test_metadata.kwargs if hasattr(node, "test_metadata") else {},
325
366
  dependencies=dependencies,
326
- **_node_base_config(node),
367
+ **node_config,
327
368
  )
328
- self._tests_per_package[node.package_name][node.name.lower()] = test
369
+ self._tests_per_package[node.package_name][node.unique_id] = test
329
370
  if test_model:
330
371
  self._tests_by_owner[test_model].append(test)
331
372
 
@@ -338,10 +379,12 @@ class ManifestHelper:
338
379
  continue
339
380
 
340
381
  macro_references = _macro_references(self._manifest, node)
341
- tests = (
382
+ all_tests = (
342
383
  self._tests_by_owner[node.name]
343
384
  + self._tests_by_owner[f"{node.package_name}.{node.name}"]
344
385
  )
386
+ # Only include non-standalone tests (tests that don't reference other models)
387
+ tests = [test for test in all_tests if not test.is_standalone]
345
388
  node_config = _node_base_config(node)
346
389
 
347
390
  node_name = node.name
@@ -357,6 +400,12 @@ class ManifestHelper:
357
400
  dependencies = dependencies.union(
358
401
  self._extra_dependencies(sql, node.package_name, track_all_model_attrs=True)
359
402
  )
403
+ for hook in [*node_config.get("pre-hook", []), *node_config.get("post-hook", [])]:
404
+ dependencies = dependencies.union(
405
+ self._extra_dependencies(
406
+ hook["sql"], node.package_name, track_all_model_attrs=True
407
+ )
408
+ )
360
409
  dependencies = dependencies.union(
361
410
  self._flatten_dependencies_from_macros(dependencies.macros, node.package_name)
362
411
  )
@@ -659,7 +708,7 @@ def _macro_references(
659
708
  return result
660
709
 
661
710
  for macro_node_id in node.depends_on.macros:
662
- if not macro_node_id:
711
+ if not macro_node_id or macro_node_id == "None":
663
712
  continue
664
713
 
665
714
  macro_node = manifest.macros[macro_node_id]
@@ -697,7 +746,12 @@ def _test_model(node: ManifestNode) -> t.Optional[str]:
697
746
  attached_node = getattr(node, "attached_node", None)
698
747
  if attached_node:
699
748
  pieces = attached_node.split(".")
700
- return pieces[-1] if pieces[0] in ["model", "seed"] else None
749
+ if pieces[0] in ["model", "seed"]:
750
+ # versioned models have format "model.package.model_name.v1" (4 parts)
751
+ if len(pieces) == 4:
752
+ return f"{pieces[2]}_{pieces[3]}"
753
+ return pieces[-1]
754
+ return None
701
755
 
702
756
  key_name = getattr(node, "file_key_name", None)
703
757
  if key_name:
@@ -730,3 +784,77 @@ def _convert_jinja_test_to_macro(test_jinja: str) -> str:
730
784
  macro = macro_tag + test_jinja[match.span()[-1] :]
731
785
 
732
786
  return re.sub(ENDTEST_REGEX, lambda m: m.group(0).replace("endtest", "endmacro"), macro)
787
+
788
+
789
+ def _strip_jinja_materialization_tags(materialization_jinja: str) -> str:
790
+ MATERIALIZATION_TAG_REGEX = r"\s*{%-?\s*materialization\s+[^%]*%}\s*\n?"
791
+ ENDMATERIALIZATION_REGEX = r"{%-?\s*endmaterialization\s*-?%}\s*\n?"
792
+
793
+ if not re.match(MATERIALIZATION_TAG_REGEX, materialization_jinja):
794
+ return materialization_jinja
795
+
796
+ materialization_jinja = re.sub(
797
+ MATERIALIZATION_TAG_REGEX,
798
+ "",
799
+ materialization_jinja,
800
+ flags=re.IGNORECASE,
801
+ )
802
+
803
+ materialization_jinja = re.sub(
804
+ ENDMATERIALIZATION_REGEX,
805
+ "",
806
+ materialization_jinja,
807
+ flags=re.IGNORECASE,
808
+ )
809
+
810
+ return materialization_jinja.strip()
811
+
812
+
813
+ def _build_test_name(node: ManifestNode, dependencies: Dependencies) -> str:
814
+ """
815
+ Build a user-friendly test name that includes the test's model/source, column,
816
+ and args for tests with custom user names. Needed because dbt only generates these
817
+ names for tests that do not specify the "name" field in their YAML definition.
818
+
819
+ Name structure
820
+ - Model test: [namespace]_[test name]_[model name]_[column name]__[arg values]
821
+ - Source test: [namespace]_source_[test name]_[source name]_[table name]_[column name]__[arg values]
822
+ """
823
+ # standalone test
824
+ if not hasattr(node, "test_metadata"):
825
+ return node.name
826
+
827
+ model_name = _test_model(node)
828
+ source_name = None
829
+ if not model_name and dependencies.sources:
830
+ # extract source and table names
831
+ source_parts = list(dependencies.sources)[0].split(".")
832
+ source_name = "_".join(source_parts) if len(source_parts) == 2 else source_parts[-1]
833
+ entity_name = model_name or source_name or ""
834
+ entity_name = f"_{entity_name}" if entity_name else ""
835
+
836
+ name_prefix = ""
837
+ if namespace := getattr(node.test_metadata, "namespace", None):
838
+ name_prefix += f"{namespace}_"
839
+ if source_name and not model_name:
840
+ name_prefix += "source_"
841
+
842
+ metadata_kwargs = node.test_metadata.kwargs
843
+ arg_val_parts = []
844
+ for arg, val in sorted(metadata_kwargs.items()):
845
+ if arg == "model":
846
+ continue
847
+ if isinstance(val, dict):
848
+ val = list(val.values())
849
+ val = [re.sub("[^0-9a-zA-Z_]+", "_", str(v)) for v in ensure_list(val)]
850
+ arg_val_parts.extend(val)
851
+ unique_args = "__".join(arg_val_parts) if arg_val_parts else ""
852
+ unique_args = f"_{unique_args}" if unique_args else ""
853
+
854
+ auto_name = f"{name_prefix}{node.test_metadata.name}{entity_name}{unique_args}"
855
+
856
+ if node.name == auto_name:
857
+ return node.name
858
+
859
+ custom_prefix = name_prefix if source_name and not model_name else ""
860
+ return f"{custom_prefix}{node.name}{entity_name}{unique_args}"
sqlmesh/dbt/model.py CHANGED
@@ -31,6 +31,7 @@ from sqlmesh.core.model.kind import (
31
31
  OnAdditiveChange,
32
32
  on_destructive_change_validator,
33
33
  on_additive_change_validator,
34
+ DbtCustomKind,
34
35
  )
35
36
  from sqlmesh.dbt.basemodel import BaseModelConfig, Materialization, SnapshotStrategy
36
37
  from sqlmesh.dbt.common import SqlStr, sql_str_validator
@@ -40,6 +41,7 @@ from sqlmesh.utils.pydantic import field_validator
40
41
  if t.TYPE_CHECKING:
41
42
  from sqlmesh.core.audit.definition import ModelAudit
42
43
  from sqlmesh.dbt.context import DbtContext
44
+ from sqlmesh.dbt.package import MaterializationConfig
43
45
 
44
46
  logger = logging.getLogger(__name__)
45
47
 
@@ -170,6 +172,22 @@ class ModelConfig(BaseModelConfig):
170
172
  return "*"
171
173
  return ensure_list(v)
172
174
 
175
+ @field_validator("updated_at", mode="before")
176
+ @classmethod
177
+ def _validate_updated_at(cls, v: t.Optional[str]) -> t.Optional[str]:
178
+ """
179
+ Extract column name if updated_at contains a cast.
180
+
181
+ SCDType2ByTimeKind and SCDType2ByColumnKind expect a column, and the casting is done later.
182
+ """
183
+ if v is None:
184
+ return None
185
+ parsed = d.parse_one(v)
186
+ if isinstance(parsed, exp.Cast) and isinstance(parsed.this, exp.Column):
187
+ return parsed.this.name
188
+
189
+ return v
190
+
173
191
  @field_validator("sql", mode="before")
174
192
  @classmethod
175
193
  def _validate_sql(cls, v: t.Union[str, SqlStr]) -> SqlStr:
@@ -197,6 +215,14 @@ class ModelConfig(BaseModelConfig):
197
215
  ):
198
216
  granularity = v["granularity"]
199
217
  raise ConfigError(f"Unexpected granularity '{granularity}' in partition_by '{v}'.")
218
+ if "data_type" in v and v["data_type"].lower() not in (
219
+ "timestamp",
220
+ "date",
221
+ "datetime",
222
+ "int64",
223
+ ):
224
+ data_type = v["data_type"]
225
+ raise ConfigError(f"Unexpected data_type '{data_type}' in partition_by '{v}'.")
200
226
  return {"data_type": "date", "granularity": "day", **v}
201
227
  raise ConfigError(f"Invalid format for partition_by '{v}'")
202
228
 
@@ -444,6 +470,19 @@ class ModelConfig(BaseModelConfig):
444
470
  if materialization == Materialization.DYNAMIC_TABLE:
445
471
  return ManagedKind()
446
472
 
473
+ if materialization == Materialization.CUSTOM:
474
+ if custom_materialization := self._get_custom_materialization(context):
475
+ return DbtCustomKind(
476
+ materialization=self.materialized,
477
+ adapter=custom_materialization.adapter,
478
+ dialect=self.dialect(context),
479
+ definition=custom_materialization.definition,
480
+ )
481
+
482
+ raise ConfigError(
483
+ f"Unknown materialization '{self.materialized}'. Custom materializations must be defined in your dbt project."
484
+ )
485
+
447
486
  raise ConfigError(f"{materialization.value} materialization not supported.")
448
487
 
449
488
  def _big_query_partition_by_expr(self, context: DbtContext) -> exp.Expression:
@@ -483,6 +522,18 @@ class ModelConfig(BaseModelConfig):
483
522
  dialect="bigquery",
484
523
  )
485
524
 
525
+ def _get_custom_materialization(self, context: DbtContext) -> t.Optional[MaterializationConfig]:
526
+ materializations = context.manifest.materializations()
527
+ name, target_adapter = self.materialized, context.target.dialect
528
+
529
+ adapter_specific_key = f"{name}_{target_adapter}"
530
+ default_key = f"{name}_default"
531
+ if adapter_specific_key in materializations:
532
+ return materializations[adapter_specific_key]
533
+ if default_key in materializations:
534
+ return materializations[default_key]
535
+ return None
536
+
486
537
  @property
487
538
  def sqlmesh_config_fields(self) -> t.Set[str]:
488
539
  return super().sqlmesh_config_fields | {
@@ -510,39 +561,59 @@ class ModelConfig(BaseModelConfig):
510
561
  physical_properties: t.Dict[str, t.Any] = {}
511
562
 
512
563
  if self.partition_by:
513
- partitioned_by = []
514
- if isinstance(self.partition_by, list):
515
- for p in self.partition_by:
516
- try:
517
- partitioned_by.append(d.parse_one(p, dialect=model_dialect))
518
- except SqlglotError as e:
519
- raise ConfigError(
520
- f"Failed to parse model '{self.canonical_name(context)}' partition_by field '{p}' in '{self.path}': {e}"
521
- ) from e
522
- elif isinstance(self.partition_by, dict):
523
- if context.target.dialect == "bigquery":
524
- partitioned_by.append(self._big_query_partition_by_expr(context))
525
- else:
526
- logger.warning(
527
- "Ignoring partition_by config for model '%s' targeting %s. The format of the config field is only supported for BigQuery.",
528
- self.name,
529
- context.target.dialect,
530
- )
564
+ if isinstance(kind, (ViewKind, EmbeddedKind)):
565
+ logger.warning(
566
+ "Ignoring partition_by config for model '%s'; partition_by is not supported for %s.",
567
+ self.name,
568
+ "views" if isinstance(kind, ViewKind) else "ephemeral models",
569
+ )
570
+ elif context.target.dialect == "snowflake":
571
+ logger.warning(
572
+ "Ignoring partition_by config for model '%s' targeting %s. The partition_by config is not supported for Snowflake.",
573
+ self.name,
574
+ context.target.dialect,
575
+ )
576
+ else:
577
+ partitioned_by = []
578
+ if isinstance(self.partition_by, list):
579
+ for p in self.partition_by:
580
+ try:
581
+ partitioned_by.append(d.parse_one(p, dialect=model_dialect))
582
+ except SqlglotError as e:
583
+ raise ConfigError(
584
+ f"Failed to parse model '{self.canonical_name(context)}' partition_by field '{p}' in '{self.path}': {e}"
585
+ ) from e
586
+ elif isinstance(self.partition_by, dict):
587
+ if context.target.dialect == "bigquery":
588
+ partitioned_by.append(self._big_query_partition_by_expr(context))
589
+ else:
590
+ logger.warning(
591
+ "Ignoring partition_by config for model '%s' targeting %s. The format of the config field is only supported for BigQuery.",
592
+ self.name,
593
+ context.target.dialect,
594
+ )
531
595
 
532
- if partitioned_by:
533
- optional_kwargs["partitioned_by"] = partitioned_by
596
+ if partitioned_by:
597
+ optional_kwargs["partitioned_by"] = partitioned_by
534
598
 
535
599
  if self.cluster_by:
536
- if isinstance(kind, ViewKind):
600
+ if isinstance(kind, (ViewKind, EmbeddedKind)):
537
601
  logger.warning(
538
- "Ignoring cluster_by config for model '%s'; cluster_by is not supported for views.",
602
+ "Ignoring cluster_by config for model '%s'; cluster_by is not supported for %s.",
539
603
  self.name,
604
+ "views" if isinstance(kind, ViewKind) else "ephemeral models",
540
605
  )
541
606
  else:
542
607
  clustered_by = []
543
608
  for c in self.cluster_by:
544
609
  try:
545
- clustered_by.append(d.parse_one(c, dialect=model_dialect))
610
+ cluster_expr = exp.maybe_parse(
611
+ c, into=exp.Cluster, prefix="CLUSTER BY", dialect=model_dialect
612
+ )
613
+ for expr in cluster_expr.expressions:
614
+ clustered_by.append(
615
+ expr.this if isinstance(expr, exp.Ordered) else expr
616
+ )
546
617
  except SqlglotError as e:
547
618
  raise ConfigError(
548
619
  f"Failed to parse model '{self.canonical_name(context)}' cluster_by field '{c}' in '{self.path}': {e}"
@@ -644,11 +715,20 @@ class ModelConfig(BaseModelConfig):
644
715
  if physical_properties:
645
716
  model_kwargs["physical_properties"] = physical_properties
646
717
 
718
+ kind = self.model_kind(context)
719
+
720
+ # A falsy grants config (None or {}) is considered as unmanaged per dbt semantics
721
+ if self.grants and kind.supports_grants:
722
+ model_kwargs["grants"] = self.grants
723
+
647
724
  allow_partials = model_kwargs.pop("allow_partials", None)
648
725
  if allow_partials is None:
649
726
  # Set allow_partials to True for dbt models to preserve the original semantics.
650
727
  allow_partials = True
651
728
 
729
+ # pop begin for all models so we don't pass it through for non-incremental materializations
730
+ # (happens if model config is microbatch but project config overrides)
731
+ begin = model_kwargs.pop("begin", None)
652
732
  if kind.is_incremental:
653
733
  if self.batch_size and isinstance(self.batch_size, str):
654
734
  if "interval_unit" in model_kwargs:
@@ -658,7 +738,7 @@ class ModelConfig(BaseModelConfig):
658
738
  else:
659
739
  model_kwargs["interval_unit"] = self.batch_size
660
740
  self.batch_size = None
661
- if begin := model_kwargs.pop("begin", None):
741
+ if begin:
662
742
  if "start" in model_kwargs:
663
743
  get_console().log_warning(
664
744
  f"Both 'begin' and 'start' are set for model '{self.canonical_name(context)}'. 'start' will be used."
@@ -687,7 +767,7 @@ class ModelConfig(BaseModelConfig):
687
767
  extract_dependencies_from_query=False,
688
768
  allow_partials=allow_partials,
689
769
  virtual_environment_mode=virtual_environment_mode,
690
- dbt_name=self.node_name,
770
+ dbt_node_info=self.node_info,
691
771
  **optional_kwargs,
692
772
  **model_kwargs,
693
773
  )
sqlmesh/dbt/package.py CHANGED
@@ -37,6 +37,16 @@ class HookConfig(PydanticModel):
37
37
  dependencies: Dependencies
38
38
 
39
39
 
40
+ class MaterializationConfig(PydanticModel):
41
+ """Class to contain custom materialization configuration."""
42
+
43
+ name: str
44
+ adapter: str
45
+ definition: str
46
+ dependencies: Dependencies
47
+ path: Path
48
+
49
+
40
50
  class Package(PydanticModel):
41
51
  """Class to contain package configuration"""
42
52
 
@@ -47,6 +57,7 @@ class Package(PydanticModel):
47
57
  models: t.Dict[str, ModelConfig]
48
58
  variables: t.Dict[str, t.Any]
49
59
  macros: t.Dict[str, MacroConfig]
60
+ materializations: t.Dict[str, MaterializationConfig]
50
61
  on_run_start: t.Dict[str, HookConfig]
51
62
  on_run_end: t.Dict[str, HookConfig]
52
63
  files: t.Set[Path]
@@ -94,6 +105,7 @@ class PackageLoader:
94
105
  models = _fix_paths(self._context.manifest.models(package_name), package_root)
95
106
  seeds = _fix_paths(self._context.manifest.seeds(package_name), package_root)
96
107
  macros = _fix_paths(self._context.manifest.macros(package_name), package_root)
108
+ materializations = _fix_paths(self._context.manifest.materializations(), package_root)
97
109
  on_run_start = _fix_paths(self._context.manifest.on_run_start(package_name), package_root)
98
110
  on_run_end = _fix_paths(self._context.manifest.on_run_end(package_name), package_root)
99
111
  sources = self._context.manifest.sources(package_name)
@@ -114,13 +126,16 @@ class PackageLoader:
114
126
  seeds=seeds,
115
127
  variables=package_variables,
116
128
  macros=macros,
129
+ materializations=materializations,
117
130
  files=config_paths,
118
131
  on_run_start=on_run_start,
119
132
  on_run_end=on_run_end,
120
133
  )
121
134
 
122
135
 
123
- T = t.TypeVar("T", TestConfig, ModelConfig, MacroConfig, SeedConfig, HookConfig)
136
+ T = t.TypeVar(
137
+ "T", TestConfig, ModelConfig, MacroConfig, MaterializationConfig, SeedConfig, HookConfig
138
+ )
124
139
 
125
140
 
126
141
  def _fix_paths(configs: t.Dict[str, T], package_root: Path) -> t.Dict[str, T]:
sqlmesh/dbt/profile.py CHANGED
@@ -60,7 +60,7 @@ class Profile:
60
60
  if not context.profile_name:
61
61
  raise ConfigError(f"{project_file.stem} must include project name.")
62
62
 
63
- profile_filepath = cls._find_profile(context.project_root)
63
+ profile_filepath = cls._find_profile(context.project_root, context.profiles_dir)
64
64
  if not profile_filepath:
65
65
  raise ConfigError(f"{cls.PROFILE_FILE} not found.")
66
66
 
@@ -68,8 +68,8 @@ class Profile:
68
68
  return Profile(profile_filepath, target_name, target)
69
69
 
70
70
  @classmethod
71
- def _find_profile(cls, project_root: Path) -> t.Optional[Path]:
72
- dir = os.environ.get("DBT_PROFILES_DIR", "")
71
+ def _find_profile(cls, project_root: Path, profiles_dir: t.Optional[Path]) -> t.Optional[Path]:
72
+ dir = os.environ.get("DBT_PROFILES_DIR", profiles_dir or "")
73
73
  path = Path(project_root, dir, cls.PROFILE_FILE)
74
74
  if path.exists():
75
75
  return path
sqlmesh/dbt/project.py CHANGED
@@ -99,16 +99,21 @@ class Project:
99
99
  package = package_loader.load(path.parent)
100
100
  packages[package.name] = package
101
101
 
102
+ # Variable resolution precedence:
103
+ # 1. Variable overrides
104
+ # 2. Package-scoped variables in the root project's dbt_project.yml
105
+ # 3. Global project variables in the root project's dbt_project.yml
106
+ # 4. Variables in the package's dbt_project.yml
102
107
  all_project_variables = {**(project_yaml.get("vars") or {}), **(variable_overrides or {})}
103
108
  for name, package in packages.items():
104
- package_vars = all_project_variables.get(name)
105
-
106
- if isinstance(package_vars, dict):
107
- package.variables.update(package_vars)
108
-
109
- if name == context.project_name:
110
- package.variables.update(all_project_variables)
109
+ if isinstance(all_project_variables.get(name), dict):
110
+ project_vars_copy = all_project_variables.copy()
111
+ package_scoped_vars = project_vars_copy.pop(name)
112
+ package.variables.update(project_vars_copy)
113
+ package.variables.update(package_scoped_vars)
111
114
  else:
115
+ package.variables.update(all_project_variables)
116
+ if variable_overrides:
112
117
  package.variables.update(variable_overrides)
113
118
 
114
119
  return Project(context, profile, packages)
sqlmesh/dbt/seed.py CHANGED
@@ -92,7 +92,7 @@ class SeedConfig(BaseModelConfig):
92
92
  audit_definitions=audit_definitions,
93
93
  virtual_environment_mode=virtual_environment_mode,
94
94
  start=self.start or context.sqlmesh_config.model_defaults.start,
95
- dbt_name=self.node_name,
95
+ dbt_node_info=self.node_info,
96
96
  **kwargs,
97
97
  )
98
98
 
sqlmesh/dbt/source.py CHANGED
@@ -36,6 +36,7 @@ class SourceConfig(GeneralConfig):
36
36
  # DBT configuration fields
37
37
  name: str = ""
38
38
  source_name_: str = Field("", alias="source_name")
39
+ fqn_: t.List[str] = Field(default_factory=list, alias="fqn")
39
40
  database: t.Optional[str] = None
40
41
  schema_: t.Optional[str] = Field(None, alias="schema")
41
42
  identifier: t.Optional[str] = None
@@ -64,6 +65,10 @@ class SourceConfig(GeneralConfig):
64
65
  def config_name(self) -> str:
65
66
  return f"{self.source_name_}.{self.name}"
66
67
 
68
+ @property
69
+ def fqn(self) -> str:
70
+ return ".".join(self.fqn_)
71
+
67
72
  def canonical_name(self, context: DbtContext) -> str:
68
73
  if self._canonical_name is None:
69
74
  source = context.get_callable_macro("source")
@@ -74,7 +79,7 @@ class SourceConfig(GeneralConfig):
74
79
  relation = source(self.source_name_, self.name)
75
80
  except Exception as e:
76
81
  raise ConfigError(
77
- f"'source' macro failed for '{self.config_name}' with exeception '{e}'."
82
+ f"'source' macro failed for '{self.config_name}' with exception '{e}'."
78
83
  )
79
84
 
80
85
  relation = relation.quote(
sqlmesh/dbt/target.py CHANGED
@@ -45,10 +45,24 @@ IncrementalKind = t.Union[
45
45
 
46
46
  # We only serialize a subset of fields in order to avoid persisting sensitive information
47
47
  SERIALIZABLE_FIELDS = {
48
- "type",
48
+ # core
49
49
  "name",
50
- "database",
51
50
  "schema_",
51
+ "type",
52
+ "threads",
53
+ # snowflake
54
+ "database",
55
+ "warehouse",
56
+ "user",
57
+ "role",
58
+ "account",
59
+ # postgres/redshift
60
+ "dbname",
61
+ "host",
62
+ "port",
63
+ # bigquery
64
+ "project",
65
+ "dataset",
52
66
  }
53
67
 
54
68
  SCHEMA_DIFFER_OVERRIDES = {
@@ -587,12 +601,17 @@ class BigQueryConfig(TargetConfig):
587
601
  if not isinstance(data, dict):
588
602
  return data
589
603
 
590
- data["schema"] = data.get("schema") or data.get("dataset")
591
- if not data["schema"]:
604
+ # dbt treats schema and dataset interchangeably
605
+ schema = data.get("schema") or data.get("dataset")
606
+ if not schema:
592
607
  raise ConfigError("Either schema or dataset must be set")
593
- data["database"] = data.get("database") or data.get("project")
594
- if not data["database"]:
608
+ data["dataset"] = data["schema"] = schema
609
+
610
+ # dbt treats database and project interchangeably
611
+ database = data.get("database") or data.get("project")
612
+ if not database:
595
613
  raise ConfigError("Either database or project must be set")
614
+ data["database"] = data["project"] = database
596
615
 
597
616
  return data
598
617