4141 to_datetime ,
4242 yesterday_ds ,
4343 to_timestamp ,
44+ time_like_to_str ,
4445)
4546from sqlmesh .utils .errors import NoChangesPlanError , PlanError
4647
@@ -55,6 +56,7 @@ class PlanBuilder:
5556 start: The start time to backfill data.
5657 end: The end time to backfill data.
5758 execution_time: The date/time time reference to use for execution time. Defaults to now.
59+ If :start or :end are relative time expressions, they are interpreted as relative to the :execution_time
5860 apply: The callback to apply the plan.
5961 restate_models: A list of models for which the data should be restated for the time range
6062 specified in this plan. Note: models defined outside SQLMesh (external) won't be a part
@@ -172,6 +174,22 @@ def is_start_and_end_allowed(self) -> bool:
172174 """Indicates whether this plan allows to set the start and end dates."""
173175 return self ._is_dev or bool (self ._restate_models )
174176
177+ @property
178+ def start (self ) -> t .Optional [TimeLike ]:
179+ if self ._start and self ._execution_time :
180+ return to_datetime (self ._start , relative_base = to_datetime (self ._execution_time ))
181+ return self ._start
182+
183+ @property
184+ def end (self ) -> t .Optional [TimeLike ]:
185+ if self ._end and self ._execution_time :
186+ return to_datetime (self ._end , relative_base = to_datetime (self ._execution_time ))
187+ return self ._end
188+
189+ @property
190+ def execution_time (self ) -> TimeLike :
191+ return self ._execution_time or now ()
192+
175193 def set_start (self , new_start : TimeLike ) -> PlanBuilder :
176194 self ._start = new_start
177195 self .override_start = True
@@ -256,7 +274,7 @@ def build(self) -> Plan:
256274 )
257275
258276 restatements = self ._build_restatements (
259- dag , earliest_interval_start (self ._context_diff .snapshots .values ())
277+ dag , earliest_interval_start (self ._context_diff .snapshots .values (), self . execution_time )
260278 )
261279 models_to_backfill = self ._build_models_to_backfill (dag , restatements )
262280
@@ -269,8 +287,8 @@ def build(self) -> Plan:
269287 plan = Plan (
270288 context_diff = self ._context_diff ,
271289 plan_id = self ._plan_id ,
272- provided_start = self ._start ,
273- provided_end = self ._end ,
290+ provided_start = self .start ,
291+ provided_end = self .end ,
274292 is_dev = self ._is_dev ,
275293 skip_backfill = self ._skip_backfill ,
276294 empty_backfill = self ._empty_backfill ,
@@ -289,7 +307,7 @@ def build(self) -> Plan:
289307 selected_models_to_backfill = self ._backfill_models ,
290308 models_to_backfill = models_to_backfill ,
291309 effective_from = self ._effective_from ,
292- execution_time = self ._execution_time ,
310+ execution_time = self .execution_time ,
293311 end_bounded = self ._end_bounded ,
294312 ensure_finalized_snapshots = self ._ensure_finalized_snapshots ,
295313 user_provided_flags = self ._user_provided_flags ,
@@ -739,6 +757,18 @@ def _ensure_valid_date_range(self) -> None:
739757 "The start and end dates can't be set for a production plan without restatements."
740758 )
741759
760+ if (start := self .start ) and (end := self .end ):
761+ if to_datetime (start ) > to_datetime (end ):
762+ raise PlanError (
763+ f"Plan end date: '{ time_like_to_str (end )} ' must be after the plan start date: '{ time_like_to_str (start )} '"
764+ )
765+
766+ if end := self .end :
767+ if to_datetime (end ) > to_datetime (self .execution_time ):
768+ raise PlanError (
769+ f"Plan end date: '{ time_like_to_str (end )} ' cannot be in the future (execution time: '{ time_like_to_str (self .execution_time )} ')"
770+ )
771+
742772 def _ensure_no_forward_only_revert (self ) -> None :
743773 """Ensures that a previously superseded breaking / non-breaking snapshot is not being
744774 used again to replace an existing forward-only snapshot with the same version.
0 commit comments