Skip to content

Commit 1f3f6b8

Browse files
committed
fix: ensure dev_only VDE always applies grants in production
1 parent 6d3e2da commit 1f3f6b8

3 files changed

Lines changed: 443 additions & 17 deletions

File tree

sqlmesh/core/snapshot/evaluator.py

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,7 @@ def _render_and_insert_snapshot(
922922
model = snapshot.model
923923
adapter = self.get_adapter(model.gateway)
924924
evaluation_strategy = _evaluation_strategy(snapshot, adapter)
925+
is_snapshot_deployable = deployability_index.is_deployable(snapshot)
925926

926927
queries_or_dfs = self._render_snapshot_for_evaluation(
927928
snapshot,
@@ -945,6 +946,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None:
945946
execution_time=execution_time,
946947
physical_properties=rendered_physical_properties,
947948
render_kwargs=create_render_kwargs,
949+
is_snapshot_deployable=is_snapshot_deployable,
948950
)
949951
else:
950952
logger.info(
@@ -967,6 +969,7 @@ def apply(query_or_df: QueryOrDF, index: int = 0) -> None:
967969
execution_time=execution_time,
968970
physical_properties=rendered_physical_properties,
969971
render_kwargs=create_render_kwargs,
972+
is_snapshot_deployable=is_snapshot_deployable,
970973
)
971974

972975
# DataFrames, unlike SQL expressions, can provide partial results by yielding dataframes. As a result,
@@ -1175,6 +1178,7 @@ def _migrate_target_table(
11751178
allow_additive_snapshots=allow_additive_snapshots,
11761179
ignore_destructive=snapshot.model.on_destructive_change.is_ignore,
11771180
ignore_additive=snapshot.model.on_additive_change.is_ignore,
1181+
deployability_index=deployability_index,
11781182
)
11791183
finally:
11801184
if snapshot.is_materialized:
@@ -1224,6 +1228,7 @@ def _promote_snapshot(
12241228
model=snapshot.model,
12251229
environment=environment_naming_info.name,
12261230
snapshots=snapshots,
1231+
snapshot=snapshot,
12271232
**render_kwargs,
12281233
)
12291234

@@ -1448,6 +1453,8 @@ def _execute_create(
14481453
is_snapshot_representative=is_snapshot_representative,
14491454
dry_run=dry_run,
14501455
physical_properties=rendered_physical_properties,
1456+
snapshot=snapshot,
1457+
deployability_index=deployability_index,
14511458
)
14521459
if run_pre_post_statements:
14531460
adapter.execute(snapshot.model.render_post_statements(**create_render_kwargs))
@@ -1690,6 +1697,7 @@ def _apply_grants(
16901697
model: Model,
16911698
table_name: str,
16921699
target_layer: GrantsTargetLayer,
1700+
is_snapshot_deployable: bool = False,
16931701
) -> None:
16941702
"""Apply grants for a model if grants are configured.
16951703
@@ -1701,6 +1709,7 @@ def _apply_grants(
17011709
model: The SQLMesh model containing grants configuration
17021710
table_name: The target table/view name to apply grants to
17031711
target_layer: The grants application layer (physical or virtual)
1712+
is_snapshot_deployable: Whether the snapshot is deployable (targeting production)
17041713
"""
17051714
grants_config = model.grants
17061715
if grants_config is None:
@@ -1714,7 +1723,16 @@ def _apply_grants(
17141723
return
17151724

17161725
model_grants_target_layer = model.grants_target_layer
1717-
if not (model_grants_target_layer.is_all or model_grants_target_layer == target_layer):
1726+
1727+
is_prod_and_dev_only = is_snapshot_deployable and model.virtual_environment_mode.is_dev_only
1728+
1729+
if not (
1730+
model_grants_target_layer.is_all
1731+
or model_grants_target_layer == target_layer
1732+
# Always apply grants in production when VDE is dev_only regardless of target_layer
1733+
# since only physical tables are created in production
1734+
or is_prod_and_dev_only
1735+
):
17181736
logger.debug(
17191737
f"Skipping grants application for model {model.name} in {target_layer} layer"
17201738
)
@@ -1832,11 +1850,15 @@ def promote(
18321850
view_properties=model.render_virtual_properties(**render_kwargs),
18331851
)
18341852

1853+
snapshot = kwargs["snapshot"]
1854+
deployability_index = kwargs["deployability_index"]
1855+
is_snapshot_deployable = deployability_index.is_deployable(snapshot)
1856+
18351857
# Apply grants to the physical layer (referenced table / view) after promotion
1836-
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL)
1858+
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
18371859

18381860
# Apply grants to the virtual layer (view) after promotion
1839-
self._apply_grants(model, view_name, GrantsTargetLayer.VIRTUAL)
1861+
self._apply_grants(model, view_name, GrantsTargetLayer.VIRTUAL, is_snapshot_deployable)
18401862

18411863
def demote(self, view_name: str, **kwargs: t.Any) -> None:
18421864
logger.info("Dropping view '%s'", view_name)
@@ -1897,7 +1919,10 @@ def create(
18971919

18981920
# Apply grants after table creation (unless explicitly skipped by caller)
18991921
if not skip_grants:
1900-
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL)
1922+
is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
1923+
self._apply_grants(
1924+
model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
1925+
)
19011926

19021927
def migrate(
19031928
self,
@@ -1925,7 +1950,13 @@ def migrate(
19251950
self.adapter.alter_table(alter_operations)
19261951

19271952
# Apply grants after schema migration
1928-
self._apply_grants(snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL)
1953+
deployability_index = kwargs.get("deployability_index")
1954+
is_snapshot_deployable = (
1955+
deployability_index.is_deployable(snapshot) if deployability_index else False
1956+
)
1957+
self._apply_grants(
1958+
snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
1959+
)
19291960

19301961
def delete(self, name: str, **kwargs: t.Any) -> None:
19311962
_check_table_db_is_physical_schema(name, kwargs["physical_schema"])
@@ -1977,7 +2008,8 @@ def _replace_query_for_model(
19772008

19782009
# Apply grants after table replacement (unless explicitly skipped by caller)
19792010
if not skip_grants:
1980-
self._apply_grants(model, name, GrantsTargetLayer.PHYSICAL)
2011+
is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
2012+
self._apply_grants(model, name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
19812013

19822014
def _get_target_and_source_columns(
19832015
self,
@@ -2267,7 +2299,10 @@ def create(
22672299

22682300
if not skip_grants:
22692301
# Apply grants after seed table creation and data insertion
2270-
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL)
2302+
is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
2303+
self._apply_grants(
2304+
model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2305+
)
22712306
except Exception:
22722307
self.adapter.drop_table(table_name)
22732308
raise
@@ -2339,7 +2374,10 @@ def create(
23392374

23402375
if not skip_grants:
23412376
# Apply grants after SCD Type 2 table creation
2342-
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL)
2377+
is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
2378+
self._apply_grants(
2379+
model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2380+
)
23432381

23442382
def insert(
23452383
self,
@@ -2465,7 +2503,8 @@ def insert(
24652503
)
24662504

24672505
# Apply grants after view creation / replacement
2468-
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL)
2506+
is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
2507+
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable)
24692508

24702509
def append(
24712510
self,
@@ -2486,14 +2525,18 @@ def create(
24862525
skip_grants: bool,
24872526
**kwargs: t.Any,
24882527
) -> None:
2528+
is_snapshot_deployable: bool = kwargs["is_snapshot_deployable"]
2529+
24892530
if self.adapter.table_exists(table_name):
24902531
# Make sure we don't recreate the view to prevent deletion of downstream views in engines with no late
24912532
# binding support (because of DROP CASCADE).
24922533
logger.info("View '%s' already exists", table_name)
24932534

24942535
if not skip_grants:
24952536
# Always apply grants when present, even if view exists, to handle grants updates
2496-
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL)
2537+
self._apply_grants(
2538+
model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2539+
)
24972540
return
24982541

24992542
logger.info("Creating view '%s'", table_name)
@@ -2519,7 +2562,9 @@ def create(
25192562

25202563
if not skip_grants:
25212564
# Apply grants after view creation
2522-
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL)
2565+
self._apply_grants(
2566+
model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2567+
)
25232568

25242569
def migrate(
25252570
self,
@@ -2548,7 +2593,13 @@ def migrate(
25482593
)
25492594

25502595
# Apply grants after view migration
2551-
self._apply_grants(snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL)
2596+
deployability_index = kwargs.get("deployability_index")
2597+
is_snapshot_deployable = (
2598+
deployability_index.is_deployable(snapshot) if deployability_index else False
2599+
)
2600+
self._apply_grants(
2601+
snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2602+
)
25522603

25532604
def delete(self, name: str, **kwargs: t.Any) -> None:
25542605
cascade = kwargs.pop("cascade", False)
@@ -2718,7 +2769,9 @@ def create(
27182769

27192770
# Apply grants after managed table creation
27202771
if not skip_grants:
2721-
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL)
2772+
self._apply_grants(
2773+
model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2774+
)
27222775

27232776
elif not is_table_deployable:
27242777
# Only create the dev preview table as a normal table.
@@ -2758,7 +2811,9 @@ def insert(
27582811
column_descriptions=model.column_descriptions,
27592812
table_format=model.table_format,
27602813
)
2761-
self._apply_grants(model, table_name, GrantsTargetLayer.PHYSICAL)
2814+
self._apply_grants(
2815+
model, table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2816+
)
27622817
elif not is_snapshot_deployable:
27632818
# Snapshot isnt deployable; update the preview table instead
27642819
# If the snapshot was deployable, then data would have already been loaded in create() because a managed table would have been created
@@ -2808,8 +2863,13 @@ def migrate(
28082863
)
28092864

28102865
# Apply grants after verifying no schema changes
2811-
# This ensures metadata-only grants changes are applied
2812-
self._apply_grants(snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL)
2866+
deployability_index = kwargs.get("deployability_index")
2867+
is_snapshot_deployable = (
2868+
deployability_index.is_deployable(snapshot) if deployability_index else False
2869+
)
2870+
self._apply_grants(
2871+
snapshot.model, target_table_name, GrantsTargetLayer.PHYSICAL, is_snapshot_deployable
2872+
)
28132873

28142874
def delete(self, name: str, **kwargs: t.Any) -> None:
28152875
# a dev preview table is created as a normal table, so it needs to be dropped as a normal table

0 commit comments

Comments
 (0)