@@ -933,6 +933,7 @@ def _render_and_insert_snapshot(
933933 model = snapshot .model
934934 adapter = self .get_adapter (model .gateway )
935935 evaluation_strategy = _evaluation_strategy (snapshot , adapter )
936+ is_snapshot_deployable = deployability_index .is_deployable (snapshot )
936937
937938 queries_or_dfs = self ._render_snapshot_for_evaluation (
938939 snapshot ,
@@ -956,6 +957,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None:
956957 execution_time = execution_time ,
957958 physical_properties = rendered_physical_properties ,
958959 render_kwargs = create_render_kwargs ,
960+ is_snapshot_deployable = is_snapshot_deployable ,
959961 )
960962 else :
961963 logger .info (
@@ -978,6 +980,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None:
978980 execution_time = execution_time ,
979981 physical_properties = rendered_physical_properties ,
980982 render_kwargs = create_render_kwargs ,
983+ is_snapshot_deployable = is_snapshot_deployable ,
981984 )
982985
983986 # DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result,
@@ -1186,6 +1189,7 @@ def _migrate_target_table(
11861189 allow_additive_snapshots = allow_additive_snapshots ,
11871190 ignore_destructive = snapshot .model .on_destructive_change .is_ignore ,
11881191 ignore_additive = snapshot .model .on_additive_change .is_ignore ,
1192+ deployability_index = deployability_index ,
11891193 )
11901194 finally :
11911195 if snapshot .is_materialized :
@@ -1235,6 +1239,7 @@ def _promote_snapshot(
12351239 model = snapshot .model ,
12361240 environment = environment_naming_info .name ,
12371241 snapshots = snapshots ,
1242+ snapshot = snapshot ,
12381243 ** render_kwargs ,
12391244 )
12401245
@@ -1461,6 +1466,8 @@ def _execute_create(
14611466 is_snapshot_representative = is_snapshot_representative ,
14621467 dry_run = dry_run ,
14631468 physical_properties = rendered_physical_properties ,
1469+ snapshot = snapshot ,
1470+ deployability_index = deployability_index ,
14641471 )
14651472 if run_pre_post_statements :
14661473 evaluation_strategy .run_post_statements (
@@ -1791,6 +1798,7 @@ def _apply_grants(
17911798 model : Model ,
17921799 table_name : str ,
17931800 target_layer : GrantsTargetLayer ,
1801+ is_snapshot_deployable : bool = False ,
17941802 ) -> None :
17951803 """Apply grants for a model if grants are configured.
17961804
@@ -1802,6 +1810,7 @@ def _apply_grants(
18021810 model: The SQLMesh model containing grants configuration
18031811 table_name: The target table/view name to apply grants to
18041812 target_layer: The grants application layer (physical or virtual)
1813+ is_snapshot_deployable: Whether the snapshot is deployable (targeting production)
18051814 """
18061815 grants_config = model .grants
18071816 if grants_config is None :
@@ -1815,7 +1824,16 @@ def _apply_grants(
18151824 return
18161825
18171826 model_grants_target_layer = model .grants_target_layer
1818- if not (model_grants_target_layer .is_all or model_grants_target_layer == target_layer ):
1827+
1828+ is_prod_and_dev_only = is_snapshot_deployable and model .virtual_environment_mode .is_dev_only
1829+
1830+ if not (
1831+ model_grants_target_layer .is_all
1832+ or model_grants_target_layer == target_layer
1833+ # Always apply grants in production when VDE is dev_only regardless of target_layer
1834+ # since only physical tables are created in production
1835+ or is_prod_and_dev_only
1836+ ):
18191837 logger .debug (
18201838 f"Skipping grants application for model { model .name } in { target_layer } layer"
18211839 )
@@ -1939,11 +1957,15 @@ def promote(
19391957 view_properties = model .render_virtual_properties (** render_kwargs ),
19401958 )
19411959
1960+ snapshot = kwargs ["snapshot" ]
1961+ deployability_index = kwargs ["deployability_index" ]
1962+ is_snapshot_deployable = deployability_index .is_deployable (snapshot )
1963+
19421964 # Apply grants to the physical layer (referenced table / view) after promotion
1943- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
1965+ self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable )
19441966
19451967 # Apply grants to the virtual layer (view) after promotion
1946- self ._apply_grants (model , view_name , GrantsTargetLayer .VIRTUAL )
1968+ self ._apply_grants (model , view_name , GrantsTargetLayer .VIRTUAL , is_snapshot_deployable )
19471969
19481970 def demote (self , view_name : str , ** kwargs : t .Any ) -> None :
19491971 logger .info ("Dropping view '%s'" , view_name )
@@ -2010,7 +2032,10 @@ def create(
20102032
20112033 # Apply grants after table creation (unless explicitly skipped by caller)
20122034 if not skip_grants :
2013- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2035+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2036+ self ._apply_grants (
2037+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2038+ )
20142039
20152040 def migrate (
20162041 self ,
@@ -2038,7 +2063,13 @@ def migrate(
20382063 self .adapter .alter_table (alter_operations )
20392064
20402065 # Apply grants after schema migration
2041- self ._apply_grants (snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL )
2066+ deployability_index = kwargs .get ("deployability_index" )
2067+ is_snapshot_deployable = (
2068+ deployability_index .is_deployable (snapshot ) if deployability_index else False
2069+ )
2070+ self ._apply_grants (
2071+ snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2072+ )
20422073
20432074 def delete (self , name : str , ** kwargs : t .Any ) -> None :
20442075 _check_table_db_is_physical_schema (name , kwargs ["physical_schema" ])
@@ -2090,7 +2121,8 @@ def _replace_query_for_model(
20902121
20912122 # Apply grants after table replacement (unless explicitly skipped by caller)
20922123 if not skip_grants :
2093- self ._apply_grants (model , name , GrantsTargetLayer .PHYSICAL )
2124+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2125+ self ._apply_grants (model , name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable )
20942126
20952127 def _get_target_and_source_columns (
20962128 self ,
@@ -2380,7 +2412,10 @@ def create(
23802412
23812413 if not skip_grants :
23822414 # Apply grants after seed table creation and data insertion
2383- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2415+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2416+ self ._apply_grants (
2417+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2418+ )
23842419 except Exception :
23852420 self .adapter .drop_table (table_name )
23862421 raise
@@ -2464,7 +2499,10 @@ def create(
24642499
24652500 if not skip_grants :
24662501 # Apply grants after SCD Type 2 table creation
2467- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2502+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2503+ self ._apply_grants (
2504+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2505+ )
24682506
24692507 def insert (
24702508 self ,
@@ -2590,7 +2628,8 @@ def insert(
25902628 )
25912629
25922630 # Apply grants after view creation / replacement
2593- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2631+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2632+ self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable )
25942633
25952634 def append (
25962635 self ,
@@ -2611,14 +2650,18 @@ def create(
26112650 skip_grants : bool ,
26122651 ** kwargs : t .Any ,
26132652 ) -> None :
2653+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2654+
26142655 if self .adapter .table_exists (table_name ):
26152656 # Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late
26162657 # binding support (because of DROP CASCADE).
26172658 logger .info ("View '%s' already exists" , table_name )
26182659
26192660 if not skip_grants :
26202661 # Always apply grants when present, even if view exists, to handle grants updates
2621- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2662+ self ._apply_grants (
2663+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2664+ )
26222665 return
26232666
26242667 logger .info ("Creating view '%s'" , table_name )
@@ -2644,7 +2687,9 @@ def create(
26442687
26452688 if not skip_grants :
26462689 # Apply grants after view creation
2647- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2690+ self ._apply_grants (
2691+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2692+ )
26482693
26492694 def migrate (
26502695 self ,
@@ -2673,7 +2718,13 @@ def migrate(
26732718 )
26742719
26752720 # Apply grants after view migration
2676- self ._apply_grants (snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL )
2721+ deployability_index = kwargs .get ("deployability_index" )
2722+ is_snapshot_deployable = (
2723+ deployability_index .is_deployable (snapshot ) if deployability_index else False
2724+ )
2725+ self ._apply_grants (
2726+ snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2727+ )
26772728
26782729 def delete (self , name : str , ** kwargs : t .Any ) -> None :
26792730 cascade = kwargs .pop ("cascade" , False )
@@ -2982,7 +3033,9 @@ def create(
29823033
29833034 # Apply grants after managed table creation
29843035 if not skip_grants :
2985- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
3036+ self ._apply_grants (
3037+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
3038+ )
29863039
29873040 elif not is_table_deployable :
29883041 # Only create the dev preview table as a normal table.
@@ -3022,7 +3075,9 @@ def insert(
30223075 column_descriptions = model .column_descriptions ,
30233076 table_format = model .table_format ,
30243077 )
3025- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
3078+ self ._apply_grants (
3079+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
3080+ )
30263081 elif not is_snapshot_deployable :
30273082 # Snapshot isnt deployable; update the preview table instead
30283083 # If the snapshot was deployable, then data would have already been loaded in create() because a managed table would have been created
@@ -3072,8 +3127,13 @@ def migrate(
30723127 )
30733128
30743129 # Apply grants after verifying no schema changes
3075- # This ensures metadata-only grants changes are applied
3076- self ._apply_grants (snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL )
3130+ deployability_index = kwargs .get ("deployability_index" )
3131+ is_snapshot_deployable = (
3132+ deployability_index .is_deployable (snapshot ) if deployability_index else False
3133+ )
3134+ self ._apply_grants (
3135+ snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
3136+ )
30773137
30783138 def delete (self , name : str , ** kwargs : t .Any ) -> None :
30793139 # a dev preview table is created as a normal table, so it needs to be dropped as a normal table
0 commit comments