From 6302571f5bfdf6709efd13caac25159fd80cfb82 Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:27:28 +0300 Subject: [PATCH 1/3] Chore: Validate start and end dates during plan build time --- sqlmesh/core/context.py | 22 +++++++++++++++ tests/core/test_context.py | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 7d27092f0e..876951be77 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1603,6 +1603,28 @@ def plan_builder( execution_time or now(), ) + # Validate that the start is not greater than the end date / time + effective_start = start or default_start + effective_end = end or default_end + if effective_start is not None and effective_end is not None: + start_ts = to_timestamp(effective_start) + end_ts = to_timestamp(effective_end) + if start_ts > end_ts: + for model_name in ( + set(backfill_models or {}) + | set(modified_model_names) + | set(max_interval_end_per_model) + ): + if snapshot := snapshots.get(model_name): + if ( + snapshot.node.start is None + or to_timestamp(snapshot.node.start) > end_ts + ): + raise SQLMeshError( + f"Start date / time ({to_datetime(start_ts)}) can't be greater than end date / time ({to_datetime(end_ts)}).\n" + f"Set the `start` attribute in your project config model defaults to avoid this issue." + ) + # Refresh snapshot intervals to ensure that they are up to date with values reflected in the max_interval_end_per_model. self.state_sync.refresh_snapshot_intervals(context_diff.snapshots.values()) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 2827981ae7..847eed526f 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( + SQLMeshError, + match=r"Set the `start` attribute in your project config model defaults to avoid this issue", + ): + context.plan("dev", execution_time="1999-01-05") From 21df382ed8962ba0748b8bdbde6025cd1bdce6df Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:10:50 +0300 Subject: [PATCH 2/3] add model name in the error message --- sqlmesh/core/context.py | 2 +- tests/core/test_context.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 876951be77..3f5c55732a 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1621,7 +1621,7 @@ def plan_builder( or to_timestamp(snapshot.node.start) > end_ts ): raise SQLMeshError( - f"Start date / time ({to_datetime(start_ts)}) can't be greater than end date / time ({to_datetime(end_ts)}).\n" + f"Model '{model_name}': Start date / time ({to_datetime(start_ts)}) can't be greater than end date / time ({to_datetime(end_ts)}).\n" f"Set the `start` attribute in your project config model defaults to avoid this issue." ) diff --git a/tests/core/test_context.py b/tests/core/test_context.py index 847eed526f..616d3d7b41 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -3060,6 +3060,6 @@ def test_plan_no_start_configured(): # 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( SQLMeshError, - match=r"Set the `start` attribute in your project config model defaults to avoid this issue", + 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") From 648523054d8c07d0adf6c83acbf0822ee5ebed6a Mon Sep 17 00:00:00 2001 From: Themis Valtinos <73662635+themisvaltinos@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:45:21 +0300 Subject: [PATCH 3/3] pr feedback --- sqlmesh/core/context.py | 22 ---------------------- sqlmesh/core/plan/builder.py | 20 ++++++++++++++++++++ tests/core/test_context.py | 2 +- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/sqlmesh/core/context.py b/sqlmesh/core/context.py index 3f5c55732a..7d27092f0e 100644 --- a/sqlmesh/core/context.py +++ b/sqlmesh/core/context.py @@ -1603,28 +1603,6 @@ def plan_builder( execution_time or now(), ) - # Validate that the start is not greater than the end date / time - effective_start = start or default_start - effective_end = end or default_end - if effective_start is not None and effective_end is not None: - start_ts = to_timestamp(effective_start) - end_ts = to_timestamp(effective_end) - if start_ts > end_ts: - for model_name in ( - set(backfill_models or {}) - | set(modified_model_names) - | set(max_interval_end_per_model) - ): - if snapshot := snapshots.get(model_name): - if ( - snapshot.node.start is None - or to_timestamp(snapshot.node.start) > end_ts - ): - raise SQLMeshError( - f"Model '{model_name}': Start date / time ({to_datetime(start_ts)}) can't be greater than end date / time ({to_datetime(end_ts)}).\n" - f"Set the `start` attribute in your project config model defaults to avoid this issue." - ) - # Refresh snapshot intervals to ensure that they are up to date with values reflected in the max_interval_end_per_model. self.state_sync.refresh_snapshot_intervals(context_diff.snapshots.values()) 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 616d3d7b41..852f00e760 100644 --- a/tests/core/test_context.py +++ b/tests/core/test_context.py @@ -3059,7 +3059,7 @@ def test_plan_no_start_configured(): # 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( - SQLMeshError, + 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")