Skip to content

Commit 0c421d0

Browse files
committed
Fix seed PR backfill interval overrides
Signed-off-by: eakmanrq <6326532+eakmanrq@users.noreply.github.com>
1 parent bf84bce commit 0c421d0

File tree

2 files changed

+67
-2
lines changed

2 files changed

+67
-2
lines changed

sqlmesh/core/context.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,6 +1666,7 @@ def plan_builder(
16661666
# This ensures that no models outside the impacted sub-DAG(s) will be backfilled unexpectedly.
16671667
backfill_models = modified_model_names or None
16681668

1669+
plan_execution_time = execution_time or now()
16691670
max_interval_end_per_model = None
16701671
default_start, default_end = None, None
16711672
if not run:
@@ -1680,17 +1681,31 @@ def plan_builder(
16801681
max_interval_end_per_model,
16811682
backfill_models,
16821683
modified_model_names,
1683-
execution_time or now(),
1684+
plan_execution_time,
16841685
)
16851686

1687+
if (
1688+
start
1689+
and default_end
1690+
and to_datetime(start, relative_base=to_datetime(plan_execution_time))
1691+
> to_datetime(default_end)
1692+
):
1693+
# If the requested start is newer than prod's latest interval end, fall back to execution time
1694+
# instead of forcing an invalid [start, default_end] range.
1695+
default_start, default_end = None, None
1696+
16861697
# Refresh snapshot intervals to ensure that they are up to date with values reflected in the max_interval_end_per_model.
16871698
self.state_sync.refresh_snapshot_intervals(context_diff.snapshots.values())
1699+
max_interval_end_per_model = self._filter_stale_end_overrides(
1700+
max_interval_end_per_model,
1701+
context_diff.snapshots_by_name,
1702+
)
16881703

16891704
start_override_per_model = self._calculate_start_override_per_model(
16901705
min_intervals,
16911706
start or default_start,
16921707
end or default_end,
1693-
execution_time or now(),
1708+
plan_execution_time,
16941709
backfill_models,
16951710
snapshots,
16961711
max_interval_end_per_model,
@@ -3181,6 +3196,20 @@ def _get_max_interval_end_per_model(
31813196
).items()
31823197
}
31833198

3199+
@staticmethod
3200+
def _filter_stale_end_overrides(
3201+
max_interval_end_per_model: t.Dict[str, datetime],
3202+
snapshots_by_name: t.Dict[str, Snapshot],
3203+
) -> t.Dict[str, datetime]:
3204+
# Drop stale interval ends for snapshots whose new versions have no intervals yet. Otherwise the old
3205+
# prod end is reused as an end_override, causing missing_intervals() to skip the new snapshot entirely
3206+
# when the requested start is newer than that stale end.
3207+
return {
3208+
model_fqn: end
3209+
for model_fqn, end in max_interval_end_per_model.items()
3210+
if model_fqn not in snapshots_by_name or snapshots_by_name[model_fqn].intervals
3211+
}
3212+
31843213
@staticmethod
31853214
def _get_models_for_interval_end(
31863215
snapshots: t.Dict[str, Snapshot], backfill_models: t.Set[str]

tests/core/test_context.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,42 @@ def test_plan_seed_model_excluded_from_default_end(copy_to_temp_path: t.Callable
12231223
context.close()
12241224

12251225

1226+
@pytest.mark.slow
1227+
def test_seed_model_pr_plan_with_stale_prod_intervals(copy_to_temp_path: t.Callable):
1228+
path = copy_to_temp_path("examples/sushi")
1229+
1230+
with time_machine.travel("2024-06-01 00:00:00 UTC"):
1231+
context = Context(paths=path, gateway="duckdb_persistent")
1232+
context.plan("prod", no_prompts=True, auto_apply=True)
1233+
context.close()
1234+
1235+
with time_machine.travel("2026-04-13 00:00:00 UTC"):
1236+
context = Context(paths=path, gateway="duckdb_persistent")
1237+
1238+
model = context.get_model("sushi.waiter_names").copy()
1239+
model.seed.content += "10,Trey\n"
1240+
context.upsert_model(model)
1241+
1242+
plan = context.plan("dev", start="2 months ago", no_prompts=True)
1243+
missing_interval_names = {si.snapshot_id.name for si in plan.missing_intervals}
1244+
1245+
assert plan.user_provided_flags == {"start": "2 months ago"}
1246+
assert plan.provided_end is None
1247+
assert to_timestamp(plan.start) == to_timestamp("2026-02-13")
1248+
assert to_timestamp(plan.end) == to_timestamp("2026-04-13")
1249+
assert any("waiter_names" in name for name in missing_interval_names)
1250+
assert any("waiter_as_customer_by_day" in name for name in missing_interval_names)
1251+
1252+
context.apply(plan)
1253+
1254+
promoted_snapshot_names = {
1255+
snapshot.name for snapshot in context.state_sync.get_environment("dev").promoted_snapshots
1256+
}
1257+
assert any("waiter_names" in name for name in promoted_snapshot_names)
1258+
assert any("waiter_as_customer_by_day" in name for name in promoted_snapshot_names)
1259+
context.close()
1260+
1261+
12261262
@pytest.mark.slow
12271263
def test_schema_error_no_default(sushi_context_pre_scheduling) -> None:
12281264
context = sushi_context_pre_scheduling

0 commit comments

Comments
 (0)