Repository: TobikoData/sqlmesh
Pull request #2424 (2024-04-11 16:57:00)

Fix: Use end-bounded missing intervals when checking for gaps during the promotion
We should ignore `lookback` and `allow_partials` attributes when checking the missing intervals for gaps during the promotion process. Setting `end_bounded` to `True` achieves exactly this.


Patch:
diff --git a/docs/concepts/models/overview.md b/docs/concepts/models/overview.md
index 6ca711ea..3544ade0 100644
--- a/docs/concepts/models/overview.md
+++ b/docs/concepts/models/overview.md
@@ -169,7 +169,7 @@ Name is ***required*** and must be ***unique***.
 - End is used to determine the latest time needed to process the model. It can be an absolute date/time (`2022-01-01`), or a relative one (`1 year ago`).
 
 ### cron
-- Cron is used to schedule your model to process or refresh at a certain interval. It uses [croniter](https://github.com/kiorky/croniter) under the hood, so expressions such as `@daily` can be used.
+- Cron is used to schedule your model to process or refresh at a certain interval. It accepts a [cron expression](https://en.wikipedia.org/wiki/Cron) or any of `@hourly`, `@daily`, `@weekly`, or `@monthly`.
 
 ### interval_unit
 - Interval unit determines the granularity of data intervals for this model. By default the interval unit is automatically derived from the `cron` expression. Supported values are: `year`, `month`, `day`, `hour`, `half_hour`, `quarter_hour`, and `five_minute`.
diff --git a/sqlmesh/core/state_sync/common.py b/sqlmesh/core/state_sync/common.py
index 8a22e9ea..b0b3d2a1 100644
--- a/sqlmesh/core/state_sync/common.py
+++ b/sqlmesh/core/state_sync/common.py
@@ -293,7 +293,9 @@ class CommonStateSyncMixin(StateSync):
                 end = prev_snapshot.intervals[-1][1]
 
                 if start < end:
-                    missing_intervals = target_snapshot.missing_intervals(start, end)
+                    missing_intervals = target_snapshot.missing_intervals(
+                        start, end, end_bounded=True
+                    )
 
                     if missing_intervals:
                         raise SQLMeshError(


Test patch:
diff --git a/tests/core/test_state_sync.py b/tests/core/test_state_sync.py
index 397d87c4..8187b910 100644
--- a/tests/core/test_state_sync.py
+++ b/tests/core/test_state_sync.py
@@ -6,6 +6,7 @@ from unittest.mock import call, patch
 import duckdb
 import pandas as pd
 import pytest
+from freezegun import freeze_time
 from pytest_mock.plugin import MockerFixture
 from sqlglot import exp
 
@@ -833,6 +834,36 @@ def test_promote_snapshots_no_gaps(state_sync: EngineAdapterStateSync, make_snap
     )
 
 
+@freeze_time("2023-01-08 16:00:00")
+def test_promote_snapshots_no_gaps_lookback(
+    state_sync: EngineAdapterStateSync, make_snapshot: t.Callable
+):
+    model = SqlModel(
+        name="a",
+        cron="@hourly",
+        query=parse_one("select 1, ds"),
+        kind=IncrementalByTimeRangeKind(time_column="ds", lookback=1),
+        start="2023-01-01",
+    )
+
+    snapshot = make_snapshot(model, version="a")
+    snapshot.change_category = SnapshotChangeCategory.BREAKING
+    state_sync.push_snapshots([snapshot])
+    state_sync.add_interval(snapshot, "2023-01-01", "2023-01-08 15:00:00")
+    promote_snapshots(state_sync, [snapshot], "prod", no_gaps=True)
+
+    assert now_timestamp() == to_timestamp("2023-01-08 16:00:00")
+
+    new_snapshot_same_version = make_snapshot(model, version="b")
+    new_snapshot_same_version.change_category = SnapshotChangeCategory.BREAKING
+    new_snapshot_same_version.fingerprint = snapshot.fingerprint.copy(
+        update={"data_hash": "new_snapshot_same_version"}
+    )
+    state_sync.push_snapshots([new_snapshot_same_version])
+    state_sync.add_interval(new_snapshot_same_version, "2023-01-01", "2023-01-08 15:00:00")
+    promote_snapshots(state_sync, [new_snapshot_same_version], "prod", no_gaps=True)
+
+
 def test_finalize(state_sync: EngineAdapterStateSync, make_snapshot: t.Callable):
     snapshot_a = make_snapshot(
         SqlModel(