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.
- 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 +20 -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 +112 -35
- sqlmesh/core/dialect.py +3 -0
- sqlmesh/core/engine_adapter/_typing.py +2 -0
- sqlmesh/core/engine_adapter/base.py +330 -23
- 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 +50 -2
- sqlmesh/core/engine_adapter/fabric.py +110 -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 +5 -2
- sqlmesh/core/janitor.py +181 -0
- sqlmesh/core/lineage.py +1 -0
- sqlmesh/core/linter/rules/builtin.py +15 -0
- sqlmesh/core/loader.py +17 -30
- sqlmesh/core/macros.py +35 -13
- sqlmesh/core/model/common.py +2 -0
- sqlmesh/core/model/definition.py +72 -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/core/test/discovery.py +4 -0
- 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/integrations/github/cicd/controller.py +6 -2
- sqlmesh/lsp/context.py +4 -2
- sqlmesh/magics.py +1 -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/git.py +3 -1
- 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.dev20.dist-info}/METADATA +5 -5
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/RECORD +188 -183
- 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.dev20.dist-info}/WHEEL +0 -0
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.217.1.dev1.dist-info → sqlmesh-0.227.2.dev20.dist-info}/top_level.txt +0 -0
sqlmesh/core/model/kind.py
CHANGED
|
@@ -23,7 +23,7 @@ from sqlmesh.utils.pydantic import (
|
|
|
23
23
|
PydanticModel,
|
|
24
24
|
SQLGlotBool,
|
|
25
25
|
SQLGlotColumn,
|
|
26
|
-
|
|
26
|
+
SQLGlotListOfFieldsOrStar,
|
|
27
27
|
SQLGlotListOfFields,
|
|
28
28
|
SQLGlotPositiveInt,
|
|
29
29
|
SQLGlotString,
|
|
@@ -119,6 +119,10 @@ class ModelKindMixin:
|
|
|
119
119
|
def is_managed(self) -> bool:
|
|
120
120
|
return self.model_kind_name == ModelKindName.MANAGED
|
|
121
121
|
|
|
122
|
+
@property
|
|
123
|
+
def is_dbt_custom(self) -> bool:
|
|
124
|
+
return self.model_kind_name == ModelKindName.DBT_CUSTOM
|
|
125
|
+
|
|
122
126
|
@property
|
|
123
127
|
def is_symbolic(self) -> bool:
|
|
124
128
|
"""A symbolic model is one that doesn't execute at all."""
|
|
@@ -150,6 +154,11 @@ class ModelKindMixin:
|
|
|
150
154
|
def supports_python_models(self) -> bool:
|
|
151
155
|
return True
|
|
152
156
|
|
|
157
|
+
@property
|
|
158
|
+
def supports_grants(self) -> bool:
|
|
159
|
+
"""Whether this model kind supports grants configuration."""
|
|
160
|
+
return self.is_materialized or self.is_view
|
|
161
|
+
|
|
153
162
|
|
|
154
163
|
class ModelKindName(str, ModelKindMixin, Enum):
|
|
155
164
|
"""The kind of model, determining how this data is computed and stored in the warehouse."""
|
|
@@ -170,6 +179,7 @@ class ModelKindName(str, ModelKindMixin, Enum):
|
|
|
170
179
|
EXTERNAL = "EXTERNAL"
|
|
171
180
|
CUSTOM = "CUSTOM"
|
|
172
181
|
MANAGED = "MANAGED"
|
|
182
|
+
DBT_CUSTOM = "DBT_CUSTOM"
|
|
173
183
|
|
|
174
184
|
@property
|
|
175
185
|
def model_kind_name(self) -> t.Optional[ModelKindName]:
|
|
@@ -842,7 +852,7 @@ class SCDType2ByTimeKind(_SCDType2Kind):
|
|
|
842
852
|
|
|
843
853
|
class SCDType2ByColumnKind(_SCDType2Kind):
|
|
844
854
|
name: t.Literal[ModelKindName.SCD_TYPE_2_BY_COLUMN] = ModelKindName.SCD_TYPE_2_BY_COLUMN
|
|
845
|
-
columns:
|
|
855
|
+
columns: SQLGlotListOfFieldsOrStar
|
|
846
856
|
execution_time_as_valid_from: SQLGlotBool = False
|
|
847
857
|
updated_at_name: t.Optional[SQLGlotColumn] = None
|
|
848
858
|
|
|
@@ -887,6 +897,46 @@ class ManagedKind(_ModelKind):
|
|
|
887
897
|
return False
|
|
888
898
|
|
|
889
899
|
|
|
900
|
+
class DbtCustomKind(_ModelKind):
|
|
901
|
+
name: t.Literal[ModelKindName.DBT_CUSTOM] = ModelKindName.DBT_CUSTOM
|
|
902
|
+
materialization: str
|
|
903
|
+
adapter: str = "default"
|
|
904
|
+
definition: str
|
|
905
|
+
dialect: t.Optional[str] = Field(None, validate_default=True)
|
|
906
|
+
|
|
907
|
+
_dialect_validator = kind_dialect_validator
|
|
908
|
+
|
|
909
|
+
@field_validator("materialization", "adapter", "definition", mode="before")
|
|
910
|
+
@classmethod
|
|
911
|
+
def _validate_fields(cls, v: t.Any) -> str:
|
|
912
|
+
return validate_string(v)
|
|
913
|
+
|
|
914
|
+
@property
|
|
915
|
+
def data_hash_values(self) -> t.List[t.Optional[str]]:
|
|
916
|
+
return [
|
|
917
|
+
*super().data_hash_values,
|
|
918
|
+
self.materialization,
|
|
919
|
+
self.definition,
|
|
920
|
+
self.adapter,
|
|
921
|
+
self.dialect,
|
|
922
|
+
]
|
|
923
|
+
|
|
924
|
+
def to_expression(
|
|
925
|
+
self, expressions: t.Optional[t.List[exp.Expression]] = None, **kwargs: t.Any
|
|
926
|
+
) -> d.ModelKind:
|
|
927
|
+
return super().to_expression(
|
|
928
|
+
expressions=[
|
|
929
|
+
*(expressions or []),
|
|
930
|
+
*_properties(
|
|
931
|
+
{
|
|
932
|
+
"materialization": exp.Literal.string(self.materialization),
|
|
933
|
+
"adapter": exp.Literal.string(self.adapter),
|
|
934
|
+
}
|
|
935
|
+
),
|
|
936
|
+
],
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
|
|
890
940
|
class EmbeddedKind(_ModelKind):
|
|
891
941
|
name: t.Literal[ModelKindName.EMBEDDED] = ModelKindName.EMBEDDED
|
|
892
942
|
|
|
@@ -992,6 +1042,7 @@ ModelKind = t.Annotated[
|
|
|
992
1042
|
SCDType2ByColumnKind,
|
|
993
1043
|
CustomKind,
|
|
994
1044
|
ManagedKind,
|
|
1045
|
+
DbtCustomKind,
|
|
995
1046
|
],
|
|
996
1047
|
Field(discriminator="name"),
|
|
997
1048
|
]
|
|
@@ -1011,6 +1062,7 @@ MODEL_KIND_NAME_TO_TYPE: t.Dict[str, t.Type[ModelKind]] = {
|
|
|
1011
1062
|
ModelKindName.SCD_TYPE_2_BY_COLUMN: SCDType2ByColumnKind,
|
|
1012
1063
|
ModelKindName.CUSTOM: CustomKind,
|
|
1013
1064
|
ModelKindName.MANAGED: ManagedKind,
|
|
1065
|
+
ModelKindName.DBT_CUSTOM: DbtCustomKind,
|
|
1014
1066
|
}
|
|
1015
1067
|
|
|
1016
1068
|
|
|
@@ -1053,6 +1105,18 @@ def create_model_kind(v: t.Any, dialect: str, defaults: t.Dict[str, t.Any]) -> M
|
|
|
1053
1105
|
):
|
|
1054
1106
|
props[on_change_property] = defaults.get(on_change_property)
|
|
1055
1107
|
|
|
1108
|
+
# only pass the batch_concurrency user default to models inheriting from _IncrementalBy
|
|
1109
|
+
# that don't explicitly set it in the model definition, but ignore subclasses of _IncrementalBy
|
|
1110
|
+
# that hardcode a specific batch_concurrency
|
|
1111
|
+
if issubclass(kind_type, _IncrementalBy):
|
|
1112
|
+
BATCH_CONCURRENCY: t.Final = "batch_concurrency"
|
|
1113
|
+
if (
|
|
1114
|
+
props.get(BATCH_CONCURRENCY) is None
|
|
1115
|
+
and defaults.get(BATCH_CONCURRENCY) is not None
|
|
1116
|
+
and kind_type.all_field_infos()[BATCH_CONCURRENCY].default is None
|
|
1117
|
+
):
|
|
1118
|
+
props[BATCH_CONCURRENCY] = defaults.get(BATCH_CONCURRENCY)
|
|
1119
|
+
|
|
1056
1120
|
if kind_type == CustomKind:
|
|
1057
1121
|
# load the custom materialization class and check if it uses a custom kind type
|
|
1058
1122
|
from sqlmesh.core.snapshot.evaluator import get_custom_materialization_type
|
sqlmesh/core/model/meta.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import typing as t
|
|
4
|
+
from enum import Enum
|
|
4
5
|
from functools import cached_property
|
|
5
6
|
from typing_extensions import Self
|
|
6
7
|
|
|
@@ -13,6 +14,7 @@ from sqlmesh.core import dialect as d
|
|
|
13
14
|
from sqlmesh.core.config.common import VirtualEnvironmentMode
|
|
14
15
|
from sqlmesh.core.config.linter import LinterConfig
|
|
15
16
|
from sqlmesh.core.dialect import normalize_model_name
|
|
17
|
+
from sqlmesh.utils import classproperty
|
|
16
18
|
from sqlmesh.core.model.common import (
|
|
17
19
|
bool_validator,
|
|
18
20
|
default_catalog_validator,
|
|
@@ -46,10 +48,41 @@ from sqlmesh.utils.pydantic import (
|
|
|
46
48
|
|
|
47
49
|
if t.TYPE_CHECKING:
|
|
48
50
|
from sqlmesh.core._typing import CustomMaterializationProperties, SessionProperties
|
|
51
|
+
from sqlmesh.core.engine_adapter._typing import GrantsConfig
|
|
49
52
|
|
|
50
53
|
FunctionCall = t.Tuple[str, t.Dict[str, exp.Expression]]
|
|
51
54
|
|
|
52
55
|
|
|
56
|
+
class GrantsTargetLayer(str, Enum):
|
|
57
|
+
"""Target layer(s) where grants should be applied."""
|
|
58
|
+
|
|
59
|
+
ALL = "all"
|
|
60
|
+
PHYSICAL = "physical"
|
|
61
|
+
VIRTUAL = "virtual"
|
|
62
|
+
|
|
63
|
+
@classproperty
|
|
64
|
+
def default(cls) -> "GrantsTargetLayer":
|
|
65
|
+
return GrantsTargetLayer.VIRTUAL
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def is_all(self) -> bool:
|
|
69
|
+
return self == GrantsTargetLayer.ALL
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def is_physical(self) -> bool:
|
|
73
|
+
return self == GrantsTargetLayer.PHYSICAL
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def is_virtual(self) -> bool:
|
|
77
|
+
return self == GrantsTargetLayer.VIRTUAL
|
|
78
|
+
|
|
79
|
+
def __str__(self) -> str:
|
|
80
|
+
return self.name
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
return str(self)
|
|
84
|
+
|
|
85
|
+
|
|
53
86
|
class ModelMeta(_Node):
|
|
54
87
|
"""Metadata for models which can be defined in SQL."""
|
|
55
88
|
|
|
@@ -85,6 +118,8 @@ class ModelMeta(_Node):
|
|
|
85
118
|
)
|
|
86
119
|
formatting: t.Optional[bool] = Field(default=None, exclude=True)
|
|
87
120
|
virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default
|
|
121
|
+
grants_: t.Optional[exp.Tuple] = Field(default=None, alias="grants")
|
|
122
|
+
grants_target_layer: GrantsTargetLayer = GrantsTargetLayer.default
|
|
88
123
|
|
|
89
124
|
_bool_validator = bool_validator
|
|
90
125
|
_model_kind_validator = model_kind_validator
|
|
@@ -247,11 +282,15 @@ class ModelMeta(_Node):
|
|
|
247
282
|
|
|
248
283
|
columns_to_types = info.data.get("columns_to_types_")
|
|
249
284
|
if columns_to_types:
|
|
250
|
-
|
|
285
|
+
from sqlmesh.core.console import get_console
|
|
286
|
+
|
|
287
|
+
console = get_console()
|
|
288
|
+
for column_name in list(col_descriptions):
|
|
251
289
|
if column_name not in columns_to_types:
|
|
252
|
-
|
|
290
|
+
console.log_warning(
|
|
253
291
|
f"In model '{info.data['name']}', a description is provided for column '{column_name}' but it is not a column in the model."
|
|
254
292
|
)
|
|
293
|
+
del col_descriptions[column_name]
|
|
255
294
|
|
|
256
295
|
return col_descriptions
|
|
257
296
|
|
|
@@ -283,6 +322,14 @@ class ModelMeta(_Node):
|
|
|
283
322
|
def ignored_rules_validator(cls, vs: t.Any) -> t.Any:
|
|
284
323
|
return LinterConfig._validate_rules(vs)
|
|
285
324
|
|
|
325
|
+
@field_validator("grants_target_layer", mode="before")
|
|
326
|
+
def _grants_target_layer_validator(cls, v: t.Any) -> t.Any:
|
|
327
|
+
if isinstance(v, exp.Identifier):
|
|
328
|
+
return v.this
|
|
329
|
+
if isinstance(v, exp.Literal) and v.is_string:
|
|
330
|
+
return v.this
|
|
331
|
+
return v
|
|
332
|
+
|
|
286
333
|
@field_validator("session_properties_", mode="before")
|
|
287
334
|
def session_properties_validator(cls, v: t.Any, info: ValidationInfo) -> t.Any:
|
|
288
335
|
# use the generic properties validator to parse the session properties
|
|
@@ -390,6 +437,10 @@ class ModelMeta(_Node):
|
|
|
390
437
|
f"Model {self.name} has `storage_format` set to a table format '{storage_format}' which is deprecated. Please use the `table_format` property instead."
|
|
391
438
|
)
|
|
392
439
|
|
|
440
|
+
# Validate grants configuration for model kind support
|
|
441
|
+
if self.grants is not None and not kind.supports_grants:
|
|
442
|
+
raise ValueError(f"grants cannot be set for {kind.name} models")
|
|
443
|
+
|
|
393
444
|
return self
|
|
394
445
|
|
|
395
446
|
@property
|
|
@@ -461,6 +512,30 @@ class ModelMeta(_Node):
|
|
|
461
512
|
return self.kind.materialization_properties
|
|
462
513
|
return {}
|
|
463
514
|
|
|
515
|
+
@cached_property
|
|
516
|
+
def grants(self) -> t.Optional[GrantsConfig]:
|
|
517
|
+
"""A dictionary of grants mapping permission names to lists of grantees."""
|
|
518
|
+
|
|
519
|
+
if self.grants_ is None:
|
|
520
|
+
return None
|
|
521
|
+
|
|
522
|
+
if not self.grants_.expressions:
|
|
523
|
+
return {}
|
|
524
|
+
|
|
525
|
+
grants_dict = {}
|
|
526
|
+
for eq_expr in self.grants_.expressions:
|
|
527
|
+
try:
|
|
528
|
+
permission_name = self._validate_config_expression(eq_expr.left)
|
|
529
|
+
grantee_list = self._validate_nested_config_values(eq_expr.expression)
|
|
530
|
+
grants_dict[permission_name] = grantee_list
|
|
531
|
+
except ConfigError as e:
|
|
532
|
+
permission_name = (
|
|
533
|
+
eq_expr.left.name if hasattr(eq_expr.left, "name") else str(eq_expr.left)
|
|
534
|
+
)
|
|
535
|
+
raise ConfigError(f"Invalid grants configuration for '{permission_name}': {e}")
|
|
536
|
+
|
|
537
|
+
return grants_dict if grants_dict else None
|
|
538
|
+
|
|
464
539
|
@property
|
|
465
540
|
def all_references(self) -> t.List[Reference]:
|
|
466
541
|
"""All references including grains."""
|
|
@@ -525,3 +600,33 @@ class ModelMeta(_Node):
|
|
|
525
600
|
@property
|
|
526
601
|
def ignored_rules(self) -> t.Set[str]:
|
|
527
602
|
return self.ignored_rules_ or set()
|
|
603
|
+
|
|
604
|
+
def _validate_config_expression(self, expr: exp.Expression) -> str:
|
|
605
|
+
if isinstance(expr, (d.MacroFunc, d.MacroVar)):
|
|
606
|
+
raise ConfigError(f"Unresolved macro: {expr.sql(dialect=self.dialect)}")
|
|
607
|
+
|
|
608
|
+
if isinstance(expr, exp.Null):
|
|
609
|
+
raise ConfigError("NULL value")
|
|
610
|
+
|
|
611
|
+
if isinstance(expr, exp.Literal):
|
|
612
|
+
return str(expr.this).strip()
|
|
613
|
+
if isinstance(expr, (exp.Column, exp.Identifier)):
|
|
614
|
+
return expr.name
|
|
615
|
+
return expr.sql(dialect=self.dialect).strip()
|
|
616
|
+
|
|
617
|
+
def _validate_nested_config_values(self, value_expr: exp.Expression) -> t.List[str]:
|
|
618
|
+
result = []
|
|
619
|
+
|
|
620
|
+
def flatten_expr(expr: exp.Expression) -> None:
|
|
621
|
+
if isinstance(expr, exp.Array):
|
|
622
|
+
for elem in expr.expressions:
|
|
623
|
+
flatten_expr(elem)
|
|
624
|
+
elif isinstance(expr, (exp.Tuple, exp.Paren)):
|
|
625
|
+
expressions = [expr.unnest()] if isinstance(expr, exp.Paren) else expr.expressions
|
|
626
|
+
for elem in expressions:
|
|
627
|
+
flatten_expr(elem)
|
|
628
|
+
else:
|
|
629
|
+
result.append(self._validate_config_expression(expr))
|
|
630
|
+
|
|
631
|
+
flatten_expr(value_expr)
|
|
632
|
+
return result
|
sqlmesh/core/node.py
CHANGED
|
@@ -153,6 +153,101 @@ class IntervalUnit(str, Enum):
|
|
|
153
153
|
return self.seconds * 1000
|
|
154
154
|
|
|
155
155
|
|
|
156
|
+
class DbtNodeInfo(PydanticModel):
|
|
157
|
+
"""
|
|
158
|
+
Represents dbt-specific model information set by the dbt loader and intended to be made available at the Snapshot level
|
|
159
|
+
(as opposed to hidden within the individual model jinja macro registries).
|
|
160
|
+
|
|
161
|
+
This allows for things like injecting implementations of variables / functions into the Jinja context that are compatible with
|
|
162
|
+
their dbt equivalents but are backed by the sqlmesh snapshots in any given plan / environment
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
unique_id: str
|
|
166
|
+
"""This is the node/resource name/unique_id that's used as the node key in the dbt manifest.
|
|
167
|
+
It's prefixed by the resource type and is exposed in context variables like {{ selected_resources }}.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
- test.jaffle_shop.unique_stg_orders_order_id.e3b841c71a
|
|
171
|
+
- seed.jaffle_shop.raw_payments
|
|
172
|
+
- model.jaffle_shop.stg_orders
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
name: str
|
|
176
|
+
"""Name of this object in the dbt global namespace, used by things like {{ ref() }} calls.
|
|
177
|
+
|
|
178
|
+
Examples:
|
|
179
|
+
- unique_stg_orders_order_id
|
|
180
|
+
- raw_payments
|
|
181
|
+
- stg_orders
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
fqn: str
|
|
185
|
+
"""Used for selectors in --select/--exclude.
|
|
186
|
+
Takes the filesystem into account so may be structured differently to :unique_id.
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
- jaffle_shop.staging.unique_stg_orders_order_id
|
|
190
|
+
- jaffle_shop.raw_payments
|
|
191
|
+
- jaffle_shop.staging.stg_orders
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
alias: t.Optional[str] = None
|
|
195
|
+
"""This is dbt's way of overriding the _physical table_ a model is written to.
|
|
196
|
+
|
|
197
|
+
It's used in the following situation:
|
|
198
|
+
- Say you have two models, "stg_customers" and "customers"
|
|
199
|
+
- You want "stg_customers" to be written to the "staging" schema as eg "staging.customers" - NOT "staging.stg_customers"
|
|
200
|
+
- But you cant rename the file to "customers" because it will conflict with your other model file "customers"
|
|
201
|
+
- Even if you put it in a different folder, eg "staging/customers.sql" - dbt still has a global namespace so it will conflict
|
|
202
|
+
when you try to do something like "{{ ref('customers') }}"
|
|
203
|
+
- So dbt's solution to this problem is to keep calling it "stg_customers" at the dbt project/model level,
|
|
204
|
+
but allow overriding the physical table to "customers" via something like "{{ config(alias='customers', schema='staging') }}"
|
|
205
|
+
|
|
206
|
+
Note that if :alias is set, it does *not* replace :name at the model level and cannot be used interchangably with :name.
|
|
207
|
+
It also does not affect the :fqn or :unique_id. It's just used to override :name when it comes time to generate the physical table name.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
@model_validator(mode="after")
|
|
211
|
+
def post_init(self) -> Self:
|
|
212
|
+
# by default, dbt sets alias to the same as :name
|
|
213
|
+
# however, we only want to include :alias if it is actually different / actually providing an override
|
|
214
|
+
if self.alias == self.name:
|
|
215
|
+
self.alias = None
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
def to_expression(self) -> exp.Expression:
|
|
219
|
+
"""Produce a SQLGlot expression representing this object, for use in things like the model/audit definition renderers"""
|
|
220
|
+
return exp.tuple_(
|
|
221
|
+
*(
|
|
222
|
+
exp.PropertyEQ(this=exp.var(k), expression=exp.Literal.string(v))
|
|
223
|
+
for k, v in sorted(self.model_dump(exclude_none=True).items())
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class DbtInfoMixin:
|
|
229
|
+
"""This mixin encapsulates properties that only exist for dbt compatibility and are otherwise not required
|
|
230
|
+
for native projects"""
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def dbt_node_info(self) -> t.Optional[DbtNodeInfo]:
|
|
234
|
+
raise NotImplementedError()
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def dbt_unique_id(self) -> t.Optional[str]:
|
|
238
|
+
"""Used for compatibility with jinja context variables such as {{ selected_resources }}"""
|
|
239
|
+
if self.dbt_node_info:
|
|
240
|
+
return self.dbt_node_info.unique_id
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def dbt_fqn(self) -> t.Optional[str]:
|
|
245
|
+
"""Used in the selector engine for compatibility with selectors that select models by dbt fqn"""
|
|
246
|
+
if self.dbt_node_info:
|
|
247
|
+
return self.dbt_node_info.fqn
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
156
251
|
# this must be sorted in descending order
|
|
157
252
|
INTERVAL_SECONDS = {
|
|
158
253
|
IntervalUnit.YEAR: 60 * 60 * 24 * 365,
|
|
@@ -165,7 +260,7 @@ INTERVAL_SECONDS = {
|
|
|
165
260
|
}
|
|
166
261
|
|
|
167
262
|
|
|
168
|
-
class _Node(PydanticModel):
|
|
263
|
+
class _Node(DbtInfoMixin, PydanticModel):
|
|
169
264
|
"""
|
|
170
265
|
Node is the core abstraction for entity that can be executed within the scheduler.
|
|
171
266
|
|
|
@@ -199,7 +294,7 @@ class _Node(PydanticModel):
|
|
|
199
294
|
interval_unit_: t.Optional[IntervalUnit] = Field(alias="interval_unit", default=None)
|
|
200
295
|
tags: t.List[str] = []
|
|
201
296
|
stamp: t.Optional[str] = None
|
|
202
|
-
|
|
297
|
+
dbt_node_info_: t.Optional[DbtNodeInfo] = Field(alias="dbt_node_info", default=None)
|
|
203
298
|
_path: t.Optional[Path] = None
|
|
204
299
|
_data_hash: t.Optional[str] = None
|
|
205
300
|
_metadata_hash: t.Optional[str] = None
|
|
@@ -446,6 +541,10 @@ class _Node(PydanticModel):
|
|
|
446
541
|
"""Return True if this is an audit node"""
|
|
447
542
|
return False
|
|
448
543
|
|
|
544
|
+
@property
|
|
545
|
+
def dbt_node_info(self) -> t.Optional[DbtNodeInfo]:
|
|
546
|
+
return self.dbt_node_info_
|
|
547
|
+
|
|
449
548
|
|
|
450
549
|
class NodeType(str, Enum):
|
|
451
550
|
MODEL = "model"
|
sqlmesh/core/plan/builder.py
CHANGED
|
@@ -65,6 +65,9 @@ class PlanBuilder:
|
|
|
65
65
|
restate_models: A list of models for which the data should be restated for the time range
|
|
66
66
|
specified in this plan. Note: models defined outside SQLMesh (external) won't be a part
|
|
67
67
|
of the restatement.
|
|
68
|
+
restate_all_snapshots: If restatements are present, this flag indicates whether or not the intervals
|
|
69
|
+
being restated should be cleared from state for other versions of this model (typically, versions that are present in other environments).
|
|
70
|
+
If set to None, the default behaviour is to not clear anything unless the target environment is prod.
|
|
68
71
|
backfill_models: A list of fully qualified model names for which the data should be backfilled as part of this plan.
|
|
69
72
|
no_gaps: Whether to ensure that new snapshots for nodes that are already a
|
|
70
73
|
part of the target environment have no data gaps when compared against previous
|
|
@@ -103,6 +106,7 @@ class PlanBuilder:
|
|
|
103
106
|
execution_time: t.Optional[TimeLike] = None,
|
|
104
107
|
apply: t.Optional[t.Callable[[Plan], None]] = None,
|
|
105
108
|
restate_models: t.Optional[t.Iterable[str]] = None,
|
|
109
|
+
restate_all_snapshots: bool = False,
|
|
106
110
|
backfill_models: t.Optional[t.Iterable[str]] = None,
|
|
107
111
|
no_gaps: bool = False,
|
|
108
112
|
skip_backfill: bool = False,
|
|
@@ -154,6 +158,7 @@ class PlanBuilder:
|
|
|
154
158
|
self._auto_categorization_enabled = auto_categorization_enabled
|
|
155
159
|
self._include_unmodified = include_unmodified
|
|
156
160
|
self._restate_models = set(restate_models) if restate_models is not None else None
|
|
161
|
+
self._restate_all_snapshots = restate_all_snapshots
|
|
157
162
|
self._effective_from = effective_from
|
|
158
163
|
|
|
159
164
|
# note: this deliberately doesnt default to now() here.
|
|
@@ -277,7 +282,6 @@ class PlanBuilder:
|
|
|
277
282
|
if self._latest_plan:
|
|
278
283
|
return self._latest_plan
|
|
279
284
|
|
|
280
|
-
self._ensure_no_new_snapshots_with_restatements()
|
|
281
285
|
self._ensure_new_env_with_changes()
|
|
282
286
|
self._ensure_valid_date_range()
|
|
283
287
|
self._ensure_no_broken_references()
|
|
@@ -338,7 +342,9 @@ class PlanBuilder:
|
|
|
338
342
|
directly_modified=directly_modified,
|
|
339
343
|
indirectly_modified=indirectly_modified,
|
|
340
344
|
deployability_index=deployability_index,
|
|
345
|
+
selected_models_to_restate=self._restate_models,
|
|
341
346
|
restatements=restatements,
|
|
347
|
+
restate_all_snapshots=self._restate_all_snapshots,
|
|
342
348
|
start_override_per_model=self._start_override_per_model,
|
|
343
349
|
end_override_per_model=end_override_per_model,
|
|
344
350
|
selected_models_to_backfill=self._backfill_models,
|
|
@@ -674,6 +680,14 @@ class PlanBuilder:
|
|
|
674
680
|
if mode == AutoCategorizationMode.FULL:
|
|
675
681
|
snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only)
|
|
676
682
|
elif self._context_diff.indirectly_modified(snapshot.name):
|
|
683
|
+
if snapshot.is_materialized_view and not forward_only:
|
|
684
|
+
# We categorize changes as breaking to allow for instantaneous switches in a virtual layer.
|
|
685
|
+
# Otherwise, there might be a potentially long downtime during MVs recreation.
|
|
686
|
+
# In the case of forward-only changes this optimization is not applicable because we want to continue
|
|
687
|
+
# using the same (existing) table version.
|
|
688
|
+
snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING, forward_only)
|
|
689
|
+
return
|
|
690
|
+
|
|
677
691
|
all_upstream_forward_only = set()
|
|
678
692
|
all_upstream_categories = set()
|
|
679
693
|
direct_parent_categories = set()
|
|
@@ -858,15 +872,6 @@ class PlanBuilder:
|
|
|
858
872
|
f"""Removed {broken_references_msg} are referenced in '{snapshot.name}'. Please remove broken references before proceeding."""
|
|
859
873
|
)
|
|
860
874
|
|
|
861
|
-
def _ensure_no_new_snapshots_with_restatements(self) -> None:
|
|
862
|
-
if self._restate_models is not None and (
|
|
863
|
-
self._context_diff.new_snapshots or self._context_diff.modified_snapshots
|
|
864
|
-
):
|
|
865
|
-
raise PlanError(
|
|
866
|
-
"Model changes and restatements can't be a part of the same plan. "
|
|
867
|
-
"Revert or apply changes before proceeding with restatements."
|
|
868
|
-
)
|
|
869
|
-
|
|
870
875
|
def _ensure_new_env_with_changes(self) -> None:
|
|
871
876
|
if (
|
|
872
877
|
self._is_dev
|