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
@@ -23,7 +23,7 @@ from sqlmesh.utils.pydantic import (
23
23
  PydanticModel,
24
24
  SQLGlotBool,
25
25
  SQLGlotColumn,
26
- SQLGlotListOfColumnsOrStar,
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: SQLGlotListOfColumnsOrStar
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
@@ -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
- for column_name in col_descriptions:
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
- raise ConfigError(
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
- dbt_name: t.Optional[str] = None # dbt node name
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"
@@ -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