Skip to content

Commit 3934324

Browse files
committed
PR feedback
1 parent 7efec2c commit 3934324

6 files changed

Lines changed: 65 additions & 11 deletions

File tree

sqlmesh/core/context.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,14 @@
120120
from sqlmesh.utils import UniqueKeyDict, Verbosity
121121
from sqlmesh.utils.concurrency import concurrent_apply_to_values
122122
from sqlmesh.utils.dag import DAG
123-
from sqlmesh.utils.date import TimeLike, now_ds, to_timestamp, format_tz_datetime, now_timestamp
123+
from sqlmesh.utils.date import (
124+
TimeLike,
125+
now_ds,
126+
to_timestamp,
127+
format_tz_datetime,
128+
now_timestamp,
129+
now,
130+
)
124131
from sqlmesh.utils.errors import (
125132
CircuitBreakerError,
126133
ConfigError,
@@ -1517,7 +1524,7 @@ def plan_builder(
15171524
max_interval_end_per_model,
15181525
backfill_models,
15191526
modified_model_names,
1520-
execution_time,
1527+
execution_time or now(),
15211528
)
15221529

15231530
# Refresh snapshot intervals to ensure that they are up to date with values reflected in the max_interval_end_per_model.

sqlmesh/core/plan/builder.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
yesterday_ds,
4343
to_timestamp,
4444
time_like_to_str,
45+
is_relative,
4546
)
4647
from 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,

sqlmesh/core/plan/definition.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ def start(self) -> TimeLike:
8383
def end(self) -> TimeLike:
8484
return self.provided_end or self.execution_time
8585

86-
@property
86+
@cached_property
8787
def execution_time(self) -> TimeLike:
88+
# note: property is cached so that it returns a consistent timestamp for now()
8889
return self.execution_time_ or now()
8990

9091
@property

sqlmesh/utils/date.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,16 @@ def is_categorical_relative_expression(expression: str) -> bool:
373373
return not any(k in TIME_UNITS for k in grain_kwargs)
374374

375375

376+
def is_relative(value: TimeLike) -> bool:
377+
"""
378+
Tests a TimeLike object to see if it is a relative expression, eg '1 week ago' as opposed to an absolute timestamp
379+
"""
380+
if isinstance(value, str):
381+
return is_categorical_relative_expression(value)
382+
383+
return False
384+
385+
376386
def to_time_column(
377387
time_column: t.Union[TimeLike, exp.Null],
378388
time_column_type: exp.DataType,

tests/integrations/github/cicd/test_github_controller.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
GithubCheckStatus,
2020
MergeStateStatus,
2121
)
22+
from sqlmesh.utils.date import to_datetime, now
2223
from tests.integrations.github.cicd.conftest import MockIssueComment
2324

2425
pytestmark = pytest.mark.github
@@ -236,6 +237,7 @@ def test_pr_plan(github_client, make_controller):
236237
def test_pr_plan_auto_categorization(github_client, make_controller):
237238
custom_categorizer_config = CategorizerConfig.all_semi()
238239
default_start = "1 week ago"
240+
default_start_absolute = to_datetime(default_start, relative_base=now())
239241
controller = make_controller(
240242
"tests/fixtures/github/pull_request_synchronized.json",
241243
github_client,
@@ -249,7 +251,7 @@ def test_pr_plan_auto_categorization(github_client, make_controller):
249251
assert not controller._context.apply.called
250252
assert controller._context._run_plan_tests.call_args == call(skip_tests=True)
251253
assert controller._pr_plan_builder._categorizer_config == custom_categorizer_config
252-
assert controller.pr_plan.start == default_start
254+
assert controller.pr_plan.start == default_start_absolute
253255

254256

255257
def test_prod_plan(github_client, make_controller):

tests/utils/test_date.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
date_dict,
1313
format_tz_datetime,
1414
is_categorical_relative_expression,
15+
is_relative,
1516
make_inclusive,
1617
to_datetime,
1718
to_time_column,
@@ -324,3 +325,17 @@ def test_format_tz_datetime():
324325
test_datetime = to_datetime("2020-01-01 00:00:00")
325326
assert format_tz_datetime(test_datetime) == "2020-01-01 12:00AM UTC"
326327
assert format_tz_datetime(test_datetime, format_string=None) == "2020-01-01 00:00:00+00:00"
328+
329+
330+
def test_is_relative():
331+
assert is_relative("1 week ago")
332+
assert is_relative("1 week")
333+
assert is_relative("1 day ago")
334+
assert is_relative("yesterday")
335+
336+
assert not is_relative("2024-01-01")
337+
assert not is_relative("2024-01-01 01:02:03")
338+
assert not is_relative(to_datetime("2024-01-01 01:02:03"))
339+
assert not is_relative(to_timestamp("2024-01-01 01:02:03"))
340+
assert not is_relative(to_datetime("1 week ago"))
341+
assert not is_relative(to_timestamp("1 day ago"))

0 commit comments

Comments
 (0)