diff --git a/sqlmesh/core/plan/builder.py b/sqlmesh/core/plan/builder.py index 3dd74755d3..7918556bad 100644 --- a/sqlmesh/core/plan/builder.py +++ b/sqlmesh/core/plan/builder.py @@ -155,6 +155,7 @@ def __init__( self._backfill_models = backfill_models self._end = end or default_end + self._default_start = default_start self._apply = apply self._console = console or get_console() self._choices: t.Dict[SnapshotId, SnapshotChangeCategory] = {} @@ -802,6 +803,25 @@ def _ensure_valid_date_range(self) -> None: f"Plan end date: '{time_like_to_str(end)}' cannot be in the future (execution time: '{time_like_to_str(self.execution_time)}')" ) + # Validate model-specific start/end dates + if (start := self.start or self._default_start) and (end := self.end): + start_ts = to_datetime(start) + end_ts = to_datetime(end) + if start_ts > end_ts: + models_to_check: t.Set[str] = ( + set(self._backfill_models or []) + | set(self._context_diff.modified_snapshots.keys()) + | {s.name for s in self._context_diff.added} + | set((self._end_override_per_model or {}).keys()) + ) + for model_name in models_to_check: + if snapshot := self._model_fqn_to_snapshot.get(model_name): + if snapshot.node.start is None or to_datetime(snapshot.node.start) > end_ts: + raise PlanError( + f"Model '{model_name}': Start date / time '({time_like_to_str(start_ts)})' can't be greater than end date / time '({time_like_to_str(end_ts)})'.\n" + f"Set the `start` attribute in your project config model defaults to avoid this issue." + ) + def _ensure_no_broken_references(self) -> None: for snapshot in self._context_diff.snapshots.values(): broken_references = { diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 2827981ae7..852f00e760 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -3007,3 +3007,59 @@ def test_uppercase_gateway_external_models(tmp_path): assert len(uppercase_in_yaml_models) == 1, ( f"External model with uppercase gateway in YAML should be found. Found {len(uppercase_in_yaml_models)} models" ) + + +def test_plan_no_start_configured(): + context = Context(config=Config()) + context.upsert_model( + load_sql_based_model( + parse( + """ + MODEL( + name db.xvg, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds + ), + cron '@daily' + ); + + SELECT id, ds FROM (VALUES + ('1', '2020-01-01'), + ) data(id, ds) + WHERE ds BETWEEN @start_ds AND @end_ds + """ + ) + ) + ) + + prod_plan = context.plan(auto_apply=True) + assert len(prod_plan.new_snapshots) == 1 + + context.upsert_model( + load_sql_based_model( + parse( + """ + MODEL( + name db.xvg, + kind INCREMENTAL_BY_TIME_RANGE ( + time_column ds + ), + cron '@daily', + physical_properties ('some_prop' = 1), + ); + + SELECT id, ds FROM (VALUES + ('1', '2020-01-01'), + ) data(id, ds) + WHERE ds BETWEEN @start_ds AND @end_ds + """ + ) + ) + ) + + # This should raise an error because the model has no start configured and the end time is less than the start time which will be calculated from the intervals + with pytest.raises( + PlanError, + match=r"Model '.*xvg.*': Start date / time .* can't be greater than end date / time .*\.\nSet the `start` attribute in your project config model defaults to avoid this issue", + ): + context.plan("dev", execution_time="1999-01-05")