Skip to content

Commit 0bf202c

Browse files
authored
Feat!: Categorize indirect MV changes as breaking for seamless version switching (#5374)
1 parent 08950c8 commit 0bf202c

File tree

2 files changed

+149
-1
lines changed

2 files changed

+149
-1
lines changed

sqlmesh/core/plan/builder.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,14 @@ def _categorize_snapshot(
680680
if mode == AutoCategorizationMode.FULL:
681681
snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only)
682682
elif self._context_diff.indirectly_modified(snapshot.name):
683+
if snapshot.is_materialized_view and not forward_only:
684+
# We categorize changes as breaking to allow for instantaneous switches in a virtual layer.
685+
# Otherwise, there might be a potentially long downtime during MVs recreation.
686+
# In the case of forward-only changes this optimization is not applicable because we want to continue
687+
# using the same (existing) table version.
688+
snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING, forward_only)
689+
return
690+
683691
all_upstream_forward_only = set()
684692
all_upstream_categories = set()
685693
direct_parent_categories = set()

tests/core/test_plan.py

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
SqlModel,
2727
ModelKindName,
2828
)
29-
from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange
29+
from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange, ViewKind
3030
from sqlmesh.core.model.seed import Seed
3131
from sqlmesh.core.plan import Plan, PlanBuilder, SnapshotIntervals
3232
from sqlmesh.core.snapshot import (
@@ -4162,3 +4162,143 @@ def test_plan_ignore_cron_flag(make_snapshot):
41624162
],
41634163
)
41644164
]
4165+
4166+
4167+
def test_indirect_change_to_materialized_view_is_breaking(make_snapshot):
4168+
snapshot_a_old = make_snapshot(
4169+
SqlModel(
4170+
name="a",
4171+
query=parse_one("select 1 as col_a"),
4172+
kind=ViewKind(materialized=True),
4173+
)
4174+
)
4175+
snapshot_a_old.categorize_as(SnapshotChangeCategory.BREAKING)
4176+
4177+
snapshot_b_old = make_snapshot(
4178+
SqlModel(
4179+
name="b",
4180+
query=parse_one("select col_a from a"),
4181+
kind=ViewKind(materialized=True),
4182+
),
4183+
nodes={'"a"': snapshot_a_old.model},
4184+
)
4185+
snapshot_b_old.categorize_as(SnapshotChangeCategory.BREAKING)
4186+
4187+
snapshot_a_new = make_snapshot(
4188+
SqlModel(
4189+
name="a",
4190+
query=parse_one("select 1 as col_a, 2 as col_b"),
4191+
kind=ViewKind(materialized=True),
4192+
)
4193+
)
4194+
4195+
snapshot_a_new.previous_versions = snapshot_a_old.all_versions
4196+
4197+
snapshot_b_new = make_snapshot(
4198+
snapshot_b_old.model,
4199+
nodes={'"a"': snapshot_a_new.model},
4200+
)
4201+
snapshot_b_new.previous_versions = snapshot_b_old.all_versions
4202+
4203+
context_diff = ContextDiff(
4204+
environment="test_environment",
4205+
is_new_environment=True,
4206+
is_unfinalized_environment=False,
4207+
normalize_environment_name=True,
4208+
create_from="prod",
4209+
create_from_env_exists=True,
4210+
added=set(),
4211+
removed_snapshots={},
4212+
modified_snapshots={
4213+
snapshot_a_new.name: (snapshot_a_new, snapshot_a_old),
4214+
snapshot_b_new.name: (snapshot_b_new, snapshot_b_old),
4215+
},
4216+
snapshots={
4217+
snapshot_a_new.snapshot_id: snapshot_a_new,
4218+
snapshot_b_new.snapshot_id: snapshot_b_new,
4219+
},
4220+
new_snapshots={
4221+
snapshot_a_new.snapshot_id: snapshot_a_new,
4222+
snapshot_b_new.snapshot_id: snapshot_b_new,
4223+
},
4224+
previous_plan_id=None,
4225+
previously_promoted_snapshot_ids=set(),
4226+
previous_finalized_snapshots=None,
4227+
previous_gateway_managed_virtual_layer=False,
4228+
gateway_managed_virtual_layer=False,
4229+
environment_statements=[],
4230+
)
4231+
4232+
PlanBuilder(context_diff, forward_only=False).build()
4233+
4234+
assert snapshot_b_new.change_category == SnapshotChangeCategory.INDIRECT_BREAKING
4235+
4236+
4237+
def test_forward_only_indirect_change_to_materialized_view(make_snapshot):
4238+
snapshot_a_old = make_snapshot(
4239+
SqlModel(
4240+
name="a",
4241+
query=parse_one("select 1 as col_a"),
4242+
)
4243+
)
4244+
snapshot_a_old.categorize_as(SnapshotChangeCategory.BREAKING)
4245+
4246+
snapshot_b_old = make_snapshot(
4247+
SqlModel(
4248+
name="b",
4249+
query=parse_one("select col_a from a"),
4250+
kind=ViewKind(materialized=True),
4251+
),
4252+
nodes={'"a"': snapshot_a_old.model},
4253+
)
4254+
snapshot_b_old.categorize_as(SnapshotChangeCategory.BREAKING)
4255+
4256+
snapshot_a_new = make_snapshot(
4257+
SqlModel(
4258+
name="a",
4259+
query=parse_one("select 1 as col_a, 2 as col_b"),
4260+
)
4261+
)
4262+
4263+
snapshot_a_new.previous_versions = snapshot_a_old.all_versions
4264+
4265+
snapshot_b_new = make_snapshot(
4266+
snapshot_b_old.model,
4267+
nodes={'"a"': snapshot_a_new.model},
4268+
)
4269+
snapshot_b_new.previous_versions = snapshot_b_old.all_versions
4270+
4271+
context_diff = ContextDiff(
4272+
environment="test_environment",
4273+
is_new_environment=True,
4274+
is_unfinalized_environment=False,
4275+
normalize_environment_name=True,
4276+
create_from="prod",
4277+
create_from_env_exists=True,
4278+
added=set(),
4279+
removed_snapshots={},
4280+
modified_snapshots={
4281+
snapshot_a_new.name: (snapshot_a_new, snapshot_a_old),
4282+
snapshot_b_new.name: (snapshot_b_new, snapshot_b_old),
4283+
},
4284+
snapshots={
4285+
snapshot_a_new.snapshot_id: snapshot_a_new,
4286+
snapshot_b_new.snapshot_id: snapshot_b_new,
4287+
},
4288+
new_snapshots={
4289+
snapshot_a_new.snapshot_id: snapshot_a_new,
4290+
snapshot_b_new.snapshot_id: snapshot_b_new,
4291+
},
4292+
previous_plan_id=None,
4293+
previously_promoted_snapshot_ids=set(),
4294+
previous_finalized_snapshots=None,
4295+
previous_gateway_managed_virtual_layer=False,
4296+
gateway_managed_virtual_layer=False,
4297+
environment_statements=[],
4298+
)
4299+
4300+
PlanBuilder(context_diff, forward_only=True).build()
4301+
4302+
# Forward-only indirect changes to MVs should not always be classified as indirect breaking.
4303+
# Instead, we want to preserve the standard categorization.
4304+
assert snapshot_b_new.change_category == SnapshotChangeCategory.INDIRECT_NON_BREAKING

0 commit comments

Comments
 (0)