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
@@ -18,7 +18,7 @@ from functools import cached_property, partial
18
18
 
19
19
  from sqlglot import Dialect, exp
20
20
  from sqlglot.errors import ErrorLevel
21
- from sqlglot.helper import ensure_list
21
+ from sqlglot.helper import ensure_list, seq_get
22
22
  from sqlglot.optimizer.qualify_columns import quote_identifiers
23
23
 
24
24
  from sqlmesh.core.dialect import (
@@ -63,6 +63,7 @@ if t.TYPE_CHECKING:
63
63
  from sqlmesh.core.engine_adapter._typing import (
64
64
  DF,
65
65
  BigframeSession,
66
+ GrantsConfig,
66
67
  PySparkDataFrame,
67
68
  PySparkSession,
68
69
  Query,
@@ -114,11 +115,13 @@ class EngineAdapter:
114
115
  SUPPORTS_TUPLE_IN = True
115
116
  HAS_VIEW_BINDING = False
116
117
  SUPPORTS_REPLACE_TABLE = True
118
+ SUPPORTS_GRANTS = False
117
119
  DEFAULT_CATALOG_TYPE = DIALECT
118
120
  QUOTE_IDENTIFIERS_IN_VIEWS = True
119
121
  MAX_IDENTIFIER_LENGTH: t.Optional[int] = None
120
122
  ATTACH_CORRELATION_ID = True
121
123
  SUPPORTS_QUERY_EXECUTION_TRACKING = False
124
+ SUPPORTS_METADATA_TABLE_LAST_MODIFIED_TS = False
122
125
 
123
126
  def __init__(
124
127
  self,
@@ -160,6 +163,7 @@ class EngineAdapter:
160
163
  self.correlation_id = correlation_id
161
164
  self._schema_differ_overrides = schema_differ_overrides
162
165
  self._query_execution_tracker = query_execution_tracker
166
+ self._data_object_cache: t.Dict[str, t.Optional[DataObject]] = {}
163
167
 
164
168
  def with_settings(self, **kwargs: t.Any) -> EngineAdapter:
165
169
  extra_kwargs = {
@@ -223,6 +227,10 @@ class EngineAdapter:
223
227
  }
224
228
  )
225
229
 
230
+ @property
231
+ def _catalog_type_overrides(self) -> t.Dict[str, str]:
232
+ return self._extra_config.get("catalog_type_overrides") or {}
233
+
226
234
  @classmethod
227
235
  def _casted_columns(
228
236
  cls,
@@ -430,7 +438,11 @@ class EngineAdapter:
430
438
  raise UnsupportedCatalogOperationError(
431
439
  f"{self.dialect} does not support catalogs and a catalog was provided: {catalog}"
432
440
  )
433
- return self.DEFAULT_CATALOG_TYPE
441
+ return (
442
+ self._catalog_type_overrides.get(catalog, self.DEFAULT_CATALOG_TYPE)
443
+ if catalog
444
+ else self.DEFAULT_CATALOG_TYPE
445
+ )
434
446
 
435
447
  def get_catalog_type_from_table(self, table: TableName) -> str:
436
448
  """Get the catalog type from a table name if it has a catalog specified, otherwise return the current catalog type"""
@@ -539,11 +551,13 @@ class EngineAdapter:
539
551
  target_table,
540
552
  source_queries,
541
553
  target_columns_to_types,
554
+ **kwargs,
542
555
  )
543
556
  return self._insert_overwrite_by_condition(
544
557
  target_table,
545
558
  source_queries,
546
559
  target_columns_to_types,
560
+ **kwargs,
547
561
  )
548
562
 
549
563
  def create_index(
@@ -797,6 +811,7 @@ class EngineAdapter:
797
811
  column_descriptions: t.Optional[t.Dict[str, str]] = None,
798
812
  expressions: t.Optional[t.List[exp.PrimaryKey]] = None,
799
813
  is_view: bool = False,
814
+ materialized: bool = False,
800
815
  ) -> exp.Schema:
801
816
  """
802
817
  Build a schema expression for a table, columns, column comments, and additional schema properties.
@@ -809,6 +824,7 @@ class EngineAdapter:
809
824
  target_columns_to_types=target_columns_to_types,
810
825
  column_descriptions=column_descriptions,
811
826
  is_view=is_view,
827
+ materialized=materialized,
812
828
  )
813
829
  + expressions,
814
830
  )
@@ -818,6 +834,7 @@ class EngineAdapter:
818
834
  target_columns_to_types: t.Dict[str, exp.DataType],
819
835
  column_descriptions: t.Optional[t.Dict[str, str]] = None,
820
836
  is_view: bool = False,
837
+ materialized: bool = False,
821
838
  ) -> t.List[exp.ColumnDef]:
822
839
  engine_supports_schema_comments = (
823
840
  self.COMMENT_CREATION_VIEW.supports_schema_def
@@ -974,6 +991,13 @@ class EngineAdapter:
974
991
  ),
975
992
  track_rows_processed=track_rows_processed,
976
993
  )
994
+ # Extract table name to clear cache
995
+ table_name = (
996
+ table_name_or_schema.this
997
+ if isinstance(table_name_or_schema, exp.Schema)
998
+ else table_name_or_schema
999
+ )
1000
+ self._clear_data_object_cache(table_name)
977
1001
 
978
1002
  def _build_create_table_exp(
979
1003
  self,
@@ -1029,13 +1053,15 @@ class EngineAdapter:
1029
1053
  target_table_name: The name of the table to create. Can be fully qualified or just table name.
1030
1054
  source_table_name: The name of the table to base the new table on.
1031
1055
  """
1032
- self.create_table(target_table_name, self.columns(source_table_name), exists=exists)
1056
+ self._create_table_like(target_table_name, source_table_name, exists=exists, **kwargs)
1057
+ self._clear_data_object_cache(target_table_name)
1033
1058
 
1034
1059
  def clone_table(
1035
1060
  self,
1036
1061
  target_table_name: TableName,
1037
1062
  source_table_name: TableName,
1038
1063
  replace: bool = False,
1064
+ exists: bool = True,
1039
1065
  clone_kwargs: t.Optional[t.Dict[str, t.Any]] = None,
1040
1066
  **kwargs: t.Any,
1041
1067
  ) -> None:
@@ -1045,6 +1071,7 @@ class EngineAdapter:
1045
1071
  target_table_name: The name of the table that should be created.
1046
1072
  source_table_name: The name of the source table that should be cloned.
1047
1073
  replace: Whether or not to replace an existing table.
1074
+ exists: Indicates whether to include the IF NOT EXISTS check.
1048
1075
  """
1049
1076
  if not self.SUPPORTS_CLONING:
1050
1077
  raise NotImplementedError(f"Engine does not support cloning: {type(self)}")
@@ -1055,6 +1082,7 @@ class EngineAdapter:
1055
1082
  this=exp.to_table(target_table_name),
1056
1083
  kind="TABLE",
1057
1084
  replace=replace,
1085
+ exists=exists,
1058
1086
  clone=exp.Clone(
1059
1087
  this=exp.to_table(source_table_name),
1060
1088
  **(clone_kwargs or {}),
@@ -1062,6 +1090,7 @@ class EngineAdapter:
1062
1090
  **kwargs,
1063
1091
  )
1064
1092
  )
1093
+ self._clear_data_object_cache(target_table_name)
1065
1094
 
1066
1095
  def drop_data_object(self, data_object: DataObject, ignore_if_not_exists: bool = True) -> None:
1067
1096
  """Drops a data object of arbitrary type.
@@ -1127,6 +1156,7 @@ class EngineAdapter:
1127
1156
  drop_args["cascade"] = cascade
1128
1157
 
1129
1158
  self.execute(exp.Drop(this=exp.to_table(name), kind=kind, exists=exists, **drop_args))
1159
+ self._clear_data_object_cache(name)
1130
1160
 
1131
1161
  def get_alter_operations(
1132
1162
  self,
@@ -1233,7 +1263,11 @@ class EngineAdapter:
1233
1263
  schema: t.Union[exp.Table, exp.Schema] = exp.to_table(view_name)
1234
1264
  if target_columns_to_types:
1235
1265
  schema = self._build_schema_exp(
1236
- exp.to_table(view_name), target_columns_to_types, column_descriptions, is_view=True
1266
+ exp.to_table(view_name),
1267
+ target_columns_to_types,
1268
+ column_descriptions,
1269
+ is_view=True,
1270
+ materialized=materialized,
1237
1271
  )
1238
1272
 
1239
1273
  properties = create_kwargs.pop("properties", None)
@@ -1317,6 +1351,8 @@ class EngineAdapter:
1317
1351
  quote_identifiers=self.QUOTE_IDENTIFIERS_IN_VIEWS,
1318
1352
  )
1319
1353
 
1354
+ self._clear_data_object_cache(view_name)
1355
+
1320
1356
  # Register table comment with commands if the engine doesn't support doing it in CREATE
1321
1357
  if (
1322
1358
  table_description
@@ -1446,8 +1482,14 @@ class EngineAdapter:
1446
1482
  }
1447
1483
 
1448
1484
  def table_exists(self, table_name: TableName) -> bool:
1485
+ table = exp.to_table(table_name)
1486
+ data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name)
1487
+ if data_object_cache_key in self._data_object_cache:
1488
+ logger.debug("Table existence cache hit: %s", data_object_cache_key)
1489
+ return self._data_object_cache[data_object_cache_key] is not None
1490
+
1449
1491
  try:
1450
- self.execute(exp.Describe(this=exp.to_table(table_name), kind="TABLE"))
1492
+ self.execute(exp.Describe(this=table, kind="TABLE"))
1451
1493
  return True
1452
1494
  except Exception:
1453
1495
  return False
@@ -1581,7 +1623,7 @@ class EngineAdapter:
1581
1623
  **kwargs: t.Any,
1582
1624
  ) -> None:
1583
1625
  return self._insert_overwrite_by_condition(
1584
- table_name, source_queries, target_columns_to_types, where
1626
+ table_name, source_queries, target_columns_to_types, where, **kwargs
1585
1627
  )
1586
1628
 
1587
1629
  def _values_to_sql(
@@ -1633,6 +1675,30 @@ class EngineAdapter:
1633
1675
  target_columns_to_types=target_columns_to_types,
1634
1676
  order_projections=False,
1635
1677
  )
1678
+ elif insert_overwrite_strategy.is_merge:
1679
+ columns = [exp.column(col) for col in target_columns_to_types]
1680
+ when_not_matched_by_source = exp.When(
1681
+ matched=False,
1682
+ source=True,
1683
+ condition=where,
1684
+ then=exp.Delete(),
1685
+ )
1686
+ when_not_matched_by_target = exp.When(
1687
+ matched=False,
1688
+ source=False,
1689
+ then=exp.Insert(
1690
+ this=exp.Tuple(expressions=columns),
1691
+ expression=exp.Tuple(expressions=columns),
1692
+ ),
1693
+ )
1694
+ self._merge(
1695
+ target_table=table_name,
1696
+ query=query,
1697
+ on=exp.false(),
1698
+ whens=exp.Whens(
1699
+ expressions=[when_not_matched_by_source, when_not_matched_by_target]
1700
+ ),
1701
+ )
1636
1702
  else:
1637
1703
  insert_exp = exp.insert(
1638
1704
  query,
@@ -1715,7 +1781,7 @@ class EngineAdapter:
1715
1781
  valid_from_col: exp.Column,
1716
1782
  valid_to_col: exp.Column,
1717
1783
  execution_time: t.Union[TimeLike, exp.Column],
1718
- check_columns: t.Union[exp.Star, t.Sequence[exp.Column]],
1784
+ check_columns: t.Union[exp.Star, t.Sequence[exp.Expression]],
1719
1785
  invalidate_hard_deletes: bool = True,
1720
1786
  execution_time_as_valid_from: bool = False,
1721
1787
  target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
@@ -1753,7 +1819,7 @@ class EngineAdapter:
1753
1819
  execution_time: t.Union[TimeLike, exp.Column],
1754
1820
  invalidate_hard_deletes: bool = True,
1755
1821
  updated_at_col: t.Optional[exp.Column] = None,
1756
- check_columns: t.Optional[t.Union[exp.Star, t.Sequence[exp.Column]]] = None,
1822
+ check_columns: t.Optional[t.Union[exp.Star, t.Sequence[exp.Expression]]] = None,
1757
1823
  updated_at_as_valid_from: bool = False,
1758
1824
  execution_time_as_valid_from: bool = False,
1759
1825
  target_columns_to_types: t.Optional[t.Dict[str, exp.DataType]] = None,
@@ -1828,8 +1894,10 @@ class EngineAdapter:
1828
1894
  # they are equal or not, the extra check is not a problem and we gain simplified logic here.
1829
1895
  # If we want to change this, then we just need to check the expressions in unique_key and pull out the
1830
1896
  # column names and then remove them from the unmanaged_columns
1831
- if check_columns and check_columns == exp.Star():
1832
- check_columns = [exp.column(col) for col in unmanaged_columns_to_types]
1897
+ if check_columns:
1898
+ # Handle both Star directly and [Star()] (which can happen during serialization/deserialization)
1899
+ if isinstance(seq_get(ensure_list(check_columns), 0), exp.Star):
1900
+ check_columns = [exp.column(col) for col in unmanaged_columns_to_types]
1833
1901
  execution_ts = (
1834
1902
  exp.cast(execution_time, time_data_type, dialect=self.dialect)
1835
1903
  if isinstance(execution_time, exp.Column)
@@ -1866,7 +1934,8 @@ class EngineAdapter:
1866
1934
  col_qualified.set("table", exp.to_identifier("joined"))
1867
1935
 
1868
1936
  t_col = col_qualified.copy()
1869
- t_col.this.set("this", f"t_{col.name}")
1937
+ for column in t_col.find_all(exp.Column):
1938
+ column.this.set("this", f"t_{column.name}")
1870
1939
 
1871
1940
  row_check_conditions.extend(
1872
1941
  [
@@ -2217,24 +2286,34 @@ class EngineAdapter:
2217
2286
  "Tried to rename table across catalogs which is not supported"
2218
2287
  )
2219
2288
  self._rename_table(old_table_name, new_table_name)
2289
+ self._clear_data_object_cache(old_table_name)
2290
+ self._clear_data_object_cache(new_table_name)
2220
2291
 
2221
- def get_data_object(self, target_name: TableName) -> t.Optional[DataObject]:
2292
+ def get_data_object(
2293
+ self, target_name: TableName, safe_to_cache: bool = False
2294
+ ) -> t.Optional[DataObject]:
2222
2295
  target_table = exp.to_table(target_name)
2223
2296
  existing_data_objects = self.get_data_objects(
2224
- schema_(target_table.db, target_table.catalog), {target_table.name}
2297
+ schema_(target_table.db, target_table.catalog),
2298
+ {target_table.name},
2299
+ safe_to_cache=safe_to_cache,
2225
2300
  )
2226
2301
  if existing_data_objects:
2227
2302
  return existing_data_objects[0]
2228
2303
  return None
2229
2304
 
2230
2305
  def get_data_objects(
2231
- self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None
2306
+ self,
2307
+ schema_name: SchemaName,
2308
+ object_names: t.Optional[t.Set[str]] = None,
2309
+ safe_to_cache: bool = False,
2232
2310
  ) -> t.List[DataObject]:
2233
2311
  """Lists all data objects in the target schema.
2234
2312
 
2235
2313
  Args:
2236
2314
  schema_name: The name of the schema to list data objects from.
2237
2315
  object_names: If provided, only return data objects with these names.
2316
+ safe_to_cache: Whether it is safe to cache the results of this call.
2238
2317
 
2239
2318
  Returns:
2240
2319
  A list of data objects in the target schema.
@@ -2242,15 +2321,64 @@ class EngineAdapter:
2242
2321
  if object_names is not None:
2243
2322
  if not object_names:
2244
2323
  return []
2245
- object_names_list = list(object_names)
2246
- batches = [
2247
- object_names_list[i : i + self.DATA_OBJECT_FILTER_BATCH_SIZE]
2248
- for i in range(0, len(object_names_list), self.DATA_OBJECT_FILTER_BATCH_SIZE)
2249
- ]
2250
- return [
2251
- obj for batch in batches for obj in self._get_data_objects(schema_name, set(batch))
2252
- ]
2253
- return self._get_data_objects(schema_name)
2324
+
2325
+ # Check cache for each object name
2326
+ target_schema = to_schema(schema_name)
2327
+ cached_objects = []
2328
+ missing_names = set()
2329
+
2330
+ for name in object_names:
2331
+ cache_key = _get_data_object_cache_key(
2332
+ target_schema.catalog, target_schema.db, name
2333
+ )
2334
+ if cache_key in self._data_object_cache:
2335
+ logger.debug("Data object cache hit: %s", cache_key)
2336
+ data_object = self._data_object_cache[cache_key]
2337
+ # If the object is none, then the table was previously looked for but not found
2338
+ if data_object:
2339
+ cached_objects.append(data_object)
2340
+ else:
2341
+ logger.debug("Data object cache miss: %s", cache_key)
2342
+ missing_names.add(name)
2343
+
2344
+ # Fetch missing objects from database
2345
+ if missing_names:
2346
+ object_names_list = list(missing_names)
2347
+ batches = [
2348
+ object_names_list[i : i + self.DATA_OBJECT_FILTER_BATCH_SIZE]
2349
+ for i in range(0, len(object_names_list), self.DATA_OBJECT_FILTER_BATCH_SIZE)
2350
+ ]
2351
+
2352
+ fetched_objects = []
2353
+ fetched_object_names = set()
2354
+ for batch in batches:
2355
+ objects = self._get_data_objects(schema_name, set(batch))
2356
+ for obj in objects:
2357
+ if safe_to_cache:
2358
+ cache_key = _get_data_object_cache_key(
2359
+ obj.catalog, obj.schema_name, obj.name
2360
+ )
2361
+ self._data_object_cache[cache_key] = obj
2362
+ fetched_objects.append(obj)
2363
+ fetched_object_names.add(obj.name)
2364
+
2365
+ if safe_to_cache:
2366
+ for missing_name in missing_names - fetched_object_names:
2367
+ cache_key = _get_data_object_cache_key(
2368
+ target_schema.catalog, target_schema.db, missing_name
2369
+ )
2370
+ self._data_object_cache[cache_key] = None
2371
+
2372
+ return cached_objects + fetched_objects
2373
+
2374
+ return cached_objects
2375
+
2376
+ fetched_objects = self._get_data_objects(schema_name)
2377
+ if safe_to_cache:
2378
+ for obj in fetched_objects:
2379
+ cache_key = _get_data_object_cache_key(obj.catalog, obj.schema_name, obj.name)
2380
+ self._data_object_cache[cache_key] = obj
2381
+ return fetched_objects
2254
2382
 
2255
2383
  def fetchone(
2256
2384
  self,
@@ -2322,6 +2450,11 @@ class EngineAdapter:
2322
2450
  """Fetches a PySpark DataFrame from the cursor"""
2323
2451
  raise NotImplementedError(f"Engine does not support PySpark DataFrames: {type(self)}")
2324
2452
 
2453
+ @property
2454
+ def wap_enabled(self) -> bool:
2455
+ """Returns whether WAP is enabled for this engine."""
2456
+ return self._extra_config.get("wap_enabled", False)
2457
+
2325
2458
  def wap_supported(self, table_name: TableName) -> bool:
2326
2459
  """Returns whether WAP for the target table is supported."""
2327
2460
  return False
@@ -2359,6 +2492,33 @@ class EngineAdapter:
2359
2492
  """
2360
2493
  raise NotImplementedError(f"Engine does not support WAP: {type(self)}")
2361
2494
 
2495
+ def sync_grants_config(
2496
+ self,
2497
+ table: exp.Table,
2498
+ grants_config: GrantsConfig,
2499
+ table_type: DataObjectType = DataObjectType.TABLE,
2500
+ ) -> None:
2501
+ """Applies the grants_config to a table authoritatively.
2502
+ It first compares the specified grants against the current grants, and then
2503
+ applies the diffs to the table by revoking and granting privileges as needed.
2504
+
2505
+ Args:
2506
+ table: The table/view to apply grants to.
2507
+ grants_config: Dictionary mapping privileges to lists of grantees.
2508
+ table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW).
2509
+ """
2510
+ if not self.SUPPORTS_GRANTS:
2511
+ raise NotImplementedError(f"Engine does not support grants: {type(self)}")
2512
+
2513
+ current_grants = self._get_current_grants_config(table)
2514
+ new_grants, revoked_grants = self._diff_grants_configs(grants_config, current_grants)
2515
+ revoke_exprs = self._revoke_grants_config_expr(table, revoked_grants, table_type)
2516
+ grant_exprs = self._apply_grants_config_expr(table, new_grants, table_type)
2517
+ dcl_exprs = revoke_exprs + grant_exprs
2518
+
2519
+ if dcl_exprs:
2520
+ self.execute(dcl_exprs)
2521
+
2362
2522
  @contextlib.contextmanager
2363
2523
  def transaction(
2364
2524
  self,
@@ -2652,6 +2812,17 @@ class EngineAdapter:
2652
2812
 
2653
2813
  return expression.sql(**sql_gen_kwargs, copy=False) # type: ignore
2654
2814
 
2815
+ def _clear_data_object_cache(self, table_name: t.Optional[TableName] = None) -> None:
2816
+ """Clears the cache entry for the given table name, or clears the entire cache if table_name is None."""
2817
+ if table_name is None:
2818
+ logger.debug("Clearing entire data object cache")
2819
+ self._data_object_cache.clear()
2820
+ else:
2821
+ table = exp.to_table(table_name)
2822
+ cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name)
2823
+ logger.debug("Clearing data object cache key: %s", cache_key)
2824
+ self._data_object_cache.pop(cache_key, None)
2825
+
2655
2826
  def _get_data_objects(
2656
2827
  self, schema_name: SchemaName, object_names: t.Optional[t.Set[str]] = None
2657
2828
  ) -> t.List[DataObject]:
@@ -2837,6 +3008,15 @@ class EngineAdapter:
2837
3008
  exc_info=True,
2838
3009
  )
2839
3010
 
3011
+ def _create_table_like(
3012
+ self,
3013
+ target_table_name: TableName,
3014
+ source_table_name: TableName,
3015
+ exists: bool,
3016
+ **kwargs: t.Any,
3017
+ ) -> None:
3018
+ self.create_table(target_table_name, self.columns(source_table_name), exists=exists)
3019
+
2840
3020
  def _rename_table(
2841
3021
  self,
2842
3022
  old_table_name: TableName,
@@ -2887,6 +3067,127 @@ class EngineAdapter:
2887
3067
  f"Identifier name '{name}' (length {name_length}) exceeds {self.dialect.capitalize()}'s max identifier limit of {self.MAX_IDENTIFIER_LENGTH} characters"
2888
3068
  )
2889
3069
 
3070
+ def get_table_last_modified_ts(self, table_names: t.List[TableName]) -> t.List[int]:
3071
+ raise NotImplementedError()
3072
+
3073
+ @classmethod
3074
+ def _diff_grants_configs(
3075
+ cls, new_config: GrantsConfig, old_config: GrantsConfig
3076
+ ) -> t.Tuple[GrantsConfig, GrantsConfig]:
3077
+ """Compute additions and removals between two grants configurations.
3078
+
3079
+ This method compares new (desired) and old (current) GrantsConfigs case-insensitively
3080
+ for both privilege keys and grantees, while preserving original casing
3081
+ in the output GrantsConfigs.
3082
+
3083
+ Args:
3084
+ new_config: Desired grants configuration (specified by the user).
3085
+ old_config: Current grants configuration (returned by the database).
3086
+
3087
+ Returns:
3088
+ A tuple of (additions, removals) GrantsConfig where:
3089
+ - additions contains privileges/grantees present in new_config but not in old_config
3090
+ - additions uses keys and grantee strings from new_config (user-specified casing)
3091
+ - removals contains privileges/grantees present in old_config but not in new_config
3092
+ - removals uses keys and grantee strings from old_config (database-returned casing)
3093
+
3094
+ Notes:
3095
+ - Comparison is case-insensitive using casefold(); original casing is preserved in results.
3096
+ - Overlapping grantees (case-insensitive) are excluded from the results.
3097
+ """
3098
+
3099
+ def _diffs(config1: GrantsConfig, config2: GrantsConfig) -> GrantsConfig:
3100
+ diffs: GrantsConfig = {}
3101
+ cf_config2 = {k.casefold(): {g.casefold() for g in v} for k, v in config2.items()}
3102
+ for key, grantees in config1.items():
3103
+ cf_key = key.casefold()
3104
+
3105
+ # Missing key (add all grantees)
3106
+ if cf_key not in cf_config2:
3107
+ diffs[key] = grantees.copy()
3108
+ continue
3109
+
3110
+ # Include only grantees not in config2
3111
+ cf_grantees2 = cf_config2[cf_key]
3112
+ diff_grantees = []
3113
+ for grantee in grantees:
3114
+ if grantee.casefold() not in cf_grantees2:
3115
+ diff_grantees.append(grantee)
3116
+ if diff_grantees:
3117
+ diffs[key] = diff_grantees
3118
+ return diffs
3119
+
3120
+ return _diffs(new_config, old_config), _diffs(old_config, new_config)
3121
+
3122
+ def _get_current_grants_config(self, table: exp.Table) -> GrantsConfig:
3123
+ """Returns current grants for a table as a dictionary.
3124
+
3125
+ This method queries the database and returns the current grants/permissions
3126
+ for the given table, parsed into a dictionary format. The it handles
3127
+ case-insensitive comparison between these current grants and the desired
3128
+ grants from model configuration.
3129
+
3130
+ Args:
3131
+ table: The table/view to query grants for.
3132
+
3133
+ Returns:
3134
+ Dictionary mapping permissions to lists of grantees. Permission names
3135
+ should be returned as the database provides them (typically uppercase
3136
+ for standard SQL permissions, but engine-specific roles may vary).
3137
+
3138
+ Raises:
3139
+ NotImplementedError: If the engine does not support grants.
3140
+ """
3141
+ if not self.SUPPORTS_GRANTS:
3142
+ raise NotImplementedError(f"Engine does not support grants: {type(self)}")
3143
+ raise NotImplementedError("Subclass must implement get_current_grants")
3144
+
3145
+ def _apply_grants_config_expr(
3146
+ self,
3147
+ table: exp.Table,
3148
+ grants_config: GrantsConfig,
3149
+ table_type: DataObjectType = DataObjectType.TABLE,
3150
+ ) -> t.List[exp.Expression]:
3151
+ """Returns SQLGlot Grant expressions to apply grants to a table.
3152
+
3153
+ Args:
3154
+ table: The table/view to grant permissions on.
3155
+ grants_config: Dictionary mapping permissions to lists of grantees.
3156
+ table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW).
3157
+
3158
+ Returns:
3159
+ List of SQLGlot expressions for grant operations.
3160
+
3161
+ Raises:
3162
+ NotImplementedError: If the engine does not support grants.
3163
+ """
3164
+ if not self.SUPPORTS_GRANTS:
3165
+ raise NotImplementedError(f"Engine does not support grants: {type(self)}")
3166
+ raise NotImplementedError("Subclass must implement _apply_grants_config_expr")
3167
+
3168
+ def _revoke_grants_config_expr(
3169
+ self,
3170
+ table: exp.Table,
3171
+ grants_config: GrantsConfig,
3172
+ table_type: DataObjectType = DataObjectType.TABLE,
3173
+ ) -> t.List[exp.Expression]:
3174
+ """Returns SQLGlot expressions to revoke grants from a table.
3175
+
3176
+ Args:
3177
+ table: The table/view to revoke permissions from.
3178
+ grants_config: Dictionary mapping permissions to lists of grantees.
3179
+ table_type: The type of database object (TABLE, VIEW, MATERIALIZED_VIEW).
3180
+
3181
+ Returns:
3182
+ List of SQLGlot expressions for revoke operations.
3183
+
3184
+ Raises:
3185
+ NotImplementedError: If the engine does not support grants.
3186
+ """
3187
+ if not self.SUPPORTS_GRANTS:
3188
+ raise NotImplementedError(f"Engine does not support grants: {type(self)}")
3189
+ raise NotImplementedError("Subclass must implement _revoke_grants_config_expr")
3190
+
2890
3191
 
2891
3192
  class EngineAdapterWithIndexSupport(EngineAdapter):
2892
3193
  SUPPORTS_INDEXES = True
@@ -2896,3 +3197,9 @@ def _decoded_str(value: t.Union[str, bytes]) -> str:
2896
3197
  if isinstance(value, bytes):
2897
3198
  return value.decode("utf-8")
2898
3199
  return value
3200
+
3201
+
3202
+ def _get_data_object_cache_key(catalog: t.Optional[str], schema_name: str, object_name: str) -> str:
3203
+ """Returns a cache key for a data object based on its fully qualified name."""
3204
+ catalog = f"{catalog}." if catalog else ""
3205
+ return f"{catalog}{schema_name}.{object_name}"
@@ -1,11 +1,12 @@
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
6
7
 
7
8
  from sqlmesh.core.dialect import to_schema
8
- from sqlmesh.core.engine_adapter import EngineAdapter
9
+ from sqlmesh.core.engine_adapter.base import EngineAdapter, _get_data_object_cache_key
9
10
  from sqlmesh.core.engine_adapter.shared import (
10
11
  CatalogSupport,
11
12
  CommentCreationTable,
@@ -20,6 +21,9 @@ if t.TYPE_CHECKING:
20
21
  from sqlmesh.core.engine_adapter._typing import QueryOrDF
21
22
 
22
23
 
24
+ logger = logging.getLogger(__name__)
25
+
26
+
23
27
  class BasePostgresEngineAdapter(EngineAdapter):
24
28
  DEFAULT_BATCH_SIZE = 400
25
29
  COMMENT_CREATION_TABLE = CommentCreationTable.COMMENT_COMMAND_ONLY
@@ -58,6 +62,7 @@ class BasePostgresEngineAdapter(EngineAdapter):
58
62
  raise SQLMeshError(
59
63
  f"Could not get columns for table '{table.sql(dialect=self.dialect)}'. Table not found."
60
64
  )
65
+
61
66
  return {
62
67
  column_name: exp.DataType.build(data_type, dialect=self.dialect, udt=True)
63
68
  for column_name, data_type in resp
@@ -75,6 +80,10 @@ class BasePostgresEngineAdapter(EngineAdapter):
75
80
  Reference: https://github.com/aws/amazon-redshift-python-driver/blob/master/redshift_connector/cursor.py#L528-L553
76
81
  """
77
82
  table = exp.to_table(table_name)
83
+ data_object_cache_key = _get_data_object_cache_key(table.catalog, table.db, table.name)
84
+ if data_object_cache_key in self._data_object_cache:
85
+ logger.debug("Table existence cache hit: %s", data_object_cache_key)
86
+ return self._data_object_cache[data_object_cache_key] is not None
78
87
 
79
88
  sql = (
80
89
  exp.select("1")
@@ -188,3 +197,10 @@ class BasePostgresEngineAdapter(EngineAdapter):
188
197
  )
189
198
  for row in df.itertuples()
190
199
  ]
200
+
201
+ def _get_current_schema(self) -> str:
202
+ """Returns the current default schema for the connection."""
203
+ result = self.fetchone(exp.select(exp.func("current_schema")))
204
+ if result and result[0]:
205
+ return result[0]
206
+ return "public"