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
@@ -32,12 +32,14 @@ from functools import reduce
32
32
 
33
33
  from sqlglot import exp, select
34
34
  from sqlglot.executor import execute
35
+ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_not_exception_type
35
36
 
36
37
  from sqlmesh.core import constants as c
37
38
  from sqlmesh.core import dialect as d
38
39
  from sqlmesh.core.audit import Audit, StandaloneAudit
39
40
  from sqlmesh.core.dialect import schema_
40
41
  from sqlmesh.core.engine_adapter.shared import InsertOverwriteStrategy, DataObjectType, DataObject
42
+ from sqlmesh.core.model.meta import GrantsTargetLayer
41
43
  from sqlmesh.core.macros import RuntimeStage
42
44
  from sqlmesh.core.model import (
43
45
  AuditResult,
@@ -49,7 +51,7 @@ from sqlmesh.core.model import (
49
51
  ViewKind,
50
52
  CustomKind,
51
53
  )
52
- from sqlmesh.core.model.kind import _Incremental
54
+ from sqlmesh.core.model.kind import _Incremental, DbtCustomKind
53
55
  from sqlmesh.utils import CompletionStatus, columns_to_types_all_known
54
56
  from sqlmesh.core.schema_diff import (
55
57
  has_drop_alteration,
@@ -66,7 +68,7 @@ from sqlmesh.core.snapshot import (
66
68
  SnapshotTableCleanupTask,
67
69
  )
68
70
  from sqlmesh.core.snapshot.execution_tracker import QueryExecutionTracker
69
- from sqlmesh.utils import random_id, CorrelationId
71
+ from sqlmesh.utils import random_id, CorrelationId, AttributeDict
70
72
  from sqlmesh.utils.concurrency import (
71
73
  concurrent_apply_to_snapshots,
72
74
  concurrent_apply_to_values,
@@ -76,11 +78,13 @@ from sqlmesh.utils.date import TimeLike, now, time_like_to_str
76
78
  from sqlmesh.utils.errors import (
77
79
  ConfigError,
78
80
  DestructiveChangeError,
81
+ MigrationNotSupportedError,
79
82
  SQLMeshError,
80
83
  format_destructive_change_msg,
81
84
  format_additive_change_msg,
82
85
  AdditiveChangeError,
83
86
  )
87
+ from sqlmesh.utils.jinja import MacroReturnVal
84
88
 
85
89
  if sys.version_info >= (3, 12):
86
90
  from importlib import metadata
@@ -304,6 +308,9 @@ class SnapshotEvaluator:
304
308
  ]
305
309
  self._create_schemas(gateway_table_pairs=gateway_table_pairs)
306
310
 
311
+ # Fetch the view data objects for the promoted snapshots to get them cached
312
+ self._get_virtual_data_objects(target_snapshots, environment_naming_info)
313
+
307
314
  deployability_index = deployability_index or DeployabilityIndex.all_deployable()
308
315
  with self.concurrent_context():
309
316
  concurrent_apply_to_snapshots(
@@ -422,7 +429,9 @@ class SnapshotEvaluator:
422
429
  target_snapshots: Target snapshots.
423
430
  deployability_index: Determines snapshots that are deployable / representative in the context of this creation.
424
431
  """
425
- existing_data_objects = self._get_data_objects(target_snapshots, deployability_index)
432
+ existing_data_objects = self._get_physical_data_objects(
433
+ target_snapshots, deployability_index
434
+ )
426
435
  snapshots_to_create = []
427
436
  for snapshot in target_snapshots:
428
437
  if not snapshot.is_model or snapshot.is_symbolic:
@@ -479,7 +488,7 @@ class SnapshotEvaluator:
479
488
  deployability_index: Determines snapshots that are deployable in the context of this evaluation.
480
489
  """
481
490
  deployability_index = deployability_index or DeployabilityIndex.all_deployable()
482
- target_data_objects = self._get_data_objects(target_snapshots, deployability_index)
491
+ target_data_objects = self._get_physical_data_objects(target_snapshots, deployability_index)
483
492
  if not target_data_objects:
484
493
  return
485
494
 
@@ -489,15 +498,14 @@ class SnapshotEvaluator:
489
498
  allow_destructive_snapshots = allow_destructive_snapshots or set()
490
499
  allow_additive_snapshots = allow_additive_snapshots or set()
491
500
  snapshots_by_name = {s.name: s for s in snapshots.values()}
492
- snapshots_with_data_objects = [snapshots[s_id] for s_id in target_data_objects]
493
501
  with self.concurrent_context():
494
502
  # Only migrate snapshots for which there's an existing data object
495
503
  concurrent_apply_to_snapshots(
496
- snapshots_with_data_objects,
504
+ target_snapshots,
497
505
  lambda s: self._migrate_snapshot(
498
506
  s,
499
507
  snapshots_by_name,
500
- target_data_objects[s.snapshot_id],
508
+ target_data_objects.get(s.snapshot_id),
501
509
  allow_destructive_snapshots,
502
510
  allow_additive_snapshots,
503
511
  self.get_adapter(s.model_gateway),
@@ -517,10 +525,12 @@ class SnapshotEvaluator:
517
525
  target_snapshots: Snapshots to cleanup.
518
526
  on_complete: A callback to call on each successfully deleted database object.
519
527
  """
528
+ target_snapshots = [
529
+ t for t in target_snapshots if t.snapshot.is_model and not t.snapshot.is_symbolic
530
+ ]
520
531
  snapshots_to_dev_table_only = {
521
532
  t.snapshot.snapshot_id: t.dev_table_only for t in target_snapshots
522
533
  }
523
-
524
534
  with self.concurrent_context():
525
535
  concurrent_apply_to_snapshots(
526
536
  [t.snapshot for t in target_snapshots],
@@ -740,38 +750,51 @@ class SnapshotEvaluator:
740
750
  **render_statements_kwargs
741
751
  )
742
752
 
753
+ evaluation_strategy = _evaluation_strategy(snapshot, adapter)
754
+ evaluation_strategy.run_pre_statements(
755
+ snapshot=snapshot,
756
+ render_kwargs={**render_statements_kwargs, "inside_transaction": False},
757
+ )
758
+
743
759
  with (
744
760
  adapter.transaction(),
745
761
  adapter.session(snapshot.model.render_session_properties(**render_statements_kwargs)),
746
762
  ):
747
- adapter.execute(model.render_pre_statements(**render_statements_kwargs))
763
+ evaluation_strategy.run_pre_statements(
764
+ snapshot=snapshot,
765
+ render_kwargs={**render_statements_kwargs, "inside_transaction": True},
766
+ )
748
767
 
749
768
  if not target_table_exists or (model.is_seed and not snapshot.intervals):
750
- columns_to_types_provided = (
769
+ # Only create the empty table if the columns were provided explicitly by the user
770
+ should_create_empty_table = (
751
771
  model.kind.is_materialized
752
772
  and model.columns_to_types_
753
773
  and columns_to_types_all_known(model.columns_to_types_)
754
774
  )
775
+ if not should_create_empty_table:
776
+ # Or if the model is self-referential and its query is fully annotated with types
777
+ should_create_empty_table = model.depends_on_self and model.annotated
755
778
  if self._can_clone(snapshot, deployability_index):
756
779
  self._clone_snapshot_in_dev(
757
780
  snapshot=snapshot,
758
781
  snapshots=snapshots,
759
782
  deployability_index=deployability_index,
760
783
  render_kwargs=create_render_kwargs,
761
- rendered_physical_properties=rendered_physical_properties,
784
+ rendered_physical_properties=rendered_physical_properties.copy(),
762
785
  allow_destructive_snapshots=allow_destructive_snapshots,
763
786
  allow_additive_snapshots=allow_additive_snapshots,
764
787
  )
765
788
  runtime_stage = RuntimeStage.EVALUATING
766
789
  target_table_exists = True
767
- elif columns_to_types_provided or model.is_seed or model.kind.is_scd_type_2:
790
+ elif should_create_empty_table or model.is_seed or model.kind.is_scd_type_2:
768
791
  self._execute_create(
769
792
  snapshot=snapshot,
770
793
  table_name=target_table_name,
771
794
  is_table_deployable=is_snapshot_deployable,
772
795
  deployability_index=deployability_index,
773
796
  create_render_kwargs=create_render_kwargs,
774
- rendered_physical_properties=rendered_physical_properties,
797
+ rendered_physical_properties=rendered_physical_properties.copy(),
775
798
  dry_run=False,
776
799
  run_pre_post_statements=False,
777
800
  )
@@ -788,6 +811,7 @@ class SnapshotEvaluator:
788
811
  if (
789
812
  snapshot.is_materialized
790
813
  and target_table_exists
814
+ and adapter.wap_enabled
791
815
  and (model.wap_supported or adapter.wap_supported(target_table_name))
792
816
  ):
793
817
  wap_id = random_id()[0:8]
@@ -809,9 +833,17 @@ class SnapshotEvaluator:
809
833
  batch_index=batch_index,
810
834
  )
811
835
 
812
- adapter.execute(model.render_post_statements(**render_statements_kwargs))
836
+ evaluation_strategy.run_post_statements(
837
+ snapshot=snapshot,
838
+ render_kwargs={**render_statements_kwargs, "inside_transaction": True},
839
+ )
813
840
 
814
- return wap_id
841
+ evaluation_strategy.run_post_statements(
842
+ snapshot=snapshot,
843
+ render_kwargs={**render_statements_kwargs, "inside_transaction": False},
844
+ )
845
+
846
+ return wap_id
815
847
 
816
848
  def create_snapshot(
817
849
  self,
@@ -845,6 +877,11 @@ class SnapshotEvaluator:
845
877
  deployability_index=deployability_index,
846
878
  )
847
879
 
880
+ evaluation_strategy = _evaluation_strategy(snapshot, adapter)
881
+ evaluation_strategy.run_pre_statements(
882
+ snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False}
883
+ )
884
+
848
885
  with (
849
886
  adapter.transaction(),
850
887
  adapter.session(snapshot.model.render_session_properties(**create_render_kwargs)),
@@ -862,6 +899,7 @@ class SnapshotEvaluator:
862
899
  rendered_physical_properties=rendered_physical_properties,
863
900
  allow_destructive_snapshots=allow_destructive_snapshots,
864
901
  allow_additive_snapshots=allow_additive_snapshots,
902
+ run_pre_post_statements=True,
865
903
  )
866
904
  else:
867
905
  is_table_deployable = deployability_index.is_deployable(snapshot)
@@ -875,6 +913,10 @@ class SnapshotEvaluator:
875
913
  dry_run=True,
876
914
  )
877
915
 
916
+ evaluation_strategy.run_post_statements(
917
+ snapshot=snapshot, render_kwargs={**create_render_kwargs, "inside_transaction": False}
918
+ )
919
+
878
920
  if on_complete is not None:
879
921
  on_complete(snapshot)
880
922
 
@@ -912,6 +954,7 @@ class SnapshotEvaluator:
912
954
  model = snapshot.model
913
955
  adapter = self.get_adapter(model.gateway)
914
956
  evaluation_strategy = _evaluation_strategy(snapshot, adapter)
957
+ is_snapshot_deployable = deployability_index.is_deployable(snapshot)
915
958
 
916
959
  queries_or_dfs = self._render_snapshot_for_evaluation(
917
960
  snapshot,
@@ -935,6 +978,7 @@ class SnapshotEvaluator:
935
978
  execution_time=execution_time,
936
979
  physical_properties=rendered_physical_properties,
937
980
  render_kwargs=create_render_kwargs,
981
+ is_snapshot_deployable=is_snapshot_deployable,
938
982
  )
939
983
  else:
940
984
  logger.info(
@@ -957,6 +1001,7 @@ class SnapshotEvaluator:
957
1001
  execution_time=execution_time,
958
1002
  physical_properties=rendered_physical_properties,
959
1003
  render_kwargs=create_render_kwargs,
1004
+ is_snapshot_deployable=is_snapshot_deployable,
960
1005
  )
961
1006
 
962
1007
  # DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result,
@@ -976,6 +1021,11 @@ class SnapshotEvaluator:
976
1021
  ):
977
1022
  import pandas as pd
978
1023
 
1024
+ try:
1025
+ first_query_or_df = next(queries_or_dfs)
1026
+ except StopIteration:
1027
+ return
1028
+
979
1029
  query_or_df = reduce(
980
1030
  lambda a, b: (
981
1031
  pd.concat([a, b], ignore_index=True) # type: ignore
@@ -983,6 +1033,7 @@ class SnapshotEvaluator:
983
1033
  else a.union_all(b) # type: ignore
984
1034
  ), # type: ignore
985
1035
  queries_or_dfs,
1036
+ first_query_or_df,
986
1037
  )
987
1038
  apply(query_or_df, index=0)
988
1039
  else:
@@ -1021,6 +1072,7 @@ class SnapshotEvaluator:
1021
1072
  rendered_physical_properties: t.Dict[str, exp.Expression],
1022
1073
  allow_destructive_snapshots: t.Set[str],
1023
1074
  allow_additive_snapshots: t.Set[str],
1075
+ run_pre_post_statements: bool = False,
1024
1076
  ) -> None:
1025
1077
  adapter = self.get_adapter(snapshot.model.gateway)
1026
1078
 
@@ -1032,7 +1084,6 @@ class SnapshotEvaluator:
1032
1084
  adapter.clone_table(
1033
1085
  target_table_name,
1034
1086
  snapshot.table_name(),
1035
- replace=True,
1036
1087
  rendered_physical_properties=rendered_physical_properties,
1037
1088
  )
1038
1089
  self._migrate_target_table(
@@ -1044,7 +1095,9 @@ class SnapshotEvaluator:
1044
1095
  rendered_physical_properties=rendered_physical_properties,
1045
1096
  allow_destructive_snapshots=allow_destructive_snapshots,
1046
1097
  allow_additive_snapshots=allow_additive_snapshots,
1098
+ run_pre_post_statements=run_pre_post_statements,
1047
1099
  )
1100
+
1048
1101
  except Exception:
1049
1102
  adapter.drop_table(target_table_name)
1050
1103
  raise
@@ -1059,7 +1112,7 @@ class SnapshotEvaluator:
1059
1112
  adapter: EngineAdapter,
1060
1113
  deployability_index: DeployabilityIndex,
1061
1114
  ) -> None:
1062
- if not snapshot.requires_schema_migration_in_prod:
1115
+ if not snapshot.is_model or snapshot.is_symbolic:
1063
1116
  return
1064
1117
 
1065
1118
  deployability_index = DeployabilityIndex.all_deployable()
@@ -1071,6 +1124,11 @@ class SnapshotEvaluator:
1071
1124
  )
1072
1125
  target_table_name = snapshot.table_name()
1073
1126
 
1127
+ evaluation_strategy = _evaluation_strategy(snapshot, adapter)
1128
+ evaluation_strategy.run_pre_statements(
1129
+ snapshot=snapshot, render_kwargs={**render_kwargs, "inside_transaction": False}
1130
+ )
1131
+
1074
1132
  with (
1075
1133
  adapter.transaction(),
1076
1134
  adapter.session(snapshot.model.render_session_properties(**render_kwargs)),
@@ -1081,6 +1139,10 @@ class SnapshotEvaluator:
1081
1139
  ):
1082
1140
  table_exists = False
1083
1141
 
1142
+ rendered_physical_properties = snapshot.model.render_physical_properties(
1143
+ **render_kwargs
1144
+ )
1145
+
1084
1146
  if table_exists:
1085
1147
  self._migrate_target_table(
1086
1148
  target_table_name=target_table_name,
@@ -1088,14 +1150,35 @@ class SnapshotEvaluator:
1088
1150
  snapshots=snapshots,
1089
1151
  deployability_index=deployability_index,
1090
1152
  render_kwargs=render_kwargs,
1091
- rendered_physical_properties=snapshot.model.render_physical_properties(
1092
- **render_kwargs
1093
- ),
1153
+ rendered_physical_properties=rendered_physical_properties,
1094
1154
  allow_destructive_snapshots=allow_destructive_snapshots,
1095
1155
  allow_additive_snapshots=allow_additive_snapshots,
1096
1156
  run_pre_post_statements=True,
1097
1157
  )
1158
+ else:
1159
+ self._execute_create(
1160
+ snapshot=snapshot,
1161
+ table_name=snapshot.table_name(is_deployable=True),
1162
+ is_table_deployable=True,
1163
+ deployability_index=deployability_index,
1164
+ create_render_kwargs=render_kwargs,
1165
+ rendered_physical_properties=rendered_physical_properties,
1166
+ dry_run=True,
1167
+ )
1098
1168
 
1169
+ evaluation_strategy.run_post_statements(
1170
+ snapshot=snapshot, render_kwargs={**render_kwargs, "inside_transaction": False}
1171
+ )
1172
+
1173
+ # Retry in case when the table is migrated concurrently from another plan application
1174
+ @retry(
1175
+ reraise=True,
1176
+ stop=stop_after_attempt(5),
1177
+ wait=wait_exponential(min=1, max=16),
1178
+ retry=retry_if_not_exception_type(
1179
+ (DestructiveChangeError, AdditiveChangeError, MigrationNotSupportedError)
1180
+ ),
1181
+ )
1099
1182
  def _migrate_target_table(
1100
1183
  self,
1101
1184
  target_table_name: str,
@@ -1110,7 +1193,10 @@ class SnapshotEvaluator:
1110
1193
  ) -> None:
1111
1194
  adapter = self.get_adapter(snapshot.model.gateway)
1112
1195
 
1113
- tmp_table_name = f"{target_table_name}_schema_tmp"
1196
+ tmp_table = exp.to_table(target_table_name)
1197
+ tmp_table.this.set("this", f"{tmp_table.name}_schema_tmp")
1198
+ tmp_table_name = tmp_table.sql()
1199
+
1114
1200
  if snapshot.is_materialized:
1115
1201
  self._execute_create(
1116
1202
  snapshot=snapshot,
@@ -1121,6 +1207,7 @@ class SnapshotEvaluator:
1121
1207
  rendered_physical_properties=rendered_physical_properties,
1122
1208
  dry_run=False,
1123
1209
  run_pre_post_statements=run_pre_post_statements,
1210
+ skip_grants=True, # skip grants for tmp table
1124
1211
  )
1125
1212
  try:
1126
1213
  evaluation_strategy = _evaluation_strategy(snapshot, adapter)
@@ -1138,6 +1225,7 @@ class SnapshotEvaluator:
1138
1225
  allow_additive_snapshots=allow_additive_snapshots,
1139
1226
  ignore_destructive=snapshot.model.on_destructive_change.is_ignore,
1140
1227
  ignore_additive=snapshot.model.on_additive_change.is_ignore,
1228
+ deployability_index=deployability_index,
1141
1229
  )
1142
1230
  finally:
1143
1231
  if snapshot.is_materialized:
@@ -1187,6 +1275,7 @@ class SnapshotEvaluator:
1187
1275
  model=snapshot.model,
1188
1276
  environment=environment_naming_info.name,
1189
1277
  snapshots=snapshots,
1278
+ snapshot=snapshot,
1190
1279
  **render_kwargs,
1191
1280
  )
1192
1281
 
@@ -1386,6 +1475,7 @@ class SnapshotEvaluator:
1386
1475
  rendered_physical_properties: t.Dict[str, exp.Expression],
1387
1476
  dry_run: bool,
1388
1477
  run_pre_post_statements: bool = True,
1478
+ skip_grants: bool = False,
1389
1479
  ) -> None:
1390
1480
  adapter = self.get_adapter(snapshot.model.gateway)
1391
1481
  evaluation_strategy = _evaluation_strategy(snapshot, adapter)
@@ -1399,19 +1489,28 @@ class SnapshotEvaluator:
1399
1489
  "table_mapping": {snapshot.name: table_name},
1400
1490
  }
1401
1491
  if run_pre_post_statements:
1402
- adapter.execute(snapshot.model.render_pre_statements(**create_render_kwargs))
1492
+ evaluation_strategy.run_pre_statements(
1493
+ snapshot=snapshot,
1494
+ render_kwargs={**create_render_kwargs, "inside_transaction": True},
1495
+ )
1403
1496
  evaluation_strategy.create(
1404
1497
  table_name=table_name,
1405
1498
  model=snapshot.model,
1406
1499
  is_table_deployable=is_table_deployable,
1500
+ skip_grants=skip_grants,
1407
1501
  render_kwargs=create_render_kwargs,
1408
1502
  is_snapshot_deployable=is_snapshot_deployable,
1409
1503
  is_snapshot_representative=is_snapshot_representative,
1410
1504
  dry_run=dry_run,
1411
1505
  physical_properties=rendered_physical_properties,
1506
+ snapshot=snapshot,
1507
+ deployability_index=deployability_index,
1412
1508
  )
1413
1509
  if run_pre_post_statements:
1414
- adapter.execute(snapshot.model.render_post_statements(**create_render_kwargs))
1510
+ evaluation_strategy.run_post_statements(
1511
+ snapshot=snapshot,
1512
+ render_kwargs={**create_render_kwargs, "inside_transaction": True},
1513
+ )
1415
1514
 
1416
1515
  def _can_clone(self, snapshot: Snapshot, deployability_index: DeployabilityIndex) -> bool:
1417
1516
  adapter = self.get_adapter(snapshot.model.gateway)
@@ -1420,13 +1519,15 @@ class SnapshotEvaluator:
1420
1519
  and snapshot.is_materialized
1421
1520
  and bool(snapshot.previous_versions)
1422
1521
  and adapter.SUPPORTS_CLONING
1423
- # managed models cannot have their schema mutated because theyre based on queries, so clone + alter wont work
1522
+ # managed models cannot have their schema mutated because they're based on queries, so clone + alter won't work
1424
1523
  and not snapshot.is_managed
1425
- # If the deployable table is missing we can't clone it
1524
+ and not snapshot.is_dbt_custom
1426
1525
  and not deployability_index.is_deployable(snapshot)
1526
+ # If the deployable table is missing we can't clone it
1527
+ and adapter.table_exists(snapshot.table_name())
1427
1528
  )
1428
1529
 
1429
- def _get_data_objects(
1530
+ def _get_physical_data_objects(
1430
1531
  self,
1431
1532
  target_snapshots: t.Iterable[Snapshot],
1432
1533
  deployability_index: DeployabilityIndex,
@@ -1442,18 +1543,70 @@ class SnapshotEvaluator:
1442
1543
  A dictionary of snapshot IDs to existing data objects of their physical tables. If the data object
1443
1544
  for a snapshot is not found, it will not be included in the dictionary.
1444
1545
  """
1546
+ return self._get_data_objects(
1547
+ target_snapshots,
1548
+ lambda s: exp.to_table(
1549
+ s.table_name(deployability_index.is_deployable(s)), dialect=s.model.dialect
1550
+ ),
1551
+ )
1552
+
1553
+ def _get_virtual_data_objects(
1554
+ self,
1555
+ target_snapshots: t.Iterable[Snapshot],
1556
+ environment_naming_info: EnvironmentNamingInfo,
1557
+ ) -> t.Dict[SnapshotId, DataObject]:
1558
+ """Returns a dictionary of snapshot IDs to existing data objects of their virtual views.
1559
+
1560
+ Args:
1561
+ target_snapshots: Target snapshots.
1562
+ environment_naming_info: The environment naming info of the target virtual environment.
1563
+
1564
+ Returns:
1565
+ A dictionary of snapshot IDs to existing data objects of their virtual views. If the data object
1566
+ for a snapshot is not found, it will not be included in the dictionary.
1567
+ """
1568
+
1569
+ def _get_view_name(s: Snapshot) -> exp.Table:
1570
+ adapter = (
1571
+ self.get_adapter(s.model_gateway)
1572
+ if environment_naming_info.gateway_managed
1573
+ else self.adapter
1574
+ )
1575
+ return exp.to_table(
1576
+ s.qualified_view_name.for_environment(
1577
+ environment_naming_info, dialect=adapter.dialect
1578
+ ),
1579
+ dialect=adapter.dialect,
1580
+ )
1581
+
1582
+ return self._get_data_objects(target_snapshots, _get_view_name)
1583
+
1584
+ def _get_data_objects(
1585
+ self,
1586
+ target_snapshots: t.Iterable[Snapshot],
1587
+ table_name_callable: t.Callable[[Snapshot], exp.Table],
1588
+ ) -> t.Dict[SnapshotId, DataObject]:
1589
+ """Returns a dictionary of snapshot IDs to existing data objects.
1590
+
1591
+ Args:
1592
+ target_snapshots: Target snapshots.
1593
+ table_name_callable: A function that takes a snapshot and returns the table to look for.
1594
+
1595
+ Returns:
1596
+ A dictionary of snapshot IDs to existing data objects. If the data object for a snapshot is not found,
1597
+ it will not be included in the dictionary.
1598
+ """
1445
1599
  tables_by_gateway_and_schema: t.Dict[t.Union[str, None], t.Dict[exp.Table, set[str]]] = (
1446
1600
  defaultdict(lambda: defaultdict(set))
1447
1601
  )
1448
- snapshots_by_table_name: t.Dict[str, Snapshot] = {}
1602
+ snapshots_by_table_name: t.Dict[exp.Table, t.Dict[str, Snapshot]] = defaultdict(dict)
1449
1603
  for snapshot in target_snapshots:
1450
1604
  if not snapshot.is_model or snapshot.is_symbolic:
1451
1605
  continue
1452
- is_deployable = deployability_index.is_deployable(snapshot)
1453
- table = exp.to_table(snapshot.table_name(is_deployable), dialect=snapshot.model.dialect)
1606
+ table = table_name_callable(snapshot)
1454
1607
  table_schema = d.schema_(table.db, catalog=table.catalog)
1455
1608
  tables_by_gateway_and_schema[snapshot.model_gateway][table_schema].add(table.name)
1456
- snapshots_by_table_name[table.name] = snapshot
1609
+ snapshots_by_table_name[table_schema][table.name] = snapshot
1457
1610
 
1458
1611
  def _get_data_objects_in_schema(
1459
1612
  schema: exp.Table,
@@ -1461,26 +1614,30 @@ class SnapshotEvaluator:
1461
1614
  gateway: t.Optional[str] = None,
1462
1615
  ) -> t.List[DataObject]:
1463
1616
  logger.info("Listing data objects in schema %s", schema.sql())
1464
- return self.get_adapter(gateway).get_data_objects(schema, object_names)
1617
+ return self.get_adapter(gateway).get_data_objects(
1618
+ schema, object_names, safe_to_cache=True
1619
+ )
1465
1620
 
1466
1621
  with self.concurrent_context():
1467
- existing_objects: t.List[DataObject] = []
1622
+ snapshot_id_to_obj: t.Dict[SnapshotId, DataObject] = {}
1468
1623
  # A schema can be shared across multiple engines, so we need to group tables by both gateway and schema
1469
1624
  for gateway, tables_by_schema in tables_by_gateway_and_schema.items():
1470
- objs_for_gateway = [
1471
- obj
1472
- for objs in concurrent_apply_to_values(
1473
- list(tables_by_schema),
1474
- lambda s: _get_data_objects_in_schema(
1475
- schema=s, object_names=tables_by_schema.get(s), gateway=gateway
1476
- ),
1477
- self.ddl_concurrent_tasks,
1478
- )
1479
- for obj in objs
1480
- ]
1481
- existing_objects.extend(objs_for_gateway)
1625
+ schema_list = list(tables_by_schema.keys())
1626
+ results = concurrent_apply_to_values(
1627
+ schema_list,
1628
+ lambda s: _get_data_objects_in_schema(
1629
+ schema=s, object_names=tables_by_schema.get(s), gateway=gateway
1630
+ ),
1631
+ self.ddl_concurrent_tasks,
1632
+ )
1482
1633
 
1483
- return {snapshots_by_table_name[obj.name].snapshot_id: obj for obj in existing_objects}
1634
+ for schema, objs in zip(schema_list, results):
1635
+ snapshots_by_name = snapshots_by_table_name.get(schema, {})
1636
+ for obj in objs:
1637
+ if obj.name in snapshots_by_name:
1638
+ snapshot_id_to_obj[snapshots_by_name[obj.name].snapshot_id] = obj
1639
+
1640
+ return snapshot_id_to_obj
1484
1641
 
1485
1642
 
1486
1643
  def _evaluation_strategy(snapshot: SnapshotInfoLike, adapter: EngineAdapter) -> EvaluationStrategy:
@@ -1505,6 +1662,19 @@ def _evaluation_strategy(snapshot: SnapshotInfoLike, adapter: EngineAdapter) ->
1505
1662
  klass = ViewStrategy
1506
1663
  elif snapshot.is_scd_type_2:
1507
1664
  klass = SCDType2Strategy
1665
+ elif snapshot.is_dbt_custom:
1666
+ if hasattr(snapshot, "model") and isinstance(
1667
+ (model_kind := snapshot.model.kind), DbtCustomKind
1668
+ ):
1669
+ return DbtCustomMaterializationStrategy(
1670
+ adapter=adapter,
1671
+ materialization_name=model_kind.materialization,
1672
+ materialization_template=model_kind.definition,
1673
+ )
1674
+
1675
+ raise SQLMeshError(
1676
+ f"Expected DbtCustomKind for dbt custom materialization in model '{snapshot.name}'"
1677
+ )
1508
1678
  elif snapshot.is_custom:
1509
1679
  if snapshot.custom_materialization is None:
1510
1680
  raise SQLMeshError(
@@ -1572,6 +1742,7 @@ class EvaluationStrategy(abc.ABC):
1572
1742
  model: Model,
1573
1743
  is_table_deployable: bool,
1574
1744
  render_kwargs: t.Dict[str, t.Any],
1745
+ skip_grants: bool,
1575
1746
  **kwargs: t.Any,
1576
1747
  ) -> None:
1577
1748
  """Creates the target table or view.
@@ -1644,6 +1815,84 @@ class EvaluationStrategy(abc.ABC):
1644
1815
  view_name: The name of the target view in the virtual layer.
1645
1816
  """
1646
1817
 
1818
+ @abc.abstractmethod
1819
+ def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
1820
+ """Executes the snapshot's pre statements.
1821
+
1822
+ Args:
1823
+ snapshot: The target snapshot.
1824
+ render_kwargs: Additional key-value arguments to pass when rendering the statements.
1825
+ """
1826
+
1827
+ @abc.abstractmethod
1828
+ def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
1829
+ """Executes the snapshot's post statements.
1830
+
1831
+ Args:
1832
+ snapshot: The target snapshot.
1833
+ render_kwargs: Additional key-value arguments to pass when rendering the statements.
1834
+ """
1835
+
1836
+ def _apply_grants(
1837
+ self,
1838
+ model: Model,
1839
+ table_name: str,
1840
+ target_layer: GrantsTargetLayer,
1841
+ is_snapshot_deployable: bool = False,
1842
+ ) -> None:
1843
+ """Apply grants for a model if grants are configured.
1844
+
1845
+ This method provides consistent grants application across all evaluation strategies.
1846
+ It ensures that whenever a physical database object (table, view, materialized view)
1847
+ is created or modified, the appropriate grants are applied.
1848
+
1849
+ Args:
1850
+ model: The SQLMesh model containing grants configuration
1851
+ table_name: The target table/view name to apply grants to
1852
+ target_layer: The grants application layer (physical or virtual)
1853
+ is_snapshot_deployable: Whether the snapshot is deployable (targeting production)
1854
+ """
1855
+ grants_config = model.grants
1856
+ if grants_config is None:
1857
+ return
1858
+
1859
+ if not self.adapter.SUPPORTS_GRANTS:
1860
+ logger.warning(
1861
+ f"Engine {self.adapter.__class__.__name__} does not support grants. "
1862
+ f"Skipping grants application for model {model.name}"
1863
+ )
1864
+ return
1865
+
1866
+ model_grants_target_layer = model.grants_target_layer
1867
+ deployable_vde_dev_only = (
1868
+ is_snapshot_deployable and model.virtual_environment_mode.is_dev_only
1869
+ )
1870
+
1871
+ # table_type is always a VIEW in the virtual layer unless model is deployable and VDE is dev_only
1872
+ # in which case we fall back to the model's model_grants_table_type
1873
+ if target_layer == GrantsTargetLayer.VIRTUAL and not deployable_vde_dev_only:
1874
+ model_grants_table_type = DataObjectType.VIEW
1875
+ else:
1876
+ model_grants_table_type = model.grants_table_type
1877
+
1878
+ if (
1879
+ model_grants_target_layer.is_all
1880
+ or model_grants_target_layer == target_layer
1881
+ # Always apply grants in production when VDE is dev_only regardless of target_layer
1882
+ # since only physical tables are created in production
1883
+ or deployable_vde_dev_only
1884
+ ):
1885
+ logger.info(f"Applying grants for model {model.name} to table {table_name}")
1886
+ self.adapter.sync_grants_config(
1887
+ exp.to_table(table_name, dialect=self.adapter.dialect),
1888
+ grants_config,
1889
+ model_grants_table_type,
1890
+ )
1891
+ else:
1892
+ logger.debug(
1893
+ f"Skipping grants application for model {model.name} in {target_layer} layer"
1894
+ )
1895
+
1647
1896
 
1648
1897
  class SymbolicStrategy(EvaluationStrategy):
1649
1898
  def insert(
@@ -1673,6 +1922,7 @@ class SymbolicStrategy(EvaluationStrategy):
1673
1922
  model: Model,
1674
1923
  is_table_deployable: bool,
1675
1924
  render_kwargs: t.Dict[str, t.Any],
1925
+ skip_grants: bool,
1676
1926
  **kwargs: t.Any,
1677
1927
  ) -> None:
1678
1928
  pass
@@ -1705,6 +1955,12 @@ class SymbolicStrategy(EvaluationStrategy):
1705
1955
  def demote(self, view_name: str, **kwargs: t.Any) -> None:
1706
1956
  pass
1707
1957
 
1958
+ def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None:
1959
+ pass
1960
+
1961
+ def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Dict[str, t.Any]) -> None:
1962
+ pass
1963
+
1708
1964
 
1709
1965
  class EmbeddedStrategy(SymbolicStrategy):
1710
1966
  def promote(
@@ -1748,10 +2004,27 @@ class PromotableStrategy(EvaluationStrategy, abc.ABC):
1748
2004
  view_properties=model.render_virtual_properties(**render_kwargs),
1749
2005
  )
1750
2006
 
2007
+ snapshot = kwargs.get("snapshot")
2008
+ deployability_index = kwargs.get("deployability_index")
2009
+ is_snapshot_deployable = (
2010
+ deployability_index.is_deployable(snapshot)
2011
+ if snapshot and deployability_index
2012
+ else False
2013
+ )
2014
+
2015
+ # Apply grants to the virtual layer (view) after promotion
2016
+ self._apply_grants(model, view_name, GrantsTargetLayer.VIRTUAL, is_snapshot_deployable)
2017
+
1751
2018
  def demote(self, view_name: str, **kwargs: t.Any) -> None:
1752
2019
  logger.info("Dropping view '%s'", view_name)
1753
2020
  self.adapter.drop_view(view_name, cascade=False)
1754
2021
 
2022
+ def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
2023
+ self.adapter.execute(snapshot.model.render_pre_statements(**render_kwargs))
2024
+
2025
+ def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
2026
+ self.adapter.execute(snapshot.model.render_post_statements(**render_kwargs))
2027
+
1755
2028
 
1756
2029
  class MaterializableStrategy(PromotableStrategy, abc.ABC):
1757
2030
  def create(
@@ -1760,6 +2033,7 @@ class MaterializableStrategy(PromotableStrategy, abc.ABC):
1760
2033
  model: Model,
1761
2034
  is_table_deployable: bool,
1762
2035
  render_kwargs: t.Dict[str, t.Any],
2036
+ skip_grants: bool,
1763
2037
  **kwargs: t.Any,
1764
2038
  ) -> None:
1765
2039
  ctas_query = model.ctas_query(**render_kwargs)
@@ -1804,6 +2078,13 @@ class MaterializableStrategy(PromotableStrategy, abc.ABC):
1804
2078
  column_descriptions=model.column_descriptions if is_table_deployable else None,
1805
2079
  )
1806
2080
 
2081
+ # Apply grants after table creation (unless explicitly skipped by caller)
2082
+ if not skip_grants:
2083
+ is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2084
+ self._apply_grants(
2085
+ model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2086
+ )
2087
+
1807
2088
  def migrate(
1808
2089
  self,
1809
2090
  target_table_name: str,
@@ -1829,6 +2110,15 @@ class MaterializableStrategy(PromotableStrategy, abc.ABC):
1829
2110
  )
1830
2111
  self.adapter.alter_table(alter_operations)
1831
2112
 
2113
+ # Apply grants after schema migration
2114
+ deployability_index = kwargs.get("deployability_index")
2115
+ is_snapshot_deployable = (
2116
+ deployability_index.is_deployable(snapshot) if deployability_index else False
2117
+ )
2118
+ self._apply_grants(
2119
+ snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2120
+ )
2121
+
1832
2122
  def delete(self, name: str, **kwargs: t.Any) -> None:
1833
2123
  _check_table_db_is_physical_schema(name, kwargs["physical_schema"])
1834
2124
  self.adapter.drop_table(name, cascade=kwargs.pop("cascade", False))
@@ -1840,6 +2130,7 @@ class MaterializableStrategy(PromotableStrategy, abc.ABC):
1840
2130
  name: str,
1841
2131
  query_or_df: QueryOrDF,
1842
2132
  render_kwargs: t.Dict[str, t.Any],
2133
+ skip_grants: bool = False,
1843
2134
  **kwargs: t.Any,
1844
2135
  ) -> None:
1845
2136
  """Replaces the table for the given model.
@@ -1876,6 +2167,11 @@ class MaterializableStrategy(PromotableStrategy, abc.ABC):
1876
2167
  source_columns=source_columns,
1877
2168
  )
1878
2169
 
2170
+ # Apply grants after table replacement (unless explicitly skipped by caller)
2171
+ if not skip_grants:
2172
+ is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2173
+ self._apply_grants(model, name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
2174
+
1879
2175
  def _get_target_and_source_columns(
1880
2176
  self,
1881
2177
  model: Model,
@@ -1897,7 +2193,13 @@ class MaterializableStrategy(PromotableStrategy, abc.ABC):
1897
2193
  if model.on_destructive_change.is_ignore or model.on_additive_change.is_ignore:
1898
2194
  # We need to identify the columns that are only in the source so we create an empty table with
1899
2195
  # the user query to determine that
1900
- with self.adapter.temp_table(model.ctas_query(**render_kwargs)) as temp_table:
2196
+ temp_table_name = exp.table_(
2197
+ "diff",
2198
+ db=model.physical_schema,
2199
+ )
2200
+ with self.adapter.temp_table(
2201
+ model.ctas_query(**render_kwargs), name=temp_table_name
2202
+ ) as temp_table:
1901
2203
  source_columns = list(self.adapter.columns(temp_table))
1902
2204
  else:
1903
2205
  source_columns = None
@@ -2123,6 +2425,7 @@ class SeedStrategy(MaterializableStrategy):
2123
2425
  model: Model,
2124
2426
  is_table_deployable: bool,
2125
2427
  render_kwargs: t.Dict[str, t.Any],
2428
+ skip_grants: bool,
2126
2429
  **kwargs: t.Any,
2127
2430
  ) -> None:
2128
2431
  model = t.cast(SeedModel, model)
@@ -2136,20 +2439,53 @@ class SeedStrategy(MaterializableStrategy):
2136
2439
  )
2137
2440
  return
2138
2441
 
2139
- super().create(table_name, model, is_table_deployable, render_kwargs, **kwargs)
2442
+ super().create(
2443
+ table_name,
2444
+ model,
2445
+ is_table_deployable,
2446
+ render_kwargs,
2447
+ skip_grants=True, # Skip grants; they're applied after data insertion
2448
+ **kwargs,
2449
+ )
2140
2450
  # For seeds we insert data at the time of table creation.
2141
2451
  try:
2142
2452
  for index, df in enumerate(model.render_seed()):
2143
2453
  if index == 0:
2144
- self._replace_query_for_model(model, table_name, df, render_kwargs, **kwargs)
2454
+ self._replace_query_for_model(
2455
+ model,
2456
+ table_name,
2457
+ df,
2458
+ render_kwargs,
2459
+ skip_grants=True, # Skip grants; they're applied after data insertion
2460
+ **kwargs,
2461
+ )
2145
2462
  else:
2146
2463
  self.adapter.insert_append(
2147
2464
  table_name, df, target_columns_to_types=model.columns_to_types
2148
2465
  )
2466
+
2467
+ if not skip_grants:
2468
+ # Apply grants after seed table creation and data insertion
2469
+ is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2470
+ self._apply_grants(
2471
+ model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2472
+ )
2149
2473
  except Exception:
2150
2474
  self.adapter.drop_table(table_name)
2151
2475
  raise
2152
2476
 
2477
+ def migrate(
2478
+ self,
2479
+ target_table_name: str,
2480
+ source_table_name: str,
2481
+ snapshot: Snapshot,
2482
+ *,
2483
+ ignore_destructive: bool,
2484
+ ignore_additive: bool,
2485
+ **kwargs: t.Any,
2486
+ ) -> None:
2487
+ raise NotImplementedError("Seeds do not support migrations.")
2488
+
2153
2489
  def insert(
2154
2490
  self,
2155
2491
  table_name: str,
@@ -2181,6 +2517,7 @@ class SCDType2Strategy(IncrementalStrategy):
2181
2517
  model: Model,
2182
2518
  is_table_deployable: bool,
2183
2519
  render_kwargs: t.Dict[str, t.Any],
2520
+ skip_grants: bool,
2184
2521
  **kwargs: t.Any,
2185
2522
  ) -> None:
2186
2523
  assert isinstance(model.kind, (SCDType2ByTimeKind, SCDType2ByColumnKind))
@@ -2210,9 +2547,17 @@ class SCDType2Strategy(IncrementalStrategy):
2210
2547
  model,
2211
2548
  is_table_deployable,
2212
2549
  render_kwargs,
2550
+ skip_grants,
2213
2551
  **kwargs,
2214
2552
  )
2215
2553
 
2554
+ if not skip_grants:
2555
+ # Apply grants after SCD Type 2 table creation
2556
+ is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2557
+ self._apply_grants(
2558
+ model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2559
+ )
2560
+
2216
2561
  def insert(
2217
2562
  self,
2218
2563
  table_name: str,
@@ -2280,6 +2625,10 @@ class SCDType2Strategy(IncrementalStrategy):
2280
2625
  f"Unexpected SCD Type 2 kind: {model.kind}. This is not expected and please report this as a bug."
2281
2626
  )
2282
2627
 
2628
+ # Apply grants after SCD Type 2 table recreation
2629
+ is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2630
+ self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
2631
+
2283
2632
  def append(
2284
2633
  self,
2285
2634
  table_name: str,
@@ -2336,6 +2685,10 @@ class ViewStrategy(PromotableStrategy):
2336
2685
  column_descriptions=model.column_descriptions,
2337
2686
  )
2338
2687
 
2688
+ # Apply grants after view creation / replacement
2689
+ is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2690
+ self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
2691
+
2339
2692
  def append(
2340
2693
  self,
2341
2694
  table_name: str,
@@ -2352,12 +2705,21 @@ class ViewStrategy(PromotableStrategy):
2352
2705
  model: Model,
2353
2706
  is_table_deployable: bool,
2354
2707
  render_kwargs: t.Dict[str, t.Any],
2708
+ skip_grants: bool,
2355
2709
  **kwargs: t.Any,
2356
2710
  ) -> None:
2711
+ is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2712
+
2357
2713
  if self.adapter.table_exists(table_name):
2358
2714
  # Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late
2359
2715
  # binding support (because of DROP CASCADE).
2360
2716
  logger.info("View '%s' already exists", table_name)
2717
+
2718
+ if not skip_grants:
2719
+ # Always apply grants when present, even if view exists, to handle grants updates
2720
+ self._apply_grants(
2721
+ model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2722
+ )
2361
2723
  return
2362
2724
 
2363
2725
  logger.info("Creating view '%s'", table_name)
@@ -2381,6 +2743,12 @@ class ViewStrategy(PromotableStrategy):
2381
2743
  column_descriptions=model.column_descriptions if is_table_deployable else None,
2382
2744
  )
2383
2745
 
2746
+ if not skip_grants:
2747
+ # Apply grants after view creation
2748
+ self._apply_grants(
2749
+ model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2750
+ )
2751
+
2384
2752
  def migrate(
2385
2753
  self,
2386
2754
  target_table_name: str,
@@ -2407,6 +2775,15 @@ class ViewStrategy(PromotableStrategy):
2407
2775
  column_descriptions=model.column_descriptions,
2408
2776
  )
2409
2777
 
2778
+ # Apply grants after view migration
2779
+ deployability_index = kwargs.get("deployability_index")
2780
+ is_snapshot_deployable = (
2781
+ deployability_index.is_deployable(snapshot) if deployability_index else False
2782
+ )
2783
+ self._apply_grants(
2784
+ snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2785
+ )
2786
+
2410
2787
  def delete(self, name: str, **kwargs: t.Any) -> None:
2411
2788
  cascade = kwargs.pop("cascade", False)
2412
2789
  try:
@@ -2546,6 +2923,169 @@ def get_custom_materialization_type_or_raise(
2546
2923
  raise SQLMeshError(f"Custom materialization '{name}' not present in the Python environment")
2547
2924
 
2548
2925
 
2926
+ class DbtCustomMaterializationStrategy(MaterializableStrategy):
2927
+ def __init__(
2928
+ self,
2929
+ adapter: EngineAdapter,
2930
+ materialization_name: str,
2931
+ materialization_template: str,
2932
+ ):
2933
+ super().__init__(adapter)
2934
+ self.materialization_name = materialization_name
2935
+ self.materialization_template = materialization_template
2936
+
2937
+ def create(
2938
+ self,
2939
+ table_name: str,
2940
+ model: Model,
2941
+ is_table_deployable: bool,
2942
+ render_kwargs: t.Dict[str, t.Any],
2943
+ skip_grants: bool,
2944
+ **kwargs: t.Any,
2945
+ ) -> None:
2946
+ original_query = model.render_query_or_raise(**render_kwargs)
2947
+ self._execute_materialization(
2948
+ table_name=table_name,
2949
+ query_or_df=original_query.limit(0),
2950
+ model=model,
2951
+ is_first_insert=True,
2952
+ render_kwargs=render_kwargs,
2953
+ create_only=True,
2954
+ **kwargs,
2955
+ )
2956
+
2957
+ # Apply grants after dbt custom materialization table creation
2958
+ if not skip_grants:
2959
+ is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2960
+ self._apply_grants(
2961
+ model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2962
+ )
2963
+
2964
+ def insert(
2965
+ self,
2966
+ table_name: str,
2967
+ query_or_df: QueryOrDF,
2968
+ model: Model,
2969
+ is_first_insert: bool,
2970
+ render_kwargs: t.Dict[str, t.Any],
2971
+ **kwargs: t.Any,
2972
+ ) -> None:
2973
+ self._execute_materialization(
2974
+ table_name=table_name,
2975
+ query_or_df=query_or_df,
2976
+ model=model,
2977
+ is_first_insert=is_first_insert,
2978
+ render_kwargs=render_kwargs,
2979
+ **kwargs,
2980
+ )
2981
+
2982
+ # Apply grants after custom materialization insert (only on first insert)
2983
+ if is_first_insert:
2984
+ is_snapshot_deployable = kwargs.get("is_snapshot_deployable", False)
2985
+ self._apply_grants(
2986
+ model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2987
+ )
2988
+
2989
+ def append(
2990
+ self,
2991
+ table_name: str,
2992
+ query_or_df: QueryOrDF,
2993
+ model: Model,
2994
+ render_kwargs: t.Dict[str, t.Any],
2995
+ **kwargs: t.Any,
2996
+ ) -> None:
2997
+ return self.insert(
2998
+ table_name,
2999
+ query_or_df,
3000
+ model,
3001
+ is_first_insert=False,
3002
+ render_kwargs=render_kwargs,
3003
+ **kwargs,
3004
+ )
3005
+
3006
+ def run_pre_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
3007
+ # in dbt custom materialisations it's up to the user to run the pre hooks inside the transaction
3008
+ if not render_kwargs.get("inside_transaction", True):
3009
+ super().run_pre_statements(
3010
+ snapshot=snapshot,
3011
+ render_kwargs=render_kwargs,
3012
+ )
3013
+
3014
+ def run_post_statements(self, snapshot: Snapshot, render_kwargs: t.Any) -> None:
3015
+ # in dbt custom materialisations it's up to the user to run the post hooks inside the transaction
3016
+ if not render_kwargs.get("inside_transaction", True):
3017
+ super().run_post_statements(
3018
+ snapshot=snapshot,
3019
+ render_kwargs=render_kwargs,
3020
+ )
3021
+
3022
+ def _execute_materialization(
3023
+ self,
3024
+ table_name: str,
3025
+ query_or_df: QueryOrDF,
3026
+ model: Model,
3027
+ is_first_insert: bool,
3028
+ render_kwargs: t.Dict[str, t.Any],
3029
+ create_only: bool = False,
3030
+ **kwargs: t.Any,
3031
+ ) -> None:
3032
+ jinja_macros = model.jinja_macros
3033
+
3034
+ # For vdes we need to use the table, since we don't know the schema/table at parse time
3035
+ parts = exp.to_table(table_name, dialect=self.adapter.dialect)
3036
+
3037
+ existing_globals = jinja_macros.global_objs
3038
+ relation_info = existing_globals.get("this")
3039
+ if isinstance(relation_info, dict):
3040
+ relation_info["database"] = parts.catalog
3041
+ relation_info["identifier"] = parts.name
3042
+ relation_info["name"] = parts.name
3043
+
3044
+ jinja_globals = {
3045
+ **existing_globals,
3046
+ "this": relation_info,
3047
+ "database": parts.catalog,
3048
+ "schema": parts.db,
3049
+ "identifier": parts.name,
3050
+ "target": existing_globals.get("target", {"type": self.adapter.dialect}),
3051
+ "execution_dt": kwargs.get("execution_time"),
3052
+ "engine_adapter": self.adapter,
3053
+ "sql": str(query_or_df),
3054
+ "is_first_insert": is_first_insert,
3055
+ "create_only": create_only,
3056
+ "pre_hooks": [
3057
+ AttributeDict({"sql": s.this.this, "transaction": transaction})
3058
+ for s in model.pre_statements
3059
+ if (transaction := s.args.get("transaction", True))
3060
+ ],
3061
+ "post_hooks": [
3062
+ AttributeDict({"sql": s.this.this, "transaction": transaction})
3063
+ for s in model.post_statements
3064
+ if (transaction := s.args.get("transaction", True))
3065
+ ],
3066
+ "model_instance": model,
3067
+ **kwargs,
3068
+ }
3069
+
3070
+ try:
3071
+ jinja_env = jinja_macros.build_environment(**jinja_globals)
3072
+ template = jinja_env.from_string(self.materialization_template)
3073
+
3074
+ try:
3075
+ template.render()
3076
+ except MacroReturnVal as ret:
3077
+ # this is a successful return from a macro call (dbt uses this list of Relations to update their relation cache)
3078
+ returned_relations = ret.value.get("relations", [])
3079
+ logger.info(
3080
+ f"Materialization {self.materialization_name} returned relations: {returned_relations}"
3081
+ )
3082
+
3083
+ except Exception as e:
3084
+ raise SQLMeshError(
3085
+ f"Failed to execute dbt materialization '{self.materialization_name}': {e}"
3086
+ ) from e
3087
+
3088
+
2549
3089
  class EngineManagedStrategy(MaterializableStrategy):
2550
3090
  def create(
2551
3091
  self,
@@ -2553,6 +3093,7 @@ class EngineManagedStrategy(MaterializableStrategy):
2553
3093
  model: Model,
2554
3094
  is_table_deployable: bool,
2555
3095
  render_kwargs: t.Dict[str, t.Any],
3096
+ skip_grants: bool,
2556
3097
  **kwargs: t.Any,
2557
3098
  ) -> None:
2558
3099
  is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
@@ -2571,6 +3112,13 @@ class EngineManagedStrategy(MaterializableStrategy):
2571
3112
  column_descriptions=model.column_descriptions,
2572
3113
  table_format=model.table_format,
2573
3114
  )
3115
+
3116
+ # Apply grants after managed table creation
3117
+ if not skip_grants:
3118
+ self._apply_grants(
3119
+ model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3120
+ )
3121
+
2574
3122
  elif not is_table_deployable:
2575
3123
  # Only create the dev preview table as a normal table.
2576
3124
  # For the main table, if the snapshot is cant be deployed to prod (eg upstream is forward-only) do nothing.
@@ -2581,6 +3129,7 @@ class EngineManagedStrategy(MaterializableStrategy):
2581
3129
  model=model,
2582
3130
  is_table_deployable=is_table_deployable,
2583
3131
  render_kwargs=render_kwargs,
3132
+ skip_grants=skip_grants,
2584
3133
  **kwargs,
2585
3134
  )
2586
3135
 
@@ -2596,7 +3145,6 @@ class EngineManagedStrategy(MaterializableStrategy):
2596
3145
  deployability_index: DeployabilityIndex = kwargs["deployability_index"]
2597
3146
  snapshot: Snapshot = kwargs["snapshot"]
2598
3147
  is_snapshot_deployable = deployability_index.is_deployable(snapshot)
2599
-
2600
3148
  if is_first_insert and is_snapshot_deployable and not self.adapter.table_exists(table_name):
2601
3149
  self.adapter.create_managed_table(
2602
3150
  table_name=table_name,
@@ -2609,6 +3157,9 @@ class EngineManagedStrategy(MaterializableStrategy):
2609
3157
  column_descriptions=model.column_descriptions,
2610
3158
  table_format=model.table_format,
2611
3159
  )
3160
+ self._apply_grants(
3161
+ model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3162
+ )
2612
3163
  elif not is_snapshot_deployable:
2613
3164
  # Snapshot isnt deployable; update the preview table instead
2614
3165
  # If the snapshot was deployable, then data would have already been loaded in create() because a managed table would have been created
@@ -2653,10 +3204,19 @@ class EngineManagedStrategy(MaterializableStrategy):
2653
3204
  )
2654
3205
  if len(potential_alter_operations) > 0:
2655
3206
  # this can happen if a user changes a managed model and deliberately overrides a plan to be forward only, eg `sqlmesh plan --forward-only`
2656
- raise SQLMeshError(
3207
+ raise MigrationNotSupportedError(
2657
3208
  f"The schema of the managed model '{target_table_name}' cannot be updated in a forward-only fashion."
2658
3209
  )
2659
3210
 
3211
+ # Apply grants after verifying no schema changes
3212
+ deployability_index = kwargs.get("deployability_index")
3213
+ is_snapshot_deployable = (
3214
+ deployability_index.is_deployable(snapshot) if deployability_index else False
3215
+ )
3216
+ self._apply_grants(
3217
+ snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
3218
+ )
3219
+
2660
3220
  def delete(self, name: str, **kwargs: t.Any) -> None:
2661
3221
  # a dev preview table is created as a normal table, so it needs to be dropped as a normal table
2662
3222
  _check_table_db_is_physical_schema(name, kwargs["physical_schema"])