4242 yesterday_ds ,
4343 to_timestamp ,
4444 time_like_to_str ,
45+ is_relative ,
4546)
4647from sqlmesh .utils .errors import NoChangesPlanError , PlanError
4748
@@ -139,7 +140,14 @@ def __init__(
139140 self ._include_unmodified = include_unmodified
140141 self ._restate_models = set (restate_models ) if restate_models is not None else None
141142 self ._effective_from = effective_from
143+
144+ # note: this deliberately doesnt default to now() here.
145+ # There may be an significant delay between the PlanBuilder producing a Plan and the Plan actually being run
146+ # so if execution_time=None is passed to the PlanBuilder, then the resulting Plan should also have execution_time=None
147+ # in order to prevent the Plan that was intended to run "as at now" from having "now" fixed to some time in the past
148+ # ref: https://github.com/TobikoData/sqlmesh/pull/4702#discussion_r2140696156
142149 self ._execution_time = execution_time
150+
143151 self ._backfill_models = backfill_models
144152 self ._end = end or default_end
145153 self ._apply = apply
@@ -176,18 +184,22 @@ def is_start_and_end_allowed(self) -> bool:
176184
177185 @property
178186 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 ))
187+ if self ._start and is_relative (self ._start ):
188+ # only do this for relative expressions otherwise inclusive date strings like '2020-01-01' can be turned into exclusive timestamps eg '2020-01-01 00:00:00'
189+ return to_datetime (self ._start , relative_base = to_datetime (self .execution_time ))
181190 return self ._start
182191
183192 @property
184193 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 ))
194+ if self ._end and is_relative (self ._end ):
195+ # only do this for relative expressions otherwise inclusive date strings like '2020-01-01' can be turned into exclusive timestamps eg '2020-01-01 00:00:00'
196+ return to_datetime (self ._end , relative_base = to_datetime (self .execution_time ))
187197 return self ._end
188198
189- @property
199+ @cached_property
190200 def execution_time (self ) -> TimeLike :
201+ # this is cached to return a stable value from now() in the places where the execution time matters for resolving relative date strings
202+ # during the plan building process
191203 return self ._execution_time or now ()
192204
193205 def set_start (self , new_start : TimeLike ) -> PlanBuilder :
@@ -274,7 +286,8 @@ def build(self) -> Plan:
274286 )
275287
276288 restatements = self ._build_restatements (
277- dag , earliest_interval_start (self ._context_diff .snapshots .values (), self .execution_time )
289+ dag ,
290+ earliest_interval_start (self ._context_diff .snapshots .values (), self .execution_time ),
278291 )
279292 models_to_backfill = self ._build_models_to_backfill (dag , restatements )
280293
@@ -284,6 +297,12 @@ def build(self) -> Plan:
284297 # model should be ignored.
285298 interval_end_per_model = None
286299
300+ # this deliberately uses the passed in self._execution_time and not self.execution_time cached property
301+ # the reason is because that there can be a delay between the Plan being built and the Plan being actually run,
302+ # so this ensures that an _execution_time of None can be propagated to the Plan and thus be re-resolved to
303+ # the current timestamp of when the Plan is eventually run
304+ plan_execution_time = self ._execution_time
305+
287306 plan = Plan (
288307 context_diff = self ._context_diff ,
289308 plan_id = self ._plan_id ,
@@ -307,7 +326,7 @@ def build(self) -> Plan:
307326 selected_models_to_backfill = self ._backfill_models ,
308327 models_to_backfill = models_to_backfill ,
309328 effective_from = self ._effective_from ,
310- execution_time = self . execution_time ,
329+ execution_time = plan_execution_time ,
311330 end_bounded = self ._end_bounded ,
312331 ensure_finalized_snapshots = self ._ensure_finalized_snapshots ,
313332 user_provided_flags = self ._user_provided_flags ,
0 commit comments