ADR 0010: Full-range dates (proleptic-Gregorian calendar) and a wider duration model
Status: Accepted
Date: 2026-06-30
Build target: v0.5.0 (conformance hardening); openCypher Temporal10 large-value cases
Related: builds on ADR 0007 (runtime temporal values)
Context
Section titled “Context”openCypher dates span years −999,999,999 … +999,999,999 and duration.between/inSeconds
can produce spans of billions of years. ADR 0007 represented a date as Arrow Date32 (signed
i32 days since the Unix epoch) and did all date arithmetic via chrono NaiveDate. Both cap far
short of the spec:
- chrono
NaiveDateis limited to roughly year ±262,143. Date32is i32 days ≈ year ±5.8M.- The duration model
{months:i32, days:i32, nanos:i64}overflowed: ~24e9 months don’t fiti32, and a billion-year span in total sub-day nanoseconds (~6.3e25) doesn’t fiti64.
So date('-999999999-01-01') parsed to null, and Temporal10 [9]/[10] returned null — the
last two temporal scenarios. ADR 0007’s (Date32, Time64) split for localdatetime/datetime was
chosen to span “1…9999+ at nanosecond precision”; it left no headroom for the full year range.
Decision
Section titled “Decision”-
A custom proleptic-Gregorian calendar on
i64days (crates/gf-rel/src/calendar.rs) replaces chrono for all DATE math — Howard Hinnant’sdays_from_civil/civil_from_days, plus ISO-week, ordinal, weekday, day-clampedadd_months, and signed-year ISO formatting. Exact for anyi64year; cross-checked against chrono for the in-range era in unit tests. -
The
dateArrow representation widens fromDate32(i32 days) to i64 days, self-describing so storage decode can still tell a date from an integer:- standalone
date→Struct{epoch_day: Int64}(a bareInt64is indistinguishable from an integer property; a one-field struct is self-describing and orders chronologically, consistent with howduration/localdatetime/datetimeare already struct-typed); - the
datechild of thelocaldatetime/datetimestructs →Int64days (the parent struct already self-describes).
- standalone
-
The
durationmodel widens to{months:i64, days:i64, seconds:i64, nanos:i64}in the Neo4j/openCypher floor-canonical form:secondscarries the sign andnanosis the non-negative nanoseconds-of-second[0, 1e9); rendering reconstructs sign-coherent components. Splittingsecondsfromnanoslets a ~6.3e16-second span fiti64;i64months hold ~24e9. -
Out of scope (unchanged): time-of-day stays
Time64(nanoseconds-of-day — year-independent); named-zone resolution stayschrono_tz. No corpus scenario combines a named zone with an extreme year (extreme dates are offset-only / unzoned), sochrono_tz’s ±262k limit is not a constraint.
Consequences
Section titled “Consequences”- Ordering preserved. Native
Int64is chronological; the date-firstlocaldatetime/datetimestructs keep row-format lexicographic = chronological order. - Storage layout changes for
date,duration,localdatetime,datetimecolumns. Pre-1.0, no on-disk format guarantee, so no migration is provided; the struct/field-name decode dispatch distinguishes each type. - Rendering:
calendar::format_dateemits signed years for the extreme range (chrono’s%Ycannot); in-range output is byte-identical to chrono. - Risk concentrates in calendar correctness (ISO-week-year boundaries, the proleptic year 0, leap years, day-clamped month add) — covered by dedicated unit tests + an in-range chrono cross-check, and ultimately by the whole-corpus regression gate.
Phasing
Section titled “Phasing”- Phase 1 (foundation): the
calendarmodule + the duration-model widening. Behavior-preserving on the in-range corpus (0 new/lost scenarios); the calendar lands validated but not yet wired. - Phase 2: migrate the date math off chrono to
calendarand widen the date Arrow representation together (each date function touched once). Delivers Temporal10 [9]/[10] → the temporal corpus reaches 1004/1004 and closes.