@@ -928,6 +928,7 @@ def _render_and_insert_snapshot(
928928 model = snapshot .model
929929 adapter = self .get_adapter (model .gateway )
930930 evaluation_strategy = _evaluation_strategy (snapshot , adapter )
931+ is_snapshot_deployable = deployability_index .is_deployable (snapshot )
931932
932933 queries_or_dfs = self ._render_snapshot_for_evaluation (
933934 snapshot ,
@@ -951,6 +952,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None:
951952 execution_time = execution_time ,
952953 physical_properties = rendered_physical_properties ,
953954 render_kwargs = create_render_kwargs ,
955+ is_snapshot_deployable = is_snapshot_deployable ,
954956 )
955957 else :
956958 logger .info (
@@ -973,6 +975,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None:
973975 execution_time = execution_time ,
974976 physical_properties = rendered_physical_properties ,
975977 render_kwargs = create_render_kwargs ,
978+ is_snapshot_deployable = is_snapshot_deployable ,
976979 )
977980
978981 # DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result,
@@ -1181,6 +1184,7 @@ def _migrate_target_table(
11811184 allow_additive_snapshots = allow_additive_snapshots ,
11821185 ignore_destructive = snapshot .model .on_destructive_change .is_ignore ,
11831186 ignore_additive = snapshot .model .on_additive_change .is_ignore ,
1187+ deployability_index = deployability_index ,
11841188 )
11851189 finally :
11861190 if snapshot .is_materialized :
@@ -1230,6 +1234,7 @@ def _promote_snapshot(
12301234 model = snapshot .model ,
12311235 environment = environment_naming_info .name ,
12321236 snapshots = snapshots ,
1237+ snapshot = snapshot ,
12331238 ** render_kwargs ,
12341239 )
12351240
@@ -1456,6 +1461,8 @@ def _execute_create(
14561461 is_snapshot_representative = is_snapshot_representative ,
14571462 dry_run = dry_run ,
14581463 physical_properties = rendered_physical_properties ,
1464+ snapshot = snapshot ,
1465+ deployability_index = deployability_index ,
14591466 )
14601467 if run_pre_post_statements :
14611468 evaluation_strategy .run_post_statements (
@@ -1732,6 +1739,7 @@ def _apply_grants(
17321739 model : Model ,
17331740 table_name : str ,
17341741 target_layer : GrantsTargetLayer ,
1742+ is_snapshot_deployable : bool = False ,
17351743 ) -> None :
17361744 """Apply grants for a model if grants are configured.
17371745
@@ -1743,6 +1751,7 @@ def _apply_grants(
17431751 model: The SQLMesh model containing grants configuration
17441752 table_name: The target table/view name to apply grants to
17451753 target_layer: The grants application layer (physical or virtual)
1754+ is_snapshot_deployable: Whether the snapshot is deployable (targeting production)
17461755 """
17471756 grants_config = model .grants
17481757 if grants_config is None :
@@ -1756,7 +1765,16 @@ def _apply_grants(
17561765 return
17571766
17581767 model_grants_target_layer = model .grants_target_layer
1759- if not (model_grants_target_layer .is_all or model_grants_target_layer == target_layer ):
1768+
1769+ is_prod_and_dev_only = is_snapshot_deployable and model .virtual_environment_mode .is_dev_only
1770+
1771+ if not (
1772+ model_grants_target_layer .is_all
1773+ or model_grants_target_layer == target_layer
1774+ # Always apply grants in production when VDE is dev_only regardless of target_layer
1775+ # since only physical tables are created in production
1776+ or is_prod_and_dev_only
1777+ ):
17601778 logger .debug (
17611779 f"Skipping grants application for model { model .name } in { target_layer } layer"
17621780 )
@@ -1880,11 +1898,15 @@ def promote(
18801898 view_properties = model .render_virtual_properties (** render_kwargs ),
18811899 )
18821900
1901+ snapshot = kwargs ["snapshot" ]
1902+ deployability_index = kwargs ["deployability_index" ]
1903+ is_snapshot_deployable = deployability_index .is_deployable (snapshot )
1904+
18831905 # Apply grants to the physical layer (referenced table / view) after promotion
1884- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
1906+ self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable )
18851907
18861908 # Apply grants to the virtual layer (view) after promotion
1887- self ._apply_grants (model , view_name , GrantsTargetLayer .VIRTUAL )
1909+ self ._apply_grants (model , view_name , GrantsTargetLayer .VIRTUAL , is_snapshot_deployable )
18881910
18891911 def demote (self , view_name : str , ** kwargs : t .Any ) -> None :
18901912 logger .info ("Dropping view '%s'" , view_name )
@@ -1951,7 +1973,10 @@ def create(
19511973
19521974 # Apply grants after table creation (unless explicitly skipped by caller)
19531975 if not skip_grants :
1954- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
1976+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
1977+ self ._apply_grants (
1978+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
1979+ )
19551980
19561981 def migrate (
19571982 self ,
@@ -1979,7 +2004,13 @@ def migrate(
19792004 self .adapter .alter_table (alter_operations )
19802005
19812006 # Apply grants after schema migration
1982- self ._apply_grants (snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL )
2007+ deployability_index = kwargs .get ("deployability_index" )
2008+ is_snapshot_deployable = (
2009+ deployability_index .is_deployable (snapshot ) if deployability_index else False
2010+ )
2011+ self ._apply_grants (
2012+ snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2013+ )
19832014
19842015 def delete (self , name : str , ** kwargs : t .Any ) -> None :
19852016 _check_table_db_is_physical_schema (name , kwargs ["physical_schema" ])
@@ -2031,7 +2062,8 @@ def _replace_query_for_model(
20312062
20322063 # Apply grants after table replacement (unless explicitly skipped by caller)
20332064 if not skip_grants :
2034- self ._apply_grants (model , name , GrantsTargetLayer .PHYSICAL )
2065+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2066+ self ._apply_grants (model , name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable )
20352067
20362068 def _get_target_and_source_columns (
20372069 self ,
@@ -2321,7 +2353,10 @@ def create(
23212353
23222354 if not skip_grants :
23232355 # Apply grants after seed table creation and data insertion
2324- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2356+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2357+ self ._apply_grants (
2358+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2359+ )
23252360 except Exception :
23262361 self .adapter .drop_table (table_name )
23272362 raise
@@ -2405,7 +2440,10 @@ def create(
24052440
24062441 if not skip_grants :
24072442 # Apply grants after SCD Type 2 table creation
2408- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2443+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2444+ self ._apply_grants (
2445+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2446+ )
24092447
24102448 def insert (
24112449 self ,
@@ -2531,7 +2569,8 @@ def insert(
25312569 )
25322570
25332571 # Apply grants after view creation / replacement
2534- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2572+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2573+ self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable )
25352574
25362575 def append (
25372576 self ,
@@ -2552,14 +2591,18 @@ def create(
25522591 skip_grants : bool ,
25532592 ** kwargs : t .Any ,
25542593 ) -> None :
2594+ is_snapshot_deployable : bool = kwargs ["is_snapshot_deployable" ]
2595+
25552596 if self .adapter .table_exists (table_name ):
25562597 # Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late
25572598 # binding support (because of DROP CASCADE).
25582599 logger .info ("View '%s' already exists" , table_name )
25592600
25602601 if not skip_grants :
25612602 # Always apply grants when present, even if view exists, to handle grants updates
2562- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2603+ self ._apply_grants (
2604+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2605+ )
25632606 return
25642607
25652608 logger .info ("Creating view '%s'" , table_name )
@@ -2585,7 +2628,9 @@ def create(
25852628
25862629 if not skip_grants :
25872630 # Apply grants after view creation
2588- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2631+ self ._apply_grants (
2632+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2633+ )
25892634
25902635 def migrate (
25912636 self ,
@@ -2614,7 +2659,13 @@ def migrate(
26142659 )
26152660
26162661 # Apply grants after view migration
2617- self ._apply_grants (snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL )
2662+ deployability_index = kwargs .get ("deployability_index" )
2663+ is_snapshot_deployable = (
2664+ deployability_index .is_deployable (snapshot ) if deployability_index else False
2665+ )
2666+ self ._apply_grants (
2667+ snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2668+ )
26182669
26192670 def delete (self , name : str , ** kwargs : t .Any ) -> None :
26202671 cascade = kwargs .pop ("cascade" , False )
@@ -2923,7 +2974,9 @@ def create(
29232974
29242975 # Apply grants after managed table creation
29252976 if not skip_grants :
2926- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
2977+ self ._apply_grants (
2978+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
2979+ )
29272980
29282981 elif not is_table_deployable :
29292982 # Only create the dev preview table as a normal table.
@@ -2963,7 +3016,9 @@ def insert(
29633016 column_descriptions = model .column_descriptions ,
29643017 table_format = model .table_format ,
29653018 )
2966- self ._apply_grants (model , table_name , GrantsTargetLayer .PHYSICAL )
3019+ self ._apply_grants (
3020+ model , table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
3021+ )
29673022 elif not is_snapshot_deployable :
29683023 # Snapshot isnt deployable; update the preview table instead
29693024 # If the snapshot was deployable, then data would have already been loaded in create() because a managed table would have been created
@@ -3013,8 +3068,13 @@ def migrate(
30133068 )
30143069
30153070 # Apply grants after verifying no schema changes
3016- # This ensures metadata-only grants changes are applied
3017- self ._apply_grants (snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL )
3071+ deployability_index = kwargs .get ("deployability_index" )
3072+ is_snapshot_deployable = (
3073+ deployability_index .is_deployable (snapshot ) if deployability_index else False
3074+ )
3075+ self ._apply_grants (
3076+ snapshot .model , target_table_name , GrantsTargetLayer .PHYSICAL , is_snapshot_deployable
3077+ )
30183078
30193079 def delete (self , name : str , ** kwargs : t .Any ) -> None :
30203080 # a dev preview table is created as a normal table, so it needs to be dropped as a normal table
0 commit comments