Skip to content

Commit 300d652

Browse files
committed
feat: Search within date range in pipeline run API
1 parent 7a7a60b commit 300d652

3 files changed

Lines changed: 773 additions & 13 deletions

File tree

cloud_pipelines_backend/filter_query_sql.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import dataclasses
3+
import datetime
34
import json
45
import enum
56
from typing import Final
@@ -17,6 +18,7 @@
1718
class SystemKey(enum.StrEnum):
1819
CREATED_BY = f"{_PIPELINE_RUN_KEY_PREFIX}created_by"
1920
NAME = f"{_PIPELINE_RUN_KEY_PREFIX}name"
21+
CREATED_AT = f"{_PIPELINE_RUN_KEY_PREFIX}date.created_at"
2022

2123

2224
SYSTEM_KEY_SUPPORTED_PREDICATES: dict[SystemKey, set[type]] = {
@@ -31,6 +33,9 @@ class SystemKey(enum.StrEnum):
3133
filter_query_models.ValueContainsPredicate,
3234
filter_query_models.ValueInPredicate,
3335
},
36+
SystemKey.CREATED_AT: {
37+
filter_query_models.TimeRangePredicate,
38+
},
3439
}
3540

3641
# ---------------------------------------------------------------------------
@@ -72,6 +77,8 @@ def _get_predicate_key(*, predicate: filter_query_models.Predicate) -> str | Non
7277
return predicate.value_contains.key
7378
case filter_query_models.ValueInPredicate():
7479
return predicate.value_in.key
80+
case filter_query_models.TimeRangePredicate():
81+
return predicate.time_range.key
7582
case _:
7683
return None
7784

@@ -299,8 +306,9 @@ def _predicate_to_clause(
299306
return _value_contains_to_clause(predicate=predicate)
300307
case filter_query_models.ValueInPredicate():
301308
return _value_in_to_clause(predicate=predicate)
309+
case filter_query_models.TimeRangePredicate():
310+
return _time_range_to_clause(predicate=predicate)
302311
case _:
303-
# TODO: TimeRangePredicate -- not supported currently, will be supported in the future.
304312
raise NotImplementedError(
305313
f"Predicate type {type(predicate).__name__} is not yet implemented."
306314
)
@@ -361,3 +369,56 @@ def _value_in_to_clause(
361369
bts.PipelineRunAnnotation.value.in_(predicate.value_in.values),
362370
],
363371
)
372+
373+
374+
# ---------------------------------------------------------------------------
375+
# Column-based predicates (bypass annotation table)
376+
# ---------------------------------------------------------------------------
377+
378+
379+
def _time_range_to_clause(
380+
*, predicate: filter_query_models.TimeRangePredicate
381+
) -> sql.ColumnElement:
382+
"""Build a WHERE clause for pipeline_run.created_at from a time range.
383+
384+
Pydantic's AwareDatetime preserves the original timezone offset, so we
385+
must normalize to naive UTC before comparing against the DB column.
386+
387+
The DB stores "naive UTC" datetimes -- the values represent UTC but carry
388+
no timezone label. For example, the DB stores '2024-01-01 02:30:00', not
389+
'2024-01-01 02:30:00+00:00'. The UtcDateTime type decorator (in
390+
backend_types_sql.py) strips tzinfo on write and re-attaches UTC on read.
391+
392+
Conversion pipeline for input '2024-01-01T08:00:00+05:30':
393+
394+
API request (JSON string)
395+
'2024-01-01T08:00:00+05:30'
396+
|
397+
v
398+
Pydantic AwareDatetime (preserves offset)
399+
datetime(2024, 1, 1, 8, 0, 0, tzinfo=+05:30)
400+
|
401+
v .astimezone(utc) -- converts 08:00 - 05:30 = 02:30
402+
UTC-aware datetime
403+
datetime(2024, 1, 1, 2, 30, 0, tzinfo=UTC)
404+
|
405+
v .replace(tzinfo=None) -- strips timezone label
406+
Naive datetime
407+
datetime(2024, 1, 1, 2, 30, 0)
408+
|
409+
v SQLAlchemy literal_binds -- adds microsecond precision
410+
SQL string
411+
'2024-01-01 02:30:00.000000' <-- matches DB storage format
412+
"""
413+
tr = predicate.time_range
414+
if tr.key != SystemKey.CREATED_AT:
415+
raise errors.InvalidAnnotationKeyError(
416+
f"time_range only supports key {SystemKey.CREATED_AT!r}, got {tr.key!r}"
417+
)
418+
# Convert aware datetimes to naive UTC to match DB storage format.
419+
start_utc = tr.start_time.astimezone(datetime.timezone.utc).replace(tzinfo=None)
420+
clauses: list[sql.ColumnElement] = [bts.PipelineRun.created_at >= start_utc]
421+
if tr.end_time is not None:
422+
end_utc = tr.end_time.astimezone(datetime.timezone.utc).replace(tzinfo=None)
423+
clauses.append(bts.PipelineRun.created_at < end_utc)
424+
return sql.and_(*clauses)

0 commit comments

Comments
 (0)