Skip to content

Commit 684316e

Browse files
committed
test: cover plans with an execution time ahead of the prod frontier
Signed-off-by: wtruongdata <quang072000@gmail.com>
1 parent be3a699 commit 684316e

1 file changed

Lines changed: 176 additions & 1 deletion

File tree

tests/core/test_context.py

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1671,6 +1671,176 @@ def test_plan_start_ahead_of_end(copy_to_temp_path):
16711671
context.close()
16721672

16731673

1674+
@pytest.mark.slow
1675+
def test_plan_execution_time_ahead_of_prod_frontier(copy_to_temp_path):
1676+
"""An explicitly provided `execution_time` should be able to extend the plan's default end
1677+
past the recorded prod frontier, since it represents the plan's effective "now". Without
1678+
this, an explicit execution_time beyond the last applied interval is silently ignored and
1679+
the plan reports no changes/no backfill even though new intervals are due.
1680+
1681+
See: https://github.com/SQLMesh/sqlmesh/issues/5640
1682+
"""
1683+
path = copy_to_temp_path("examples/sushi")
1684+
with time_machine.travel("2024-01-02 00:00:00 UTC"):
1685+
context = Context(paths=path, gateway="duckdb_persistent")
1686+
context.plan("prod", no_prompts=True, auto_apply=True)
1687+
assert all(
1688+
i == to_timestamp("2024-01-02")
1689+
for i in context.state_sync.max_interval_end_per_model("prod").values()
1690+
)
1691+
context.close()
1692+
1693+
# No model changes, but a subsequent plan explicitly passes an execution_time that is ahead
1694+
# of the recorded prod frontier (2024-01-02). This mimics running
1695+
# `sqlmesh plan --execution-time '2024-01-05'` a few days later.
1696+
with time_machine.travel("2024-01-06 00:00:00 UTC"):
1697+
context = Context(paths=path, gateway="duckdb_persistent")
1698+
plan = context.plan_builder("prod", execution_time="2024-01-05").build()
1699+
assert plan.requires_backfill
1700+
assert to_timestamp(plan.end) == to_timestamp("2024-01-05")
1701+
context.apply(plan)
1702+
# The seed model isn't backfilled on a cron schedule like the other models, so its
1703+
# recorded interval end doesn't advance to the new execution time (same exclusion as
1704+
# test_plan_seed_model_excluded_from_default_end).
1705+
max_ends = context.state_sync.max_interval_end_per_model("prod")
1706+
assert all(
1707+
i == to_timestamp("2024-01-05")
1708+
for fqn, i in max_ends.items()
1709+
if "waiter_names" not in fqn
1710+
)
1711+
context.close()
1712+
1713+
# Sanity check that the downward clamp is unaffected: an explicit execution_time that is
1714+
# *behind* the recorded prod frontier should still clamp the default end down to it (this is
1715+
# already covered for the dev case by test_plan_execution_time_start_end).
1716+
with time_machine.travel("2024-01-07 00:00:00 UTC"):
1717+
context = Context(paths=path, gateway="duckdb_persistent")
1718+
plan = context.plan_builder("prod", execution_time="2024-01-04").build()
1719+
assert not plan.requires_backfill
1720+
assert to_timestamp(plan.end) == to_timestamp("2024-01-04")
1721+
context.close()
1722+
1723+
1724+
def _write_daily_and_weekly_model_project(tmp_path: Path) -> None:
1725+
"""Minimal 2-model project used to exercise the interaction between execution_time and
1726+
multiple, differently-cadenced models' recorded prod frontiers. A daily and a weekly model
1727+
are used so that, after an initial backfill, they end up with different recorded interval
1728+
ends (the weekly model's frontier lags the daily model's), which is what the original bug
1729+
report's project topology looked like.
1730+
"""
1731+
(tmp_path / "models").mkdir()
1732+
(tmp_path / "config.yaml").write_text(
1733+
"""
1734+
model_defaults:
1735+
dialect: duckdb
1736+
"""
1737+
)
1738+
(tmp_path / "models" / "daily_model.sql").write_text(
1739+
"""
1740+
MODEL (
1741+
name daily_model,
1742+
kind INCREMENTAL_BY_TIME_RANGE (
1743+
time_column start_dt
1744+
),
1745+
start '2024-01-01',
1746+
cron '@daily'
1747+
);
1748+
1749+
select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt;
1750+
"""
1751+
)
1752+
(tmp_path / "models" / "weekly_model.sql").write_text(
1753+
"""
1754+
MODEL (
1755+
name weekly_model,
1756+
kind INCREMENTAL_BY_TIME_RANGE (
1757+
time_column start_dt
1758+
),
1759+
start '2024-01-01',
1760+
cron '@weekly'
1761+
);
1762+
1763+
select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt;
1764+
"""
1765+
)
1766+
1767+
1768+
def _missing_intervals_by_name(plan: Plan) -> t.Dict[str, t.Tuple[t.Tuple[int, int], ...]]:
1769+
return {si.snapshot_id.name: tuple(si.merged_intervals) for si in plan.missing_intervals}
1770+
1771+
1772+
def test_plan_execution_time_ahead_of_prod_frontier_matches_run_for_all_models(tmp_path: Path):
1773+
"""Locks in that raising `max_interval_end_per_model` for an explicitly provided
1774+
`execution_time` sweeps in *every* model with a recorded prod frontier, not just
1775+
modified/selected ones. This is intentional, not an oversight: it's what makes a plain,
1776+
unscoped `sqlmesh plan --execution-time X` in prod report the exact same missing intervals
1777+
that `sqlmesh plan --run --execution-time X` would report at the same simulated time - the
1778+
parity the original bug report asks for (https://github.com/SQLMesh/sqlmesh/issues/5640,
1779+
which cites `--run` as already having the correct behavior). A future change that "scopes"
1780+
the raise down to fewer models would silently break this `plan`/`plan --run` parity and
1781+
should fail this test.
1782+
"""
1783+
_write_daily_and_weekly_model_project(tmp_path)
1784+
context = Context(paths=tmp_path)
1785+
1786+
# Catch both models up, but to different frontiers: the weekly model's cadence means its
1787+
# last fully-elapsed interval (2024-01-14) is a day behind the daily model's (2024-01-15).
1788+
context.plan(auto_apply=True, no_prompts=True, execution_time="2024-01-15 00:00:01")
1789+
max_ends = context.state_sync.max_interval_end_per_model("prod")
1790+
assert max_ends['"daily_model"'] == to_timestamp("2024-01-15")
1791+
assert max_ends['"weekly_model"'] == to_timestamp("2024-01-14")
1792+
1793+
# A plain, unscoped prod plan (no model changes, no --select-model/--backfill-model, no
1794+
# restatement) with execution_time set well ahead of both frontiers and no explicit end.
1795+
execution_time = "2024-01-25 00:00:01"
1796+
plan = context.plan_builder("prod", execution_time=execution_time).build()
1797+
assert plan.requires_backfill
1798+
assert to_timestamp(plan.end) == to_timestamp(execution_time)
1799+
1800+
missing = _missing_intervals_by_name(plan)
1801+
# Both models show missing intervals, even though only the daily model's cadence would
1802+
# naturally put it "due" first - the weekly model is swept in too.
1803+
assert set(missing) == {'"daily_model"', '"weekly_model"'}
1804+
1805+
# An equivalent `plan --run` at the same execution_time computes missing intervals with no
1806+
# caps at all. If it matches exactly, that confirms the plain-plan raise reproduces the
1807+
# `--run` result rather than under- or over-shooting it.
1808+
run_plan = context.plan_builder("prod", execution_time=execution_time, run=True).build()
1809+
assert run_plan.requires_backfill
1810+
assert _missing_intervals_by_name(run_plan) == missing
1811+
1812+
1813+
def test_plan_execution_time_ahead_of_prod_frontier_with_explicit_end(tmp_path: Path):
1814+
"""When the user provides an explicit `end` alongside `execution_time`, the new
1815+
`end is None` guard means the per-model interval end caps are never raised towards
1816+
`execution_time` in the first place - the plan's end is exactly the explicit end, and
1817+
the pre-existing `PlanBuilder.override_end` behavior (which drops the per-model caps
1818+
entirely once `end` is explicit, see sqlmesh/core/plan/builder.py) takes over instead, same
1819+
as it did before this fix. This locks in that combining explicit `end` with a far-future
1820+
`execution_time` cannot make the backfill silently jump past the requested end.
1821+
"""
1822+
_write_daily_and_weekly_model_project(tmp_path)
1823+
context = Context(paths=tmp_path)
1824+
context.plan(auto_apply=True, no_prompts=True, execution_time="2024-01-15 00:00:01")
1825+
1826+
# Explicit start/end is only allowed for dev plans (or prod plans with restatements), so use
1827+
# a dev plan here; the guard being exercised doesn't depend on which of those it is.
1828+
plan = context.plan_builder(
1829+
"dev",
1830+
execution_time="2024-01-30 00:00:01",
1831+
end="2024-01-21",
1832+
include_unmodified=True,
1833+
).build()
1834+
assert plan.requires_backfill
1835+
# The plan's end matches the explicit end exactly - it is not raised towards execution_time.
1836+
assert to_timestamp(plan.end) == to_timestamp("2024-01-21")
1837+
1838+
# Missing intervals for both models stop at the explicit end and never reach anywhere near
1839+
# the much-later execution_time.
1840+
for si in plan.missing_intervals:
1841+
assert si.merged_intervals[-1][1] <= to_timestamp("2024-01-22")
1842+
1843+
16741844
@pytest.mark.slow
16751845
def test_plan_seed_model_excluded_from_default_end(copy_to_temp_path: t.Callable):
16761846
path = copy_to_temp_path("examples/sushi")
@@ -3254,7 +3424,12 @@ def test_plan_min_intervals(tmp_path: Path):
32543424
plan = context.plan(execution_time=current_time)
32553425

32563426
assert to_datetime(plan.start) == to_datetime("2020-01-01 00:00:00")
3257-
assert to_datetime(plan.end) == to_datetime("2020-02-01 00:00:00")
3427+
# the explicitly provided execution_time is 1 second past the day-aligned frontier of the
3428+
# other, already-caught-up models, so the plan end now matches it exactly instead of being
3429+
# capped at that frontier (see https://github.com/SQLMesh/sqlmesh/issues/5640). This
3430+
# doesn't change which intervals are missing below since they're still bucketed by each
3431+
# model's own cron.
3432+
assert to_datetime(plan.end) == to_datetime("2020-02-01 00:00:01")
32583433
assert to_datetime(plan.execution_time) == to_datetime("2020-02-01 00:00:01")
32593434

32603435
def _get_missing_intervals(plan: Plan, name: str) -> t.List[t.Tuple[datetime, datetime]]:

0 commit comments

Comments
 (0)