Skip to content

[python] Support Date/Time/Timestamp/Decimal filter literals in py_to_datum#443

Merged
JingsongLi merged 2 commits into
apache:mainfrom
JunRuiLee:feat/py-temporal-decimal-literals
Jul 3, 2026
Merged

[python] Support Date/Time/Timestamp/Decimal filter literals in py_to_datum#443
JingsongLi merged 2 commits into
apache:mainfrom
JunRuiLee:feat/py-temporal-decimal-literals

Conversation

@JunRuiLee

Copy link
Copy Markdown
Contributor

Purpose

py_to_datum in the Python binding raises NotImplementedError for any non-primitive literal, so a filter on a temporal or decimal column can never be pushed down through the DataFrame-style read path — every such query fails even though the core predicate machinery fully supports these Datum variants. This adds DataType-driven literal conversion for Date / Time / Timestamp / LocalZonedTimestamp / Decimal.

Complements #430, which adds the equivalent temporal conversions on the DataFusion (SQL) path; this PR covers the Python binding path.

Linked issue: none.

Brief change log

  • Date: datetime.date → epoch days. datetime.datetime (a date subclass) is rejected so the time-of-day part is never silently dropped.
  • Time: naive datetime.time → millis-of-day. Timezone-aware times and sub-millisecond microseconds are rejected (Datum::Time carries millis; silent truncation would change comparison semantics).
  • Timestamp: naive datetime.datetime{millis, nanos} with a Euclidean split so pre-epoch instants keep a non-negative nano remainder — same convention as BinaryRow::get_timestamp_raw and the DataFusion pushdown conversion. Timezone-aware datetimes are rejected (parity with the SQL path, which refuses zoned literals for TIMESTAMP).
  • LocalZonedTimestamp: timezone-aware datetime.datetime normalized through astimezone(utc), so any tzinfo implementation (zoneinfo, pytz, fixed offsets) is handled by Python itself. Naive datetimes are rejected as ambiguous.
  • Decimal: decimal.Decimal or int, rescaled losslessly to the column's scale via as_tuple() (avoids Decimal context rounding). Values needing rounding, exceeding the column precision, non-finite values (NaN/Infinity), bool, and binary floats are rejected.
  • All rejections raise ValueError (type mismatch); NotImplementedError remains for genuinely unsupported types (Bytes/complex).
  • Enables the chrono feature of pyo3 (chrono is already in the workspace dependency tree; no new crates).

Tests

  • Rust: cargo test -p pypaimon_rust — 51 passed. New unit tests cover epoch-day conversion (incl. pre-epoch), datetime-as-date rejection, time tz/sub-milli rejection, naive/aware timestamp acceptance+rejection for both TIMESTAMP and TIMESTAMP_LTZ, pre-epoch Euclidean split, and decimal exact-scale / lossless-rescale / scientific-notation / negative / int-literal / rounding / overflow / NaN / float / bool cases.
  • Python: uv run --no-sync pytest tests/test_read.py — 39 passed. New end-to-end tests build a table with DATE/TIMESTAMP/DECIMAL columns across two files and assert the filter both prunes splits (stats-based) and returns the matching rows via TableRead.read.

API and Format

No API surface change (the same with_filter(dict) accepts more literal types). No storage format change.

Documentation

Behavior documented on py_to_datum and the new conversion helpers.

JunRuiLee added 2 commits July 3, 2026 13:15
…_datum

py_to_datum previously raised NotImplementedError for any non-primitive
literal, so filters on temporal or decimal columns could never be pushed
down through the Python binding. Add DataType-driven conversions:

- Date: datetime.date -> epoch days; rejects datetime.datetime (a date
  subclass) so the time-of-day part is never silently dropped.
- Time: naive datetime.time -> millis-of-day; rejects tz-aware times and
  sub-millisecond microseconds (Datum::Time carries millis; truncation
  would change comparison semantics).
- Timestamp: naive datetime.datetime -> {millis, nanos} with a Euclidean
  split for pre-epoch instants; rejects tz-aware datetimes, matching the
  DataFusion pushdown path which refuses zoned literals for TIMESTAMP.
- LocalZonedTimestamp: tz-aware datetime.datetime normalized through
  astimezone(utc) (works with zoneinfo/pytz/fixed offsets); rejects naive
  datetimes as ambiguous.
- Decimal: decimal.Decimal or int, rescaled losslessly to the column
  scale via as_tuple(); rejects rounding, precision overflow, NaN/Inf,
  bool, and binary floats.

All rejections are ValueError (type mismatch), keeping NotImplementedError
for genuinely unsupported types (Bytes/complex).
…literals

The aware check was tzinfo-is-set, but Python defines aware as tzinfo
set AND utcoffset() not None. A tzinfo whose utcoffset() returns None
(a 'floating' timezone) passed the check, and astimezone(utc) then
interpreted the value as process-local time — the same literal would
normalize to different instants depending on the machine's timezone.

Check utcoffset() explicitly and reject such pseudo-naive datetimes
with the same ValueError as plain naive ones. Timestamp keeps rejecting
ANY tzinfo (stricter than Python's definition, but explicit and
deterministic); a new test documents that choice.

@JingsongLi JingsongLi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@JingsongLi JingsongLi merged commit 9cf89f2 into apache:main Jul 3, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants