Skip to content

add index-friendly date extractors - #38

Open
manuelpelloni wants to merge 5 commits into
mainfrom
date-part
Open

add index-friendly date extractors#38
manuelpelloni wants to merge 5 commits into
mainfrom
date-part

Conversation

@manuelpelloni

Copy link
Copy Markdown
Contributor

Adds diesel expressions for extracting and comparing dates, date parts and times of day on PostgreSQL columns. Wherever possible the comparison is rewritten into a form that keeps the column bare, so a plain B-tree index on the column can drive it — instead of the naive date_part(..) = $1 / col::date = $1 forms, which force a seq scan.

extract_date — date equality

extract_date(col).eq(date) emits, depending on the column type (picked at compile time via DateLike::Range):

  • Date columns: (col BETWEEN $1 AND $1) — a closed range, equivalent to = $1 (ClosedRange)
  • Timestamp columns: (col >= $1 AND col < $1 + 1) — a half-open range covering every instant of the day (HalfOpenRange)

Timestamptz columns are rejected at compile time: which instants fall on a calendar date depends on the session time zone.

extract_year / extract_month / extract_day — date parts

Emit CAST(date_part('<field>', col) AS integer), the exact form an expression index must be built on (EXTRACT maps to a different catalog function — the two diverged in PostgreSQL 14 — and would not match).

On date columns, extract_year(..).eq(..)/.between(..) with bounded year types (u8, u16, U15) is rewritten into a plain date range:

(col BETWEEN make_date($1, 1, 1) AND make_date($2, 12, 31))

so a regular B-tree index on the column works with no expression index. i32 years keep the date_part comparison, since part of their range is rejected by make_date() at runtime.

extract_time — time of day

  • Time columns stay bare (BareTime); eq uses the same range form as extract_date
  • Timestamp columns are emitted as CAST(col AS time) (CastToTime), matching an expression index built on exactly that cast

Timestamptz is rejected: its cast to time is only STABLE, so PostgreSQL rejects the expression index too.

All entry points have runnable doctests asserting the emitted SQL, plus integration tests via debug_query.

Rewrites date equality as a range that keeps the column bare, so a
plain B-tree index can drive it: a closed BETWEEN on Date columns
(ClosedRange), a half-open >= / < + 1 pair on Timestamp columns
(HalfOpenRange), picked at compile time via DateLike::Range.
Timestamptz is rejected: which instants fall on a calendar date
depends on the session time zone.
extract_year/extract_month/extract_day emit
CAST(date_part('<field>', expr) AS integer), the exact form an
expression index must be built on (EXTRACT maps to a different catalog
function; the two diverged in PostgreSQL 14). On date columns, bounded
year types (u8, u16, U15) rewrite eq/between into a plain
BETWEEN make_date(..) AND make_date(..) range (YearRange) so a B-tree
index on the column works; i32 years keep the date_part comparison
because part of their range is rejected by make_date() at runtime.
…risons

Time columns stay bare (BareTime) so a plain B-tree index drives the
comparison, and their eq uses the same range form as extract_date.
Timestamp columns are emitted as CAST(expr AS time) (CastToTime),
which matches an expression index built on exactly that cast.
Timestamptz is rejected: its cast to time depends on the session time
zone (it is only STABLE), so PostgreSQL rejects the expression index
too.
…zones

The timezone! macro declares unit structs implementing TimeZone, whose
NAME is spliced into the SQL as a literal (a bind would break
SELECT/GROUP BY structural matching, and a runtime name the type-keyed
prepared-statement cache). at_time_zone/TimeZone::at convert
Timestamptz columns to the zone's wall clock and reinterpret naive
Timestamp columns as UTC first, via the double
(expr AT TIME ZONE 'UTC') AT TIME ZONE '<zone>' form, picked at
compile time via InstantLike::Conversion; nullability propagates to
the output. timezone(zone, ts) is IMMUTABLE, so an expression index
built on the emitted form works, and the result is a plain Timestamp
expression the whole extract_* family composes with — which is how
extract_date/extract_time/extract_year work on Timestamptz columns.
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.

1 participant