Skip to content

Commit 50aee2c

Browse files
Fix: Pass a copy of the properties to avoid mutation of actual dict (#5417)
1 parent 672b440 commit 50aee2c

File tree

2 files changed

+78
-2
lines changed

2 files changed

+78
-2
lines changed

sqlmesh/core/snapshot/evaluator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ def _evaluate_snapshot(
763763
snapshots=snapshots,
764764
deployability_index=deployability_index,
765765
render_kwargs=create_render_kwargs,
766-
rendered_physical_properties=rendered_physical_properties,
766+
rendered_physical_properties=rendered_physical_properties.copy(),
767767
allow_destructive_snapshots=allow_destructive_snapshots,
768768
allow_additive_snapshots=allow_additive_snapshots,
769769
)
@@ -776,7 +776,7 @@ def _evaluate_snapshot(
776776
is_table_deployable=is_snapshot_deployable,
777777
deployability_index=deployability_index,
778778
create_render_kwargs=create_render_kwargs,
779-
rendered_physical_properties=rendered_physical_properties,
779+
rendered_physical_properties=rendered_physical_properties.copy(),
780780
dry_run=False,
781781
run_pre_post_statements=False,
782782
)

tests/core/test_snapshot_evaluator.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4737,3 +4737,79 @@ def test_wap_publish_failure(adapter_mock: Mock, make_snapshot: t.Callable[...,
47374737
# Execute audit with WAP ID and expect it to raise the exception
47384738
with pytest.raises(Exception, match="WAP publish failed"):
47394739
evaluator.audit(snapshot, snapshots={}, wap_id=wap_id)
4740+
4741+
4742+
def test_properties_are_preserved_in_both_create_statements(
4743+
adapter_mock: Mock, make_snapshot: t.Callable[..., Snapshot]
4744+
) -> None:
4745+
# the below mocks are needed to create a situation
4746+
# where we trigger two create statements during evaluation
4747+
transaction_mock = Mock()
4748+
transaction_mock.__enter__ = Mock()
4749+
transaction_mock.__exit__ = Mock()
4750+
session_mock = Mock()
4751+
session_mock.__enter__ = Mock()
4752+
session_mock.__exit__ = Mock()
4753+
adapter_mock = Mock()
4754+
adapter_mock.transaction.return_value = transaction_mock
4755+
adapter_mock.session.return_value = session_mock
4756+
adapter_mock.dialect = "trino"
4757+
adapter_mock.HAS_VIEW_BINDING = False
4758+
adapter_mock.wap_supported.return_value = False
4759+
adapter_mock.get_data_objects.return_value = []
4760+
adapter_mock.with_settings.return_value = adapter_mock
4761+
adapter_mock.table_exists.return_value = False
4762+
4763+
props = []
4764+
4765+
def mutate_view_properties(*args, **kwargs):
4766+
view_props = kwargs.get("view_properties")
4767+
if isinstance(view_props, dict):
4768+
props.append(view_props["creatable_type"].sql())
4769+
# simulate view pop
4770+
view_props.pop("creatable_type")
4771+
return None
4772+
4773+
adapter_mock.create_view.side_effect = mutate_view_properties
4774+
4775+
evaluator = SnapshotEvaluator(adapter_mock)
4776+
4777+
# create a view model with SECURITY INVOKER physical property
4778+
# AND self referenctial to trigger two create statements
4779+
model = load_sql_based_model(
4780+
parse( # type: ignore
4781+
"""
4782+
MODEL (
4783+
name test_schema.security_view,
4784+
kind VIEW,
4785+
physical_properties (
4786+
'creatable_type' = 'SECURITY INVOKER'
4787+
)
4788+
);
4789+
4790+
SELECT 1 as col from test_schema.security_view;
4791+
"""
4792+
),
4793+
)
4794+
4795+
snapshot = make_snapshot(model)
4796+
snapshot.categorize_as(SnapshotChangeCategory.BREAKING)
4797+
evaluator.evaluate(
4798+
snapshot,
4799+
start="2024-01-01",
4800+
end="2024-01-02",
4801+
execution_time="2024-01-02",
4802+
snapshots={},
4803+
)
4804+
4805+
# Verify create_view was called twice
4806+
assert adapter_mock.create_view.call_count == 2
4807+
first_call = adapter_mock.create_view.call_args_list[0]
4808+
second_call = adapter_mock.create_view.call_args_list[1]
4809+
4810+
# First call should be CREATE VIEW (replace=False) second CREATE OR REPLACE VIEW (replace=True)
4811+
assert first_call.kwargs.get("replace") == False
4812+
assert second_call.kwargs.get("replace") == True
4813+
4814+
# Both calls should have view_properties with security invoker
4815+
assert props == ["'SECURITY INVOKER'", "'SECURITY INVOKER'"]

0 commit comments

Comments
 (0)