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/basemodel.py CHANGED
@@ -13,6 +13,8 @@ from sqlmesh.core import dialect as d
13
13
  from sqlmesh.core.config.base import UpdateStrategy
14
14
  from sqlmesh.core.config.common import VirtualEnvironmentMode
15
15
  from sqlmesh.core.model import Model
16
+ from sqlmesh.core.model.common import ParsableSql
17
+ from sqlmesh.core.node import DbtNodeInfo
16
18
  from sqlmesh.dbt.column import (
17
19
  ColumnConfig,
18
20
  column_descriptions_to_sqlmesh,
@@ -56,6 +58,12 @@ class Materialization(str, Enum):
56
58
  # Snowflake, https://docs.getdbt.com/reference/resource-configs/snowflake-configs#dynamic-tables
57
59
  DYNAMIC_TABLE = "dynamic_table"
58
60
 
61
+ CUSTOM = "custom"
62
+
63
+ @classmethod
64
+ def _missing_(cls, value): # type: ignore
65
+ return cls.CUSTOM
66
+
59
67
 
60
68
  class SnapshotStrategy(str, Enum):
61
69
  """DBT snapshot strategies"""
@@ -80,7 +88,7 @@ class Hook(DbtConfig):
80
88
  """
81
89
 
82
90
  sql: SqlStr
83
- transaction: bool = True # TODO not yet supported
91
+ transaction: bool = True
84
92
 
85
93
  _sql_validator = sql_str_validator
86
94
 
@@ -120,8 +128,10 @@ class BaseModelConfig(GeneralConfig):
120
128
  grain: t.Union[str, t.List[str]] = []
121
129
 
122
130
  # DBT configuration fields
131
+ unique_id: str = ""
123
132
  name: str = ""
124
133
  package_name: str = ""
134
+ fqn_: t.List[str] = Field(default_factory=list, alias="fqn")
125
135
  schema_: str = Field("", alias="schema")
126
136
  database: t.Optional[str] = None
127
137
  alias: t.Optional[str] = None
@@ -156,7 +166,11 @@ class BaseModelConfig(GeneralConfig):
156
166
 
157
167
  @field_validator("grants", mode="before")
158
168
  @classmethod
159
- def _validate_grants(cls, v: t.Dict[str, str]) -> t.Dict[str, t.List[str]]:
169
+ def _validate_grants(
170
+ cls, v: t.Optional[t.Dict[str, str]]
171
+ ) -> t.Optional[t.Dict[str, t.List[str]]]:
172
+ if v is None:
173
+ return None
160
174
  return {key: ensure_list(value) for key, value in v.items()}
161
175
 
162
176
  _FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = {
@@ -268,44 +282,17 @@ class BaseModelConfig(GeneralConfig):
268
282
  and all(source in context.sources for source in test.dependencies.sources)
269
283
  ]
270
284
 
271
- def fix_circular_test_refs(self, context: DbtContext) -> None:
272
- """
273
- Checks for direct circular references between two models and moves the test to the downstream
274
- model if found. This addresses the most common circular reference - relationship tests in both
275
- directions. In the future, we may want to increase coverage by checking for indirect circular references.
276
-
277
- Args:
278
- context: The dbt context this model resides within.
279
-
280
- Returns:
281
- None
282
- """
283
- for test in self.tests.copy():
284
- for ref in test.dependencies.refs:
285
- if ref == self.name or ref in self.dependencies.refs:
286
- continue
287
- model = context.refs[ref]
288
- if (
289
- self.name in model.dependencies.refs
290
- or self.name in model.tests_ref_source_dependencies.refs
291
- ):
292
- logger.info(
293
- f"Moving test '{test.name}' from model '{self.name}' to '{model.name}' to avoid circular reference."
294
- )
295
- model.tests.append(test)
296
- self.tests.remove(test)
285
+ @property
286
+ def fqn(self) -> str:
287
+ return ".".join(self.fqn_)
297
288
 
298
289
  @property
299
290
  def sqlmesh_config_fields(self) -> t.Set[str]:
300
291
  return {"description", "owner", "stamp", "storage_format"}
301
292
 
302
293
  @property
303
- def node_name(self) -> str:
304
- resource_type = getattr(self, "resource_type", "model")
305
- node_name = f"{resource_type}.{self.package_name}.{self.name}"
306
- if self.version:
307
- node_name += f".v{self.version}"
308
- return node_name
294
+ def node_info(self) -> DbtNodeInfo:
295
+ return DbtNodeInfo(unique_id=self.unique_id, name=self.name, fqn=self.fqn, alias=self.alias)
309
296
 
310
297
  def sqlmesh_model_kwargs(
311
298
  self,
@@ -314,7 +301,6 @@ class BaseModelConfig(GeneralConfig):
314
301
  ) -> t.Dict[str, t.Any]:
315
302
  """Get common sqlmesh model parameters"""
316
303
  self.remove_tests_with_invalid_refs(context)
317
- self.fix_circular_test_refs(context)
318
304
 
319
305
  dependencies = self.dependencies.copy()
320
306
  if dependencies.has_dynamic_var_names:
@@ -322,7 +308,19 @@ class BaseModelConfig(GeneralConfig):
322
308
  # precisely which variables are referenced in the model
323
309
  dependencies.variables |= set(context.variables)
324
310
 
311
+ if (
312
+ getattr(self, "model_materialization", None) == Materialization.CUSTOM
313
+ and hasattr(self, "_get_custom_materialization")
314
+ and (custom_mat := self._get_custom_materialization(context))
315
+ ):
316
+ # include custom materialization dependencies as they might use macros
317
+ dependencies = dependencies.union(custom_mat.dependencies)
318
+
325
319
  model_dialect = self.dialect(context)
320
+
321
+ # Only keep refs and sources that exist in the context to match dbt behavior
322
+ dependencies.refs.intersection_update(context.refs)
323
+ dependencies.sources.intersection_update(context.sources)
326
324
  model_context = context.context_for_dependencies(
327
325
  dependencies.union(self.tests_ref_source_dependencies)
328
326
  )
@@ -332,15 +330,28 @@ class BaseModelConfig(GeneralConfig):
332
330
  jinja_macros.add_globals(self._model_jinja_context(model_context, dependencies))
333
331
 
334
332
  model_kwargs = {
335
- "audits": [(test.name, {}) for test in self.tests],
333
+ "audits": [(test.canonical_name, {}) for test in self.tests],
336
334
  "column_descriptions": column_descriptions_to_sqlmesh(self.columns) or None,
337
335
  "depends_on": {
338
336
  model.canonical_name(context) for model in model_context.refs.values()
339
- }.union({source.canonical_name(context) for source in model_context.sources.values()}),
337
+ }.union(
338
+ {
339
+ source.canonical_name(context)
340
+ for source in model_context.sources.values()
341
+ if source.fqn not in context.model_fqns
342
+ # Allow dbt projects to reference a model as a source without causing a cycle
343
+ },
344
+ ),
340
345
  "jinja_macros": jinja_macros,
341
346
  "path": self.path,
342
- "pre_statements": [d.jinja_statement(hook.sql) for hook in self.pre_hook],
343
- "post_statements": [d.jinja_statement(hook.sql) for hook in self.post_hook],
347
+ "pre_statements": [
348
+ ParsableSql(sql=d.jinja_statement(hook.sql).sql(), transaction=hook.transaction)
349
+ for hook in self.pre_hook
350
+ ],
351
+ "post_statements": [
352
+ ParsableSql(sql=d.jinja_statement(hook.sql).sql(), transaction=hook.transaction)
353
+ for hook in self.post_hook
354
+ ],
344
355
  "tags": self.tags,
345
356
  "physical_schema_mapping": context.sqlmesh_config.physical_schema_mapping,
346
357
  "default_catalog": context.target.database,
@@ -377,8 +388,8 @@ class BaseModelConfig(GeneralConfig):
377
388
  def _model_jinja_context(
378
389
  self, context: DbtContext, dependencies: Dependencies
379
390
  ) -> t.Dict[str, t.Any]:
380
- if context._manifest and self.node_name in context._manifest._manifest.nodes:
381
- attributes = context._manifest._manifest.nodes[self.node_name].to_dict()
391
+ if context._manifest and self.unique_id in context._manifest._manifest.nodes:
392
+ attributes = context._manifest._manifest.nodes[self.unique_id].to_dict()
382
393
  if dependencies.model_attrs.all_attrs:
383
394
  model_node: AttributeDict[str, t.Any] = AttributeDict(attributes)
384
395
  else:
sqlmesh/dbt/builtin.py CHANGED
@@ -25,7 +25,7 @@ from sqlmesh.dbt.target import TARGET_TYPE_TO_CONFIG_CLASS
25
25
  from sqlmesh.dbt.util import DBT_VERSION
26
26
  from sqlmesh.utils import AttributeDict, debug_mode_enabled, yaml
27
27
  from sqlmesh.utils.date import now
28
- from sqlmesh.utils.errors import ConfigError, MacroEvalError
28
+ from sqlmesh.utils.errors import ConfigError
29
29
  from sqlmesh.utils.jinja import JinjaMacroRegistry, MacroReference, MacroReturnVal
30
30
 
31
31
  logger = logging.getLogger(__name__)
@@ -50,6 +50,22 @@ class Exceptions:
50
50
  return ""
51
51
 
52
52
 
53
+ def try_or_compiler_error(
54
+ message_if_exception: str, func: t.Callable, *args: t.Any, **kwargs: t.Any
55
+ ) -> t.Any:
56
+ try:
57
+ return func(*args, **kwargs)
58
+ except Exception:
59
+ if DBT_VERSION >= (1, 4, 0):
60
+ from dbt.exceptions import CompilationError
61
+
62
+ raise CompilationError(message_if_exception)
63
+ else:
64
+ from dbt.exceptions import CompilationException # type: ignore
65
+
66
+ raise CompilationException(message_if_exception)
67
+
68
+
53
69
  class Api:
54
70
  def __init__(self, dialect: t.Optional[str]) -> None:
55
71
  if dialect:
@@ -365,18 +381,16 @@ def do_zip(*args: t.Any, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]
365
381
  return default
366
382
 
367
383
 
368
- def as_bool(value: str) -> bool:
369
- result = _try_literal_eval(value)
370
- if isinstance(result, bool):
371
- return result
372
- raise MacroEvalError(f"Failed to convert '{value}' into boolean.")
384
+ def as_bool(value: t.Any) -> t.Any:
385
+ # dbt's jinja TEXT_FILTERS just return the input value as is
386
+ # https://github.com/dbt-labs/dbt-common/blob/main/dbt_common/clients/jinja.py#L559
387
+ return value
373
388
 
374
389
 
375
390
  def as_number(value: str) -> t.Any:
376
- result = _try_literal_eval(value)
377
- if isinstance(value, (int, float)) and not isinstance(result, bool):
378
- return result
379
- raise MacroEvalError(f"Failed to convert '{value}' into number.")
391
+ # dbt's jinja TEXT_FILTERS just return the input value as is
392
+ # https://github.com/dbt-labs/dbt-common/blob/main/dbt_common/clients/jinja.py#L559
393
+ return value
380
394
 
381
395
 
382
396
  def _try_literal_eval(value: str) -> t.Any:
@@ -411,6 +425,7 @@ BUILTIN_GLOBALS = {
411
425
  "sqlmesh_incremental": True,
412
426
  "tojson": to_json,
413
427
  "toyaml": to_yaml,
428
+ "try_or_compiler_error": try_or_compiler_error,
414
429
  "zip": do_zip,
415
430
  "zip_strict": lambda *args: list(zip(*args)),
416
431
  }
@@ -465,7 +480,7 @@ def create_builtin_globals(
465
480
  if variables is not None:
466
481
  builtin_globals["var"] = Var(variables)
467
482
 
468
- builtin_globals["config"] = Config(jinja_globals.pop("config", {}))
483
+ builtin_globals["config"] = Config(jinja_globals.pop("config", {"tags": []}))
469
484
 
470
485
  deployability_index = (
471
486
  jinja_globals.get("deployability_index") or DeployabilityIndex.all_deployable()
@@ -546,6 +561,7 @@ def create_builtin_globals(
546
561
  "statement": sql_execution.statement,
547
562
  "graph": adapter.graph,
548
563
  "selected_resources": list(jinja_globals.get("selected_models") or []),
564
+ "write": lambda input: None, # We don't support writing yet
549
565
  }
550
566
  )
551
567
 
sqlmesh/dbt/column.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import typing as t
4
+ import logging
4
5
 
5
6
  from sqlglot import exp, parse_one
6
7
  from sqlglot.helper import ensure_list
@@ -9,6 +10,8 @@ from sqlmesh.dbt.common import GeneralConfig
9
10
  from sqlmesh.utils.conversions import ensure_bool
10
11
  from sqlmesh.utils.pydantic import field_validator
11
12
 
13
+ logger = logging.getLogger(__name__)
14
+
12
15
 
13
16
  def yaml_to_columns(
14
17
  yaml: t.Dict[str, ColumnConfig] | t.List[t.Dict[str, ColumnConfig]],
@@ -31,11 +34,20 @@ def column_types_to_sqlmesh(
31
34
  Returns:
32
35
  A dict of column name to exp.DataType
33
36
  """
34
- return {
35
- name: parse_one(column.data_type, into=exp.DataType, dialect=dialect or "")
36
- for name, column in columns.items()
37
- if column.enabled and column.data_type
38
- }
37
+ col_types_to_sqlmesh: t.Dict[str, exp.DataType] = {}
38
+ for name, column in columns.items():
39
+ if column.enabled and column.data_type:
40
+ column_def = parse_one(
41
+ f"{name} {column.data_type}", into=exp.ColumnDef, dialect=dialect or ""
42
+ )
43
+ if column_def.args.get("constraints"):
44
+ logger.warning(
45
+ f"Ignoring unsupported constraints for column '{name}' with definition '{column.data_type}'. Please refer to github.com/TobikoData/sqlmesh/issues/4717 for more information."
46
+ )
47
+ kind = column_def.kind
48
+ if kind:
49
+ col_types_to_sqlmesh[name] = kind
50
+ return col_types_to_sqlmesh
39
51
 
40
52
 
41
53
  def column_descriptions_to_sqlmesh(columns: t.Dict[str, ColumnConfig]) -> t.Dict[str, str]:
sqlmesh/dbt/common.py CHANGED
@@ -46,7 +46,9 @@ def load_yaml(source: str | Path) -> t.Dict:
46
46
  raise ConfigError(f"{source}: {ex}" if isinstance(source, Path) else f"{ex}")
47
47
 
48
48
 
49
- def parse_meta(v: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
49
+ def parse_meta(v: t.Optional[t.Dict[str, t.Any]]) -> t.Dict[str, t.Any]:
50
+ if v is None:
51
+ return {}
50
52
  for key, value in v.items():
51
53
  if isinstance(value, str):
52
54
  v[key] = try_str_to_bool(value)
@@ -115,7 +117,7 @@ class GeneralConfig(DbtConfig):
115
117
 
116
118
  @field_validator("meta", mode="before")
117
119
  @classmethod
118
- def _validate_meta(cls, v: t.Dict[str, t.Union[str, t.Any]]) -> t.Dict[str, t.Any]:
120
+ def _validate_meta(cls, v: t.Optional[t.Dict[str, t.Union[str, t.Any]]]) -> t.Dict[str, t.Any]:
119
121
  return parse_meta(v)
120
122
 
121
123
  _FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = {
sqlmesh/dbt/context.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import typing as t
4
5
  from dataclasses import dataclass, field, replace
5
6
  from pathlib import Path
@@ -28,12 +29,16 @@ if t.TYPE_CHECKING:
28
29
  from sqlmesh.dbt.seed import SeedConfig
29
30
  from sqlmesh.dbt.source import SourceConfig
30
31
 
32
+ logger = logging.getLogger(__name__)
33
+
31
34
 
32
35
  @dataclass
33
36
  class DbtContext:
34
37
  """Context for DBT environment"""
35
38
 
36
39
  project_root: Path = Path()
40
+ profiles_dir: t.Optional[Path] = None
41
+ """Optional override to specify the directory where profiles.yml is located, if not at the :project_root"""
37
42
  target_name: t.Optional[str] = None
38
43
  profile_name: t.Optional[str] = None
39
44
  project_schema: t.Optional[str] = None
@@ -48,6 +53,7 @@ class DbtContext:
48
53
  _project_name: t.Optional[str] = None
49
54
  _variables: t.Dict[str, t.Any] = field(default_factory=dict)
50
55
  _models: t.Dict[str, ModelConfig] = field(default_factory=dict)
56
+ _model_fqns: t.Set[str] = field(default_factory=set)
51
57
  _seeds: t.Dict[str, SeedConfig] = field(default_factory=dict)
52
58
  _sources: t.Dict[str, SourceConfig] = field(default_factory=dict)
53
59
  _refs: t.Dict[str, t.Union[ModelConfig, SeedConfig]] = field(default_factory=dict)
@@ -125,7 +131,7 @@ class DbtContext:
125
131
  try:
126
132
  rendered_variables[k] = _render_var(v)
127
133
  except Exception as ex:
128
- raise ConfigError(f"Failed to render variable '{k}', value '{v}': {ex}") from ex
134
+ logger.warning(f"Failed to render variable '{k}', value '{v}': {ex}")
129
135
 
130
136
  self.variables = rendered_variables
131
137
 
@@ -141,6 +147,7 @@ class DbtContext:
141
147
  def models(self, models: t.Dict[str, ModelConfig]) -> None:
142
148
  self._models = {}
143
149
  self._refs = {}
150
+ self._model_fqns = set()
144
151
  self.add_models(models)
145
152
 
146
153
  def add_models(self, models: t.Dict[str, ModelConfig]) -> None:
@@ -148,6 +155,12 @@ class DbtContext:
148
155
  self._models.update(models)
149
156
  self._jinja_environment = None
150
157
 
158
+ @property
159
+ def model_fqns(self) -> t.Set[str]:
160
+ if not self._model_fqns:
161
+ self._model_fqns = {model.fqn for model in self._models.values()}
162
+ return self._model_fqns
163
+
151
164
  @property
152
165
  def seeds(self) -> t.Dict[str, SeedConfig]:
153
166
  return self._seeds
sqlmesh/dbt/loader.py CHANGED
@@ -5,11 +5,13 @@ import sys
5
5
  import typing as t
6
6
  import sqlmesh.core.dialect as d
7
7
  from pathlib import Path
8
+ from collections import defaultdict
8
9
  from sqlmesh.core.config import (
9
10
  Config,
10
11
  ConnectionConfig,
11
12
  GatewayConfig,
12
13
  ModelDefaultsConfig,
14
+ DbtConfig as RootDbtConfig,
13
15
  )
14
16
  from sqlmesh.core.environment import EnvironmentStatements
15
17
  from sqlmesh.core.loader import CacheBase, LoadedProject, Loader
@@ -48,11 +50,21 @@ def sqlmesh_config(
48
50
  dbt_profile_name: t.Optional[str] = None,
49
51
  dbt_target_name: t.Optional[str] = None,
50
52
  variables: t.Optional[t.Dict[str, t.Any]] = None,
53
+ threads: t.Optional[int] = None,
51
54
  register_comments: t.Optional[bool] = None,
55
+ infer_state_schema_name: bool = False,
56
+ profiles_dir: t.Optional[Path] = None,
52
57
  **kwargs: t.Any,
53
58
  ) -> Config:
54
59
  project_root = project_root or Path()
55
- context = DbtContext(project_root=project_root, profile_name=dbt_profile_name)
60
+ context = DbtContext(
61
+ project_root=project_root, profiles_dir=profiles_dir, profile_name=dbt_profile_name
62
+ )
63
+
64
+ # note: Profile.load() is called twice with different DbtContext's:
65
+ # - once here with the above DbtContext (to determine connnection / gateway config which has to be set up before everything else)
66
+ # - again on the SQLMesh side via GenericContext.load() -> DbtLoader._load_projects() -> Project.load() which constructs a fresh DbtContext and ignores the above one
67
+ # it's important to ensure that the DbtContext created within the DbtLoader uses the same project root / profiles dir that we use here
56
68
  profile = Profile.load(context, target_name=dbt_target_name)
57
69
  model_defaults = kwargs.pop("model_defaults", ModelDefaultsConfig())
58
70
  if model_defaults.dialect is None:
@@ -66,16 +78,45 @@ def sqlmesh_config(
66
78
  if not issubclass(loader, DbtLoader):
67
79
  raise ConfigError("The loader must be a DbtLoader.")
68
80
 
81
+ if threads is not None:
82
+ # the to_sqlmesh() function on TargetConfig maps self.threads -> concurrent_tasks
83
+ profile.target.threads = threads
84
+
85
+ gateway_kwargs = {}
86
+ if infer_state_schema_name:
87
+ profile_name = context.profile_name
88
+
89
+ # Note: we deliberately isolate state based on the target *schema* and not the target name.
90
+ # It is assumed that the project will define a target, eg 'dev', and then in each users own ~/.dbt/profiles.yml the schema
91
+ # for the 'dev' target is overriden to something user-specific, rather than making the target name itself user-specific.
92
+ # This means that the schema name is the indicator of isolated state, not the target name which may be re-used across multiple schemas.
93
+ target_schema = profile.target.schema_
94
+
95
+ # dbt-core doesnt allow schema to be undefined, but it does allow an empty string, and then just
96
+ # fails at runtime when `CREATE SCHEMA ""` doesnt work
97
+ if not target_schema:
98
+ raise ConfigError(
99
+ f"Target '{profile.target_name}' does not specify a schema.\n"
100
+ "A schema is required in order to infer where to store SQLMesh state"
101
+ )
102
+
103
+ inferred_state_schema_name = f"sqlmesh_state_{profile_name}_{target_schema}"
104
+ logger.info("Inferring state schema: %s", inferred_state_schema_name)
105
+ gateway_kwargs["state_schema"] = inferred_state_schema_name
106
+
69
107
  return Config(
70
108
  loader=loader,
109
+ loader_kwargs=dict(profiles_dir=profiles_dir),
71
110
  model_defaults=model_defaults,
72
111
  variables=variables or {},
112
+ dbt=RootDbtConfig(infer_state_schema_name=infer_state_schema_name),
73
113
  **{
74
114
  "default_gateway": profile.target_name if "gateways" not in kwargs else "",
75
115
  "gateways": {
76
116
  profile.target_name: GatewayConfig(
77
117
  connection=profile.target.to_sqlmesh(**target_to_sqlmesh_args),
78
118
  state_connection=state_connection,
119
+ **gateway_kwargs,
79
120
  )
80
121
  }, # type: ignore
81
122
  **kwargs,
@@ -84,9 +125,12 @@ def sqlmesh_config(
84
125
 
85
126
 
86
127
  class DbtLoader(Loader):
87
- def __init__(self, context: GenericContext, path: Path) -> None:
128
+ def __init__(
129
+ self, context: GenericContext, path: Path, profiles_dir: t.Optional[Path] = None
130
+ ) -> None:
88
131
  self._projects: t.List[Project] = []
89
132
  self._macros_max_mtime: t.Optional[float] = None
133
+ self._profiles_dir = profiles_dir
90
134
  super().__init__(context, path)
91
135
 
92
136
  def load(self) -> LoadedProject:
@@ -137,16 +181,22 @@ class DbtLoader(Loader):
137
181
  package_context.set_and_render_variables(package.variables, package.name)
138
182
  package_models: t.Dict[str, BaseModelConfig] = {**package.models, **package.seeds}
139
183
 
184
+ package_models_by_path: t.Dict[Path, t.List[BaseModelConfig]] = defaultdict(list)
140
185
  for model in package_models.values():
141
186
  if isinstance(model, ModelConfig) and not model.sql.strip():
142
187
  logger.info(f"Skipping empty model '{model.name}' at path '{model.path}'.")
143
188
  continue
189
+ package_models_by_path[model.path].append(model)
144
190
 
145
- sqlmesh_model = cache.get_or_load_models(
146
- model.path, loader=lambda: [_to_sqlmesh(model, package_context)]
147
- )[0]
148
-
149
- models[sqlmesh_model.fqn] = sqlmesh_model
191
+ for path, path_models in package_models_by_path.items():
192
+ sqlmesh_models = cache.get_or_load_models(
193
+ path,
194
+ loader=lambda: [
195
+ _to_sqlmesh(model, package_context) for model in path_models
196
+ ],
197
+ )
198
+ for sqlmesh_model in sqlmesh_models:
199
+ models[sqlmesh_model.fqn] = sqlmesh_model
150
200
 
151
201
  models.update(self._load_external_models(audits, cache))
152
202
 
@@ -165,7 +215,8 @@ class DbtLoader(Loader):
165
215
  for test in package.tests.values():
166
216
  logger.debug("Converting '%s' to sqlmesh format", test.name)
167
217
  try:
168
- audits[test.name] = test.to_sqlmesh(package_context)
218
+ audits[test.canonical_name] = test.to_sqlmesh(package_context)
219
+
169
220
  except BaseMissingReferenceError as e:
170
221
  ref_type = "model" if isinstance(e, MissingModelError) else "source"
171
222
  logger.warning(
@@ -186,6 +237,7 @@ class DbtLoader(Loader):
186
237
  project = Project.load(
187
238
  DbtContext(
188
239
  project_root=self.config_path,
240
+ profiles_dir=self._profiles_dir,
189
241
  target_name=target_name,
190
242
  sqlmesh_config=self.config,
191
243
  ),