Skip to content

Commit 2edacde

Browse files
committed
fix(migrations): never redefine a compatibility view whose old name a project model now uses
A recorded compatibility view whose old name is a project model's destination (for example after an interrupted early drop) is no longer refreshed, rebound, or released by builds of the destination. Before, when the project's model built at that name before the destination, the destination's refresh overwrote the project's view with the compatibility SQL; concurrent builds made that order likely. Refs CHI-391
1 parent 1a554ec commit 2edacde

5 files changed

Lines changed: 37 additions & 5 deletions

File tree

‎src/sqlbuild/compiler/planner/_helpers/migrations/old_names.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def plan_old_name_views(
119119
if history.move.target_name in (None, target_name)
120120
)
121121
views: tuple[OldNameView, ...] = tuple(
122-
_recorded_view(runtime=runtime, history=history)
122+
_recorded_view(runtime=runtime, history=history, models=runtime.project.models)
123123
for history in histories
124124
if history.created is not None and history.dropped is None
125125
)
@@ -562,7 +562,9 @@ def claim_message(*, model_name: str, view: OldNameView, now: datetime) -> str:
562562
)
563563

564564

565-
def _recorded_view(*, runtime: PlannerRuntime, history: OldNameViewHistory) -> OldNameView:
565+
def _recorded_view(
566+
*, runtime: PlannerRuntime, history: OldNameViewHistory, models: Iterable[CompiledModel]
567+
) -> OldNameView:
566568
return OldNameView(
567569
destination_model=history.move.destination_model,
568570
old=_location(runtime=runtime, relation=history.old),
@@ -571,6 +573,14 @@ def _recorded_view(*, runtime: PlannerRuntime, history: OldNameViewHistory) -> O
571573
column_aliases=() if history.created is None else history.created.column_aliases,
572574
migration_event_id=history.move.event_id,
573575
target_name=history.move.target_name,
576+
name_reused_by=next(
577+
(
578+
model.name
579+
for model in models
580+
if migration_relation_for_location(model.destination).matches(history.old)
581+
),
582+
None,
583+
),
574584
)
575585

576586

‎src/sqlbuild/compiler/planner/models.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,7 @@ class OldNameView:
987987
column_aliases: tuple[tuple[str, str], ...]
988988
migration_event_id: str
989989
target_name: str | None
990+
name_reused_by: str | None = None
990991

991992

992993
@dataclass(frozen=True)

‎src/sqlbuild/executor/migrations/_helpers/old_name_views.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ def _migration_event_id(*, entry: OldNameViewPlanEntry, run_id: str) -> str:
421421
def views_reading(
422422
*, location: CompiledRelationLocation, recorded_views: tuple[OldNameView, ...]
423423
) -> tuple[OldNameViewSource, ...]:
424-
"""Return recorded views that read one relation, then views chained onto those, in order."""
424+
"""Return recorded views reading one relation, then chained views; reused names are skipped."""
425425

426426
ordered: list[OldNameViewSource] = []
427427
seen: set[tuple[str | None, str, str]] = set()
@@ -639,7 +639,8 @@ def _direct_readers(
639639
return tuple(
640640
view
641641
for view in recorded_views
642-
if migration_relation_for_location(view.new).matches(relation)
642+
if view.name_reused_by is None
643+
and migration_relation_for_location(view.new).matches(relation)
643644
)
644645

645646

‎tests/integration/src/sqlbuild/cli/commands/main/old_name_views/_test_types.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class OldNameJanitorClaimTestCase:
4545
description: str
4646
janitor_args: tuple[str, ...]
4747
install_build_fault: Callable[[pytest.MonkeyPatch], None]
48+
claiming_builds: tuple[tuple[str, ...], ...]
4849
expected_janitor_fragment: str
4950
expected_old_name_type: str
5051
expected_final_facts: tuple[str, ...]

‎tests/integration/src/sqlbuild/cli/commands/main/old_name_views/test_old_name_janitor_resume.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
OldNameJanitorResumeTestCase,
2121
)
2222
from tests.integration.src.sqlbuild.cli.commands.main.old_name_views.helpers import (
23+
DESTINATION_MODEL,
2324
JANITOR_PROJECT_TOML,
2425
ORIGIN_MODEL,
2526
claiming_view_models,
@@ -31,6 +32,7 @@
3132
relation_type_in,
3233
)
3334

35+
_ONE_BUILD: tuple[tuple[str, ...], ...] = (("build",),)
3436
_CLAIMED: str = "└── record dropped (name now used by another relation, model:revenue)"
3537

3638

@@ -94,6 +96,7 @@ def test_given_janitor_crash_after_drop_when_rerunning_then_drop_is_recorded_as_
9496
description="plain janitor run records the claimed name without touching it",
9597
janitor_args=("janitor", "--auto-approve"),
9698
install_build_fault=no_build_fault,
99+
claiming_builds=_ONE_BUILD,
97100
expected_janitor_fragment=_CLAIMED,
98101
expected_old_name_type="VIEW",
99102
expected_final_facts=("required", "origin_archived", "view_created", "view_dropped"),
@@ -102,6 +105,7 @@ def test_given_janitor_crash_after_drop_when_rerunning_then_drop_is_recorded_as_
102105
description="repeated early drop leaves the project's view in place",
103106
janitor_args=("janitor", "--auto-approve", "--drop-old-name-view", "dev.revenue"),
104107
install_build_fault=no_build_fault,
108+
claiming_builds=_ONE_BUILD,
105109
expected_janitor_fragment=_CLAIMED,
106110
expected_old_name_type="VIEW",
107111
expected_final_facts=("required", "origin_archived", "view_created", "view_dropped"),
@@ -110,6 +114,19 @@ def test_given_janitor_crash_after_drop_when_rerunning_then_drop_is_recorded_as_
110114
description="repeated early drop spares a project view built without a fingerprint",
111115
janitor_args=("janitor", "--auto-approve", "--drop-old-name-view", "dev.revenue"),
112116
install_build_fault=fail_view_fingerprint_write,
117+
claiming_builds=_ONE_BUILD,
118+
expected_janitor_fragment=_CLAIMED,
119+
expected_old_name_type="VIEW",
120+
expected_final_facts=("required", "origin_archived", "view_created", "view_dropped"),
121+
),
122+
OldNameJanitorClaimTestCase(
123+
description="destination built after the project's view leaves that view alone",
124+
janitor_args=("janitor", "--auto-approve", "--drop-old-name-view", "dev.revenue"),
125+
install_build_fault=no_build_fault,
126+
claiming_builds=(
127+
("build", "--select", ORIGIN_MODEL),
128+
("build", "--select", DESTINATION_MODEL),
129+
),
113130
expected_janitor_fragment=_CLAIMED,
114131
expected_old_name_type="VIEW",
115132
expected_final_facts=("required", "origin_archived", "view_created", "view_dropped"),
@@ -140,7 +157,9 @@ def test_given_interrupted_early_drop_when_project_builds_the_name_then_janitor_
140157
)
141158
with monkeypatch.context() as patch:
142159
test_case.install_build_fault(patch)
143-
_ = build_ok(project_dir=tmp_path, capsys=capsys)
160+
for args in test_case.claiming_builds:
161+
claiming: CliRun = run_sqb(project_dir=tmp_path, args=args, capsys=capsys)
162+
assert claiming.exit_code == 0, claiming.output
144163

145164
janitor: CliRun = run_sqb(project_dir=tmp_path, args=test_case.janitor_args, capsys=capsys)
146165
old_name_type: str | None = relation_type_in(

0 commit comments

Comments
 (0)