Skip to content

Commit 434be40

Browse files
authored
Fix: Always treat forward-only models as non-deployable (#3510)
1 parent c512e63 commit 434be40

File tree

5 files changed

+86
-25
lines changed

5 files changed

+86
-25
lines changed

sqlmesh/core/scheduler.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,9 @@ def merged_missing_intervals(
116116
validate_date_range(start, end)
117117

118118
snapshots: t.Collection[Snapshot] = self.snapshot_per_version.values()
119-
if selected_snapshots is not None:
120-
snapshots = [s for s in snapshots if s.name in selected_snapshots]
121-
122119
self.state_sync.refresh_snapshot_intervals(snapshots)
123120

124-
return compute_interval_params(
121+
snapshots_to_intervals = compute_interval_params(
125122
snapshots,
126123
start=start or earliest_start_date(snapshots),
127124
end=end or now(),
@@ -132,6 +129,13 @@ def merged_missing_intervals(
132129
ignore_cron=ignore_cron,
133130
end_bounded=end_bounded,
134131
)
132+
# Filtering snapshots after computing missing intervals because we need all snapshots in order
133+
# to correctly infer start dates.
134+
if selected_snapshots is not None:
135+
snapshots_to_intervals = {
136+
s: i for s, i in snapshots_to_intervals.items() if s.name in selected_snapshots
137+
}
138+
return snapshots_to_intervals
135139

136140
def evaluate(
137141
self,

sqlmesh/core/snapshot/definition.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,13 +1340,7 @@ def _visit(node: SnapshotId, deployable: bool = True) -> None:
13401340

13411341
if deployable and node in snapshots:
13421342
snapshot = snapshots[node]
1343-
# Capture uncategorized snapshot which represents a forward-only model.
1344-
is_uncategorized_forward_only_model = (
1345-
snapshot.change_category is None
1346-
and snapshot.previous_versions
1347-
and snapshot.is_model
1348-
and snapshot.model.forward_only
1349-
)
1343+
is_forward_only_model = snapshot.is_model and snapshot.model.forward_only
13501344

13511345
is_valid_start = (
13521346
snapshot.is_valid_start(
@@ -1359,7 +1353,7 @@ def _visit(node: SnapshotId, deployable: bool = True) -> None:
13591353
if (
13601354
snapshot.is_forward_only
13611355
or snapshot.is_indirect_non_breaking
1362-
or is_uncategorized_forward_only_model
1356+
or is_forward_only_model
13631357
or not is_valid_start
13641358
):
13651359
# FORWARD_ONLY and INDIRECT_NON_BREAKING snapshots are not deployable by nature.
@@ -1372,8 +1366,7 @@ def _visit(node: SnapshotId, deployable: bool = True) -> None:
13721366
else:
13731367
this_deployable = True
13741368
children_deployable = is_valid_start and not (
1375-
snapshot.is_paused
1376-
and (snapshot.is_forward_only or is_uncategorized_forward_only_model)
1369+
snapshot.is_paused and (snapshot.is_forward_only or is_forward_only_model)
13771370
)
13781371
else:
13791372
this_deployable, children_deployable = False, False

tests/core/test_integration.py

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,6 +1774,72 @@ def test_new_forward_only_model_concurrent_versions(init_and_plan_context: t.Cal
17741774
assert df.to_dict() == {"ds": {0: "2023-01-07"}, "b": {0: None}}
17751775

17761776

1777+
@freeze_time("2023-01-08 15:00:00")
1778+
def test_new_forward_only_model_same_dev_environment(init_and_plan_context: t.Callable):
1779+
context, plan = init_and_plan_context("examples/sushi")
1780+
context.apply(plan)
1781+
1782+
new_model_expr = d.parse(
1783+
"""
1784+
MODEL (
1785+
name memory.sushi.new_model,
1786+
kind INCREMENTAL_BY_TIME_RANGE (
1787+
time_column ds,
1788+
forward_only TRUE,
1789+
on_destructive_change 'allow',
1790+
),
1791+
);
1792+
1793+
SELECT '2023-01-07' AS ds, 1 AS a;
1794+
"""
1795+
)
1796+
new_model = load_sql_based_model(new_model_expr)
1797+
1798+
# Add the first version of the model and apply it to dev.
1799+
context.upsert_model(new_model)
1800+
snapshot_a = context.get_snapshot(new_model.name)
1801+
plan_a = context.plan("dev", no_prompts=True)
1802+
snapshot_a = plan_a.snapshots[snapshot_a.snapshot_id]
1803+
1804+
assert snapshot_a.snapshot_id in plan_a.context_diff.new_snapshots
1805+
assert snapshot_a.snapshot_id in plan_a.context_diff.added
1806+
assert snapshot_a.change_category == SnapshotChangeCategory.BREAKING
1807+
1808+
context.apply(plan_a)
1809+
1810+
df = context.fetchdf("SELECT * FROM memory.sushi__dev.new_model")
1811+
assert df.to_dict() == {"ds": {0: "2023-01-07"}, "a": {0: 1}}
1812+
1813+
new_model_alt_expr = d.parse(
1814+
"""
1815+
MODEL (
1816+
name memory.sushi.new_model,
1817+
kind INCREMENTAL_BY_TIME_RANGE (
1818+
time_column ds,
1819+
forward_only TRUE,
1820+
on_destructive_change 'allow',
1821+
),
1822+
);
1823+
1824+
SELECT '2023-01-07' AS ds, 1 AS b;
1825+
"""
1826+
)
1827+
new_model_alt = load_sql_based_model(new_model_alt_expr)
1828+
1829+
# Add the second version of the model and apply it to the same environment.
1830+
context.upsert_model(new_model_alt)
1831+
snapshot_b = context.get_snapshot(new_model_alt.name)
1832+
1833+
context.invalidate_environment("dev", sync=True)
1834+
plan_b = context.plan("dev", no_prompts=True)
1835+
snapshot_b = plan_b.snapshots[snapshot_b.snapshot_id]
1836+
1837+
context.apply(plan_b)
1838+
1839+
df = context.fetchdf("SELECT * FROM memory.sushi__dev.new_model").replace({np.nan: None})
1840+
assert df.to_dict() == {"ds": {0: "2023-01-07"}, "b": {0: 1}}
1841+
1842+
17771843
def test_plan_twice_with_star_macro_yields_no_diff(tmp_path: Path):
17781844
init_example_project(tmp_path, dialect="duckdb")
17791845

@@ -2564,7 +2630,7 @@ def get_default_catalog_and_non_tables(
25642630
) = get_default_catalog_and_non_tables(metadata, context.default_catalog)
25652631
assert len(prod_views) == 13
25662632
assert len(dev_views) == 0
2567-
assert len(user_default_tables) == 13
2633+
assert len(user_default_tables) == 16
25682634
assert state_metadata.schemas == ["sqlmesh"]
25692635
assert {x.sql() for x in state_metadata.qualified_tables}.issuperset(
25702636
{
@@ -2583,7 +2649,7 @@ def get_default_catalog_and_non_tables(
25832649
) = get_default_catalog_and_non_tables(metadata, context.default_catalog)
25842650
assert len(prod_views) == 13
25852651
assert len(dev_views) == 13
2586-
assert len(user_default_tables) == 13
2652+
assert len(user_default_tables) == 16
25872653
assert len(non_default_tables) == 0
25882654
assert state_metadata.schemas == ["sqlmesh"]
25892655
assert {x.sql() for x in state_metadata.qualified_tables}.issuperset(
@@ -2603,7 +2669,7 @@ def get_default_catalog_and_non_tables(
26032669
) = get_default_catalog_and_non_tables(metadata, context.default_catalog)
26042670
assert len(prod_views) == 13
26052671
assert len(dev_views) == 26
2606-
assert len(user_default_tables) == 13
2672+
assert len(user_default_tables) == 16
26072673
assert len(non_default_tables) == 0
26082674
assert state_metadata.schemas == ["sqlmesh"]
26092675
assert {x.sql() for x in state_metadata.qualified_tables}.issuperset(
@@ -2624,7 +2690,7 @@ def get_default_catalog_and_non_tables(
26242690
) = get_default_catalog_and_non_tables(metadata, context.default_catalog)
26252691
assert len(prod_views) == 13
26262692
assert len(dev_views) == 13
2627-
assert len(user_default_tables) == 13
2693+
assert len(user_default_tables) == 16
26282694
assert len(non_default_tables) == 0
26292695
assert state_metadata.schemas == ["sqlmesh"]
26302696
assert {x.sql() for x in state_metadata.qualified_tables}.issuperset(

tests/core/test_snapshot.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,17 +1734,15 @@ def test_deployability_index_categorized_forward_only_model(make_snapshot):
17341734
snapshot_b.parents = (snapshot_a.snapshot_id,)
17351735
snapshot_b.categorize_as(SnapshotChangeCategory.METADATA)
17361736

1737-
# The fact that the model is forward only should be ignored if an actual category
1738-
# has been assigned.
17391737
deployability_index = DeployabilityIndex.create(
17401738
{s.snapshot_id: s for s in [snapshot_a, snapshot_b]}
17411739
)
17421740

1743-
assert deployability_index.is_deployable(snapshot_a)
1744-
assert deployability_index.is_deployable(snapshot_b)
1741+
assert not deployability_index.is_deployable(snapshot_a)
1742+
assert not deployability_index.is_deployable(snapshot_b)
17451743

1746-
assert deployability_index.is_representative(snapshot_a)
1747-
assert deployability_index.is_representative(snapshot_b)
1744+
assert not deployability_index.is_representative(snapshot_a)
1745+
assert not deployability_index.is_representative(snapshot_b)
17481746

17491747

17501748
def test_deployability_index_missing_parent(make_snapshot):

tests/integrations/jupyter/test_magics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ def test_plan(
291291

292292
# TODO: Should this be going to stdout? This is printing the status updates for when each batch finishes for
293293
# the models and how long it took
294-
assert len(output.stdout.strip().split("\n")) == 22
294+
assert len(output.stdout.strip().split("\n")) == 23
295295
assert not output.stderr
296296
assert len(output.outputs) == 4
297297
text_output = convert_all_html_output_to_text(output)

0 commit comments

Comments
 (0)