11import base64
22import dataclasses
3+ import datetime
34import json
45import enum
56from typing import Final
1718class 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
2224SYSTEM_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