Skip to content

Commit 4be3d74

Browse files
committed
fix: ensure dev_only VDE always applies grants in production
1 parent bb5e171 commit 4be3d74

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
@@ -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

Comments
 (0)