sqlmesh 0.217.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.
- sqlmesh/__init__.py +12 -2
- sqlmesh/_version.py +2 -2
- sqlmesh/cli/project_init.py +10 -2
- sqlmesh/core/_typing.py +1 -0
- sqlmesh/core/audit/definition.py +8 -2
- sqlmesh/core/config/__init__.py +1 -1
- sqlmesh/core/config/connection.py +17 -5
- sqlmesh/core/config/dbt.py +13 -0
- sqlmesh/core/config/janitor.py +12 -0
- sqlmesh/core/config/loader.py +7 -0
- sqlmesh/core/config/model.py +2 -0
- sqlmesh/core/config/root.py +3 -0
- sqlmesh/core/console.py +80 -2
- sqlmesh/core/constants.py +1 -1
- sqlmesh/core/context.py +61 -25
- sqlmesh/core/dialect.py +3 -0
- sqlmesh/core/engine_adapter/_typing.py +2 -0
- sqlmesh/core/engine_adapter/base.py +322 -22
- sqlmesh/core/engine_adapter/base_postgres.py +17 -1
- sqlmesh/core/engine_adapter/bigquery.py +146 -7
- sqlmesh/core/engine_adapter/clickhouse.py +17 -13
- sqlmesh/core/engine_adapter/databricks.py +33 -2
- sqlmesh/core/engine_adapter/fabric.py +1 -29
- sqlmesh/core/engine_adapter/mixins.py +142 -48
- sqlmesh/core/engine_adapter/mssql.py +15 -4
- sqlmesh/core/engine_adapter/mysql.py +2 -2
- sqlmesh/core/engine_adapter/postgres.py +9 -3
- sqlmesh/core/engine_adapter/redshift.py +4 -0
- sqlmesh/core/engine_adapter/risingwave.py +1 -0
- sqlmesh/core/engine_adapter/shared.py +6 -0
- sqlmesh/core/engine_adapter/snowflake.py +82 -11
- sqlmesh/core/engine_adapter/spark.py +14 -10
- sqlmesh/core/engine_adapter/trino.py +4 -2
- sqlmesh/core/janitor.py +181 -0
- sqlmesh/core/lineage.py +1 -0
- sqlmesh/core/macros.py +35 -13
- sqlmesh/core/model/common.py +2 -0
- sqlmesh/core/model/definition.py +65 -4
- sqlmesh/core/model/kind.py +66 -2
- sqlmesh/core/model/meta.py +107 -2
- sqlmesh/core/node.py +101 -2
- sqlmesh/core/plan/builder.py +15 -10
- sqlmesh/core/plan/common.py +196 -2
- sqlmesh/core/plan/definition.py +21 -6
- sqlmesh/core/plan/evaluator.py +72 -113
- sqlmesh/core/plan/explainer.py +90 -8
- sqlmesh/core/plan/stages.py +42 -21
- sqlmesh/core/renderer.py +26 -18
- sqlmesh/core/scheduler.py +60 -19
- sqlmesh/core/selector.py +137 -9
- sqlmesh/core/signal.py +64 -1
- sqlmesh/core/snapshot/__init__.py +1 -0
- sqlmesh/core/snapshot/definition.py +109 -25
- sqlmesh/core/snapshot/evaluator.py +610 -50
- sqlmesh/core/state_sync/__init__.py +0 -1
- sqlmesh/core/state_sync/base.py +31 -27
- sqlmesh/core/state_sync/cache.py +12 -4
- sqlmesh/core/state_sync/common.py +216 -111
- sqlmesh/core/state_sync/db/facade.py +30 -15
- sqlmesh/core/state_sync/db/interval.py +27 -7
- sqlmesh/core/state_sync/db/migrator.py +14 -8
- sqlmesh/core/state_sync/db/snapshot.py +119 -87
- sqlmesh/core/table_diff.py +2 -2
- sqlmesh/core/test/definition.py +14 -9
- sqlmesh/dbt/adapter.py +20 -11
- sqlmesh/dbt/basemodel.py +52 -41
- sqlmesh/dbt/builtin.py +27 -11
- sqlmesh/dbt/column.py +17 -5
- sqlmesh/dbt/common.py +4 -2
- sqlmesh/dbt/context.py +14 -1
- sqlmesh/dbt/loader.py +60 -8
- sqlmesh/dbt/manifest.py +136 -8
- sqlmesh/dbt/model.py +105 -25
- sqlmesh/dbt/package.py +16 -1
- sqlmesh/dbt/profile.py +3 -3
- sqlmesh/dbt/project.py +12 -7
- sqlmesh/dbt/seed.py +1 -1
- sqlmesh/dbt/source.py +6 -1
- sqlmesh/dbt/target.py +25 -6
- sqlmesh/dbt/test.py +31 -1
- sqlmesh/migrations/v0000_baseline.py +3 -6
- sqlmesh/migrations/v0061_mysql_fix_blob_text_type.py +2 -5
- sqlmesh/migrations/v0062_add_model_gateway.py +2 -2
- sqlmesh/migrations/v0063_change_signals.py +2 -4
- sqlmesh/migrations/v0064_join_when_matched_strings.py +2 -4
- sqlmesh/migrations/v0065_add_model_optimize.py +2 -2
- sqlmesh/migrations/v0066_add_auto_restatements.py +2 -6
- sqlmesh/migrations/v0067_add_tsql_date_full_precision.py +2 -2
- sqlmesh/migrations/v0068_include_unrendered_query_in_metadata_hash.py +2 -2
- sqlmesh/migrations/v0069_update_dev_table_suffix.py +2 -4
- sqlmesh/migrations/v0070_include_grains_in_metadata_hash.py +2 -2
- sqlmesh/migrations/v0071_add_dev_version_to_intervals.py +2 -6
- sqlmesh/migrations/v0072_add_environment_statements.py +2 -4
- sqlmesh/migrations/v0073_remove_symbolic_disable_restatement.py +2 -4
- sqlmesh/migrations/v0074_add_partition_by_time_column_property.py +2 -2
- sqlmesh/migrations/v0075_remove_validate_query.py +2 -4
- sqlmesh/migrations/v0076_add_cron_tz.py +2 -2
- sqlmesh/migrations/v0077_fix_column_type_hash_calculation.py +2 -2
- sqlmesh/migrations/v0078_warn_if_non_migratable_python_env.py +2 -4
- sqlmesh/migrations/v0079_add_gateway_managed_property.py +7 -9
- sqlmesh/migrations/v0080_add_batch_size_to_scd_type_2_models.py +2 -2
- sqlmesh/migrations/v0081_update_partitioned_by.py +2 -4
- sqlmesh/migrations/v0082_warn_if_incorrectly_duplicated_statements.py +2 -4
- sqlmesh/migrations/v0083_use_sql_for_scd_time_data_type_data_hash.py +2 -2
- sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py +2 -2
- sqlmesh/migrations/v0085_deterministic_repr.py +2 -4
- sqlmesh/migrations/v0086_check_deterministic_bug.py +2 -4
- sqlmesh/migrations/v0087_normalize_blueprint_variables.py +2 -4
- sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py +2 -4
- sqlmesh/migrations/v0089_add_virtual_environment_mode.py +2 -2
- sqlmesh/migrations/v0090_add_forward_only_column.py +2 -6
- sqlmesh/migrations/v0091_on_additive_change.py +2 -2
- sqlmesh/migrations/v0092_warn_about_dbt_data_type_diff.py +2 -4
- sqlmesh/migrations/v0093_use_raw_sql_in_fingerprint.py +2 -2
- sqlmesh/migrations/v0094_add_dev_version_and_fingerprint_columns.py +2 -6
- sqlmesh/migrations/v0095_warn_about_dbt_raw_sql_diff.py +2 -4
- sqlmesh/migrations/v0096_remove_plan_dags_table.py +2 -4
- sqlmesh/migrations/v0097_add_dbt_name_in_node.py +2 -2
- sqlmesh/migrations/v0098_add_dbt_node_info_in_node.py +103 -0
- sqlmesh/migrations/v0099_add_last_altered_to_intervals.py +25 -0
- sqlmesh/migrations/v0100_add_grants_and_grants_target_layer.py +9 -0
- sqlmesh/utils/__init__.py +8 -1
- sqlmesh/utils/cache.py +5 -1
- sqlmesh/utils/date.py +1 -1
- sqlmesh/utils/errors.py +4 -0
- sqlmesh/utils/jinja.py +25 -2
- sqlmesh/utils/pydantic.py +6 -6
- sqlmesh/utils/windows.py +13 -3
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/METADATA +5 -5
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/RECORD +181 -176
- sqlmesh_dbt/cli.py +70 -7
- sqlmesh_dbt/console.py +14 -6
- sqlmesh_dbt/operations.py +103 -24
- sqlmesh_dbt/selectors.py +39 -1
- web/client/dist/assets/{Audits-Ucsx1GzF.js → Audits-CBiYyyx-.js} +1 -1
- web/client/dist/assets/{Banner-BWDzvavM.js → Banner-DSRbUlO5.js} +1 -1
- web/client/dist/assets/{ChevronDownIcon-D2VL13Ah.js → ChevronDownIcon-MK_nrjD_.js} +1 -1
- web/client/dist/assets/{ChevronRightIcon-DWGYbf1l.js → ChevronRightIcon-CLWtT22Q.js} +1 -1
- web/client/dist/assets/{Content-DdHDZM3I.js → Content-BNuGZN5l.js} +1 -1
- web/client/dist/assets/{Content-Bikfy8fh.js → Content-CSHJyW0n.js} +1 -1
- web/client/dist/assets/{Data-CzAJH7rW.js → Data-C1oRDbLx.js} +1 -1
- web/client/dist/assets/{DataCatalog-BJF11g8f.js → DataCatalog-HXyX2-_j.js} +1 -1
- web/client/dist/assets/{Editor-s0SBpV2y.js → Editor-BDyfpUuw.js} +1 -1
- web/client/dist/assets/{Editor-DgLhgKnm.js → Editor-D0jNItwC.js} +1 -1
- web/client/dist/assets/{Errors-D0m0O1d3.js → Errors-BfuFLcPi.js} +1 -1
- web/client/dist/assets/{FileExplorer-CEv0vXkt.js → FileExplorer-BR9IE3he.js} +1 -1
- web/client/dist/assets/{Footer-BwzXn8Ew.js → Footer-CgBEtiAh.js} +1 -1
- web/client/dist/assets/{Header-6heDkEqG.js → Header-DSqR6nSO.js} +1 -1
- web/client/dist/assets/{Input-obuJsD6k.js → Input-B-oZ6fGO.js} +1 -1
- web/client/dist/assets/Lineage-DYQVwDbD.js +1 -0
- web/client/dist/assets/{ListboxShow-HM9_qyrt.js → ListboxShow-BE5-xevs.js} +1 -1
- web/client/dist/assets/{ModelLineage-zWdKo0U2.js → ModelLineage-DkIFAYo4.js} +1 -1
- web/client/dist/assets/{Models-Bcu66SRz.js → Models-D5dWr8RB.js} +1 -1
- web/client/dist/assets/{Page-BWEEQfIt.js → Page-C-XfU5BR.js} +1 -1
- web/client/dist/assets/{Plan-C4gXCqlf.js → Plan-ZEuTINBq.js} +1 -1
- web/client/dist/assets/{PlusCircleIcon-CVDO651q.js → PlusCircleIcon-DVXAHG8_.js} +1 -1
- web/client/dist/assets/{ReportErrors-BT6xFwAr.js → ReportErrors-B7FEPzMB.js} +1 -1
- web/client/dist/assets/{Root-ryJoBK4h.js → Root-8aZyhPxF.js} +1 -1
- web/client/dist/assets/{SearchList-DB04sPb9.js → SearchList-W_iT2G82.js} +1 -1
- web/client/dist/assets/{SelectEnvironment-CUYcXUu6.js → SelectEnvironment-C65jALmO.js} +1 -1
- web/client/dist/assets/{SourceList-Doo_9ZGp.js → SourceList-DSLO6nVJ.js} +1 -1
- web/client/dist/assets/{SourceListItem-D5Mj7Dly.js → SourceListItem-BHt8d9-I.js} +1 -1
- web/client/dist/assets/{SplitPane-qHmkD1qy.js → SplitPane-CViaZmw6.js} +1 -1
- web/client/dist/assets/{Tests-DH1Z74ML.js → Tests-DhaVt5t1.js} +1 -1
- web/client/dist/assets/{Welcome-DqUJUNMF.js → Welcome-DvpjH-_4.js} +1 -1
- web/client/dist/assets/context-BctCsyGb.js +71 -0
- web/client/dist/assets/{context-Dr54UHLi.js → context-DFNeGsFF.js} +1 -1
- web/client/dist/assets/{editor-DYIP1yQ4.js → editor-CcO28cqd.js} +1 -1
- web/client/dist/assets/{file-DarlIDVi.js → file-CvJN3aZO.js} +1 -1
- web/client/dist/assets/{floating-ui.react-dom-BH3TFvkM.js → floating-ui.react-dom-CjE-JNW1.js} +1 -1
- web/client/dist/assets/{help-Bl8wqaQc.js → help-DuPhjipa.js} +1 -1
- web/client/dist/assets/{index-D1sR7wpN.js → index-C-dJH7yZ.js} +1 -1
- web/client/dist/assets/{index-O3mjYpnE.js → index-Dj0i1-CA.js} +2 -2
- web/client/dist/assets/{plan-CehRrJUG.js → plan-BTRSbjKn.js} +1 -1
- web/client/dist/assets/{popover-CqgMRE0G.js → popover-_Sf0yvOI.js} +1 -1
- web/client/dist/assets/{project-6gxepOhm.js → project-BvSOI8MY.js} +1 -1
- web/client/dist/index.html +1 -1
- web/client/dist/assets/Lineage-D0Hgdz2v.js +0 -1
- web/client/dist/assets/context-DgX0fp2E.js +0 -68
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/WHEEL +0 -0
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev4.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev4.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
|
|
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(
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
304
|
-
|
|
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.
|
|
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(
|
|
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": [
|
|
343
|
-
|
|
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.
|
|
381
|
-
attributes = context._manifest._manifest.nodes[self.
|
|
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
|
|
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:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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(
|
|
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__(
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
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
|
),
|