Skip to content

Commit a4cf88c

Browse files
committed
Ensured that representative but not deployable snapshots do not have any intervals and are a pure pass-through of prod.
Signed-off-by: davem-bis <68955845+davem-bis@users.noreply.github.com>
1 parent 2c30f83 commit a4cf88c

3 files changed

Lines changed: 203 additions & 43 deletions

File tree

‎sqlmesh/core/snapshot/definition.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,13 @@ def missing_intervals(
10471047
return []
10481048

10491049
deployability_index = deployability_index or DeployabilityIndex.all_deployable()
1050+
# If a snapshot is representative but not deployable (e.g. INDIRECT_NON_BREAKING snapshots) then it will only
1051+
# ever refer to the prod physical table, and so should never have any missing intervals.
1052+
if deployability_index.is_representative(self) and not deployability_index.is_deployable(
1053+
self
1054+
):
1055+
return []
1056+
10501057
intervals = (
10511058
self.intervals if deployability_index.is_representative(self) else self.dev_intervals
10521059
)
@@ -2063,6 +2070,13 @@ def missing_intervals(
20632070
if not snapshot.evaluatable:
20642071
continue
20652072

2073+
# If a snapshot is representative but not deployable (e.g. INDIRECT_NON_BREAKING snapshots) then it will only
2074+
# ever refer to the prod physical table, and so should never have any missing intervals.
2075+
if deployability_index.is_representative(
2076+
snapshot
2077+
) and not deployability_index.is_deployable(snapshot):
2078+
continue
2079+
20662080
snapshot_start_date = start_override_per_model.get(snapshot.name, start_dt)
20672081
snapshot_end_date: TimeLike = end_date
20682082

‎tests/core/test_snapshot.py‎

Lines changed: 144 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,71 @@ def test_missing_intervals_start_override_per_model(make_snapshot: t.Callable[..
638638
]
639639

640640

641+
def test__missing_intervals__representative_snapshot_prod_intervals_returned(snapshot: Snapshot):
642+
# Arrange
643+
snapshot.add_interval(start="2020-01-01", end="2020-01-01", is_dev=False)
644+
snapshot.add_interval(start="2020-01-02", end="2020-01-02", is_dev=False)
645+
snapshot.add_interval(start="2020-01-01", end="2020-01-01", is_dev=True)
646+
647+
snapshot.categorize_as(SnapshotChangeCategory.BREAKING)
648+
deployability_index = DeployabilityIndex.create([snapshot])
649+
650+
# Act
651+
missing_intervals = snapshot.missing_intervals(
652+
start="2020-01-01", end="2020-01-03", deployability_index=deployability_index
653+
)
654+
655+
# Assert
656+
assert deployability_index.is_representative(snapshot)
657+
assert missing_intervals == [
658+
(to_timestamp("2020-01-03"), to_timestamp("2020-01-04")),
659+
]
660+
661+
662+
def test__missing_intervals__non_representative_snapshot_dev_intervals_returned(snapshot: Snapshot):
663+
# Arrange
664+
snapshot.add_interval(start="2020-01-01", end="2020-01-01", is_dev=False)
665+
snapshot.add_interval(start="2020-01-02", end="2020-01-02", is_dev=False)
666+
snapshot.add_interval(start="2020-01-01", end="2020-01-01", is_dev=True)
667+
668+
snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True)
669+
deployability_index = DeployabilityIndex.create([snapshot])
670+
671+
# Act
672+
missing_intervals = snapshot.missing_intervals(
673+
start="2020-01-01", end="2020-01-03", deployability_index=deployability_index
674+
)
675+
676+
# Assert
677+
assert not deployability_index.is_representative(snapshot)
678+
assert missing_intervals == [
679+
(to_timestamp("2020-01-02"), to_timestamp("2020-01-03")), # Not missing from prod intervals
680+
(to_timestamp("2020-01-03"), to_timestamp("2020-01-04")),
681+
]
682+
683+
684+
def test__missing_intervals__representative_but_non_deployable_snapshot_no_intervals_returned(
685+
snapshot: Snapshot,
686+
):
687+
# Arrange
688+
snapshot.add_interval(start="2020-01-01", end="2020-01-01", is_dev=False)
689+
snapshot.add_interval(start="2020-01-02", end="2020-01-02", is_dev=False)
690+
snapshot.add_interval(start="2020-01-01", end="2020-01-01", is_dev=True)
691+
692+
snapshot.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING)
693+
deployability_index = DeployabilityIndex.create([snapshot])
694+
695+
# Act
696+
missing_intervals = snapshot.missing_intervals(
697+
start="2020-01-01", end="2020-01-03", deployability_index=deployability_index
698+
)
699+
700+
# Assert
701+
assert deployability_index.is_representative(snapshot)
702+
assert not deployability_index.is_deployable(snapshot)
703+
assert missing_intervals == []
704+
705+
641706
def test_incremental_time_self_reference(make_snapshot):
642707
snapshot = make_snapshot(
643708
SqlModel(
@@ -2407,63 +2472,101 @@ def test_earliest_start_date(sushi_context: Context):
24072472

24082473

24092474
def test_deployability_index(make_snapshot):
2410-
snapshot_a = make_snapshot(SqlModel(name="a", query=parse_one("SELECT 1")))
2411-
snapshot_a.categorize_as(SnapshotChangeCategory.BREAKING)
2475+
# Breaking change - should be both deployable / representative
2476+
snapshot_breaking = make_snapshot(SqlModel(name="breaking", query=parse_one("SELECT 1")))
2477+
snapshot_breaking.categorize_as(SnapshotChangeCategory.BREAKING)
24122478

2413-
snapshot_b = make_snapshot(SqlModel(name="b", query=parse_one("SELECT 1")))
2414-
snapshot_b.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True)
2415-
snapshot_b.parents = (snapshot_a.snapshot_id,)
2479+
# Forward only breaking change - cannot be deployable / representative due to forward only
2480+
snapshot_breaking_forward_only = make_snapshot(
2481+
SqlModel(name="forward_only", query=parse_one("SELECT 1"))
2482+
)
2483+
snapshot_breaking_forward_only.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True)
2484+
snapshot_breaking_forward_only.parents = (snapshot_breaking.snapshot_id,)
24162485

2417-
snapshot_c = make_snapshot(SqlModel(name="c", query=parse_one("SELECT 1")))
2418-
snapshot_c.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING)
2419-
snapshot_c.parents = (snapshot_b.snapshot_id,)
2486+
# Indirect breaking - cannot be deployable / representative due to forward only non-representative parent
2487+
snapshot_indirect_breaking_non_representative_parent = make_snapshot(
2488+
SqlModel(name="indirect_breaking_non_representative_parent", query=parse_one("SELECT 1"))
2489+
)
2490+
snapshot_indirect_breaking_non_representative_parent.categorize_as(
2491+
SnapshotChangeCategory.INDIRECT_BREAKING
2492+
)
2493+
snapshot_indirect_breaking_non_representative_parent.parents = (
2494+
snapshot_breaking_forward_only.snapshot_id,
2495+
)
24202496

2421-
snapshot_d = make_snapshot(SqlModel(name="d", query=parse_one("SELECT 1")))
2422-
snapshot_d.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING)
2423-
snapshot_d.parents = (snapshot_b.snapshot_id, snapshot_a.snapshot_id)
2497+
# Indirect breaking - can be deployable / representative due to forward only representative parent
2498+
snapshot_indirect_breaking_representative_parent = make_snapshot(
2499+
SqlModel(name="indirect_breaking_representative_parent", query=parse_one("SELECT 1"))
2500+
)
2501+
snapshot_indirect_breaking_representative_parent.categorize_as(
2502+
SnapshotChangeCategory.INDIRECT_BREAKING
2503+
)
2504+
snapshot_indirect_breaking_representative_parent.parents = (snapshot_breaking.snapshot_id,)
24242505

2425-
snapshot_e = make_snapshot(SqlModel(name="e", query=parse_one("SELECT 1")))
2426-
snapshot_e.categorize_as(SnapshotChangeCategory.NON_BREAKING)
2506+
# Non breaking - deployable / representative due to no breaking changes
2507+
snapshot_non_breaking = make_snapshot(
2508+
SqlModel(name="non_breaking", query=parse_one("SELECT 1"))
2509+
)
2510+
snapshot_non_breaking.categorize_as(SnapshotChangeCategory.NON_BREAKING)
24272511

2428-
snapshot_f = make_snapshot(SqlModel(name="f", query=parse_one("SELECT 1")))
2429-
snapshot_f.categorize_as(SnapshotChangeCategory.INDIRECT_BREAKING)
2430-
snapshot_f.parents = (snapshot_e.snapshot_id, snapshot_a.snapshot_id)
2512+
# Indirect non breaking - can be representative but not deployable
2513+
snapshot_indirect_non_breaking = make_snapshot(
2514+
SqlModel(name="indirect_non_breaking", query=parse_one("SELECT 1"))
2515+
)
2516+
snapshot_indirect_non_breaking.intervals = [
2517+
(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))
2518+
]
2519+
snapshot_indirect_non_breaking.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING)
2520+
snapshot_indirect_non_breaking.parents = (snapshot_non_breaking.snapshot_id,)
24312521

2432-
snapshot_g = make_snapshot(SqlModel(name="g", query=parse_one("SELECT 1")))
2433-
snapshot_g.intervals = [(to_timestamp("2023-01-01"), to_timestamp("2023-01-02"))]
2434-
snapshot_g.categorize_as(SnapshotChangeCategory.INDIRECT_NON_BREAKING)
2435-
snapshot_g.parents = (snapshot_e.snapshot_id,)
2522+
# Breaking with non-representative parent - cannot be deployable due to non-representative parent
2523+
snapshot_breaking_non_representative_parent = make_snapshot(
2524+
SqlModel(name="breaking_non_deployable_parents", query=parse_one("SELECT 1"))
2525+
)
2526+
snapshot_breaking_non_representative_parent.categorize_as(SnapshotChangeCategory.BREAKING)
2527+
snapshot_breaking_non_representative_parent.parents = (
2528+
snapshot_breaking_forward_only.snapshot_id,
2529+
)
24362530

24372531
snapshots = {
24382532
s.snapshot_id: s
24392533
for s in [
2440-
snapshot_a,
2441-
snapshot_b,
2442-
snapshot_c,
2443-
snapshot_d,
2444-
snapshot_e,
2445-
snapshot_f,
2446-
snapshot_g,
2534+
snapshot_breaking,
2535+
snapshot_breaking_forward_only,
2536+
snapshot_indirect_breaking_non_representative_parent,
2537+
snapshot_indirect_breaking_representative_parent,
2538+
snapshot_non_breaking,
2539+
snapshot_indirect_non_breaking,
2540+
snapshot_breaking_non_representative_parent,
24472541
]
24482542
}
24492543

24502544
deployability_index = DeployabilityIndex.create(snapshots)
24512545

2452-
assert deployability_index.is_deployable(snapshot_a)
2453-
assert deployability_index.is_deployable(snapshot_e)
2454-
assert deployability_index.is_deployable(snapshot_f)
2455-
assert not deployability_index.is_deployable(snapshot_g)
2456-
assert not deployability_index.is_deployable(snapshot_b)
2457-
assert not deployability_index.is_deployable(snapshot_c)
2458-
assert not deployability_index.is_deployable(snapshot_d)
2546+
assert deployability_index.is_deployable(snapshot_breaking)
2547+
assert deployability_index.is_representative(snapshot_breaking)
24592548

2460-
assert deployability_index.is_representative(snapshot_a)
2461-
assert deployability_index.is_representative(snapshot_e)
2462-
assert deployability_index.is_representative(snapshot_f)
2463-
assert deployability_index.is_representative(snapshot_g)
2464-
assert not deployability_index.is_representative(snapshot_b)
2465-
assert not deployability_index.is_representative(snapshot_c)
2466-
assert not deployability_index.is_representative(snapshot_d)
2549+
assert not deployability_index.is_deployable(snapshot_breaking_forward_only)
2550+
assert not deployability_index.is_representative(snapshot_breaking_forward_only)
2551+
2552+
assert deployability_index.is_deployable(snapshot_non_breaking)
2553+
assert deployability_index.is_representative(snapshot_non_breaking)
2554+
2555+
assert not deployability_index.is_deployable(
2556+
snapshot_indirect_breaking_non_representative_parent
2557+
)
2558+
assert not deployability_index.is_representative(
2559+
snapshot_indirect_breaking_non_representative_parent
2560+
)
2561+
2562+
assert deployability_index.is_deployable(snapshot_indirect_breaking_representative_parent)
2563+
assert deployability_index.is_representative(snapshot_indirect_breaking_representative_parent)
2564+
2565+
assert not deployability_index.is_deployable(snapshot_indirect_non_breaking)
2566+
assert deployability_index.is_representative(snapshot_indirect_non_breaking)
2567+
2568+
assert not deployability_index.is_deployable(snapshot_breaking_non_representative_parent)
2569+
assert not deployability_index.is_representative(snapshot_breaking_non_representative_parent)
24672570

24682571
all_deployable_index = deployability_index.all_deployable()
24692572
assert all(all_deployable_index.is_deployable(s) for s in snapshots.values())

‎tests/core/test_snapshot_evaluator.py‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def increment_stage_counter(evaluator) -> None:
307307
)
308308

309309

310-
def test_promote(mocker: MockerFixture, adapter_mock, make_snapshot):
310+
def test_promote__representative__prod_physical(mocker: MockerFixture, adapter_mock, make_snapshot):
311311
evaluator = SnapshotEvaluator(adapter_mock)
312312

313313
model = SqlModel(
@@ -319,9 +319,15 @@ def test_promote(mocker: MockerFixture, adapter_mock, make_snapshot):
319319

320320
snapshot = make_snapshot(model)
321321
snapshot.categorize_as(SnapshotChangeCategory.BREAKING)
322+
deployability_index = DeployabilityIndex.create([snapshot])
322323

323-
evaluator.promote([snapshot], EnvironmentNamingInfo(name="test_env"))
324+
evaluator.promote(
325+
target_snapshots=[snapshot],
326+
environment_naming_info=EnvironmentNamingInfo(name="test_env"),
327+
deployability_index=deployability_index,
328+
)
324329

330+
assert deployability_index.is_representative(snapshot)
325331
adapter_mock.transaction.assert_called()
326332
adapter_mock.session.assert_called()
327333
adapter_mock.create_schema.assert_called_once_with(to_schema("test_schema__test_env"))
@@ -336,6 +342,43 @@ def test_promote(mocker: MockerFixture, adapter_mock, make_snapshot):
336342
)
337343

338344

345+
def test_promote__non_representative__dev_physical(
346+
mocker: MockerFixture, adapter_mock, make_snapshot
347+
):
348+
evaluator = SnapshotEvaluator(adapter_mock)
349+
350+
model = SqlModel(
351+
name="test_schema.test_model",
352+
kind=IncrementalByTimeRangeKind(time_column="a"),
353+
storage_format="parquet",
354+
query=parse_one("SELECT a FROM tbl WHERE ds BETWEEN @start_ds and @end_ds"),
355+
)
356+
357+
snapshot = make_snapshot(model)
358+
snapshot.categorize_as(SnapshotChangeCategory.BREAKING, forward_only=True)
359+
deployability_index = DeployabilityIndex.create([snapshot])
360+
361+
evaluator.promote(
362+
target_snapshots=[snapshot],
363+
environment_naming_info=EnvironmentNamingInfo(name="test_env"),
364+
deployability_index=deployability_index,
365+
)
366+
367+
assert not deployability_index.is_representative(snapshot)
368+
adapter_mock.transaction.assert_called()
369+
adapter_mock.session.assert_called()
370+
adapter_mock.create_schema.assert_called_once_with(to_schema("test_schema__test_env"))
371+
adapter_mock.create_view.assert_called_once_with(
372+
"test_schema__test_env.test_model",
373+
parse_one(
374+
f"SELECT * FROM sqlmesh__test_schema.test_schema__test_model__{snapshot.version}__dev"
375+
),
376+
table_description=None,
377+
column_descriptions=None,
378+
view_properties={},
379+
)
380+
381+
339382
def test_demote(mocker: MockerFixture, adapter_mock, make_snapshot):
340383
evaluator = SnapshotEvaluator(adapter_mock)
341384

0 commit comments

Comments
 (0)