Repository navigation
refactor: collapse date_bin month binning onto a single civil-calendar path - #1
Merged
ThilakShekharShriyan merged 1 commit intoOct 22, 2026
Conversation
The month-stride fix landed a civil-calendar binning path alongside the existing chrono one, leaving two implementations of the same semantics (month diff, stride distance, shift from origin, month-end clamp, step back on overshoot) and a test whose job was to keep them in sync. Collapse them onto one core. `bin_months_civil` does the binning in i64 and both widths are thin adapters over it, so the i128 path exists only to widen at the nanosecond boundary. `bin_months`, `shift_months` and `to_utc_date_time` drop out. Move the calendar arithmetic to its own `datetime::civil` module. `civil_from_days` and `days_from_civil` were private to date_trunc and had been opened up to `pub(super)` so date_bin could reach them; they now live next to `CivilTimestamp`, which owns the nanosecond conversions, `month_index` and `shift_months`. date_trunc and date_bin are both plain consumers again. Two smaller changes fall out of that move. `days_in_month` computes a month length from a table and a leap test instead of two `days_from_civil` calls, which is where most of the month path's cost was. And the helpers that cannot fail for any reachable input no longer return `Option`, leaving one real failure - whether the bin fits the output type - at the boundary where the caller already checks it. `CivilTimestamp` derives `Ord`, and binning depends on that order being chronological. Its invariants (valid day of month, normalised time of day) are documented on the type, and every constructor lives in the module that maintains them. Add a month-stride benchmark; the existing one only covered day-time strides, so the month path had no coverage. No public API change: everything touched here is private to date_bin.rs or crate-internal. Co-Authored-By: Claude Opus 5 <[email protected]>
Omega359
marked this pull request as ready for review
October 22, 2026 21:27
ThilakShekharShriyan
merged commit Oct 22, 2026
d8ddeb8
into
ThilakShekharShriyan:fix-25855-date-bin-month-stride
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
None directly — this targets your
fix-25855-date-bin-month-stridebranch, as asuggested restructuring of apache#26153 (which closes apache#25855). Take it, take part of it,
or close it; I won't be offended either way.
Rationale for this change
No user-visible change. This is purely about the shape of the implementation.
Your fix is correct, and I verified the arithmetic against the chrono path across
the overlapping domain. My concern is structural: after the fix,
date_binhas twocomplete implementations of month-bin semantics.
Both of these encode the same five rules — month-index difference,
compute_distance,shift from the origin, clamp a missing day to the month end, and step back one stride
on overshoot:
date_bin_months_intervaldate_bin_months_interval_widebin_monthsmonth_indexx2shift_months(chronoMonths)shift_civil_monthschecked_add_monthsdays_in_monthAny future change to month semantics now has to land in both, in two numeric domains,
via two different libraries.
test_date_bin_months_wide_matches_chrono_pathis thesignal — a test whose job is to keep two implementations in sync is usually a sign
there should only be one.
The civil implementation you added is strictly more general than the chrono one, so
the chrono one can simply go.
What changes are included in this PR?
One commit on top of your branch.
Collapse the two month paths.
bin_months_civildoes the binning in i64, and bothwidths become thin adapters over it — the i128 path exists only to widen at the
nanosecond boundary.
bin_months,shift_monthsandto_utc_date_timedrop out,along with four of six chrono imports.
Move the calendar arithmetic to
datetime::civil.civil_from_daysanddays_from_civilwere private todate_truncand your PR opened them topub(super)so
date_bincould reach them. That couples two sibling UDF modules and quietly makesdate_trunca utility module for calendar math. They now live in their own modulealongside
CivilTimestamp, which owns the nanosecond conversions,month_indexandshift_months. Bothdate_truncanddate_binare plain consumers again.days_in_monthuses a lookup table instead of twodays_from_civilcalls. Thatwas ~12 integer divisions per row to get a month length, and it was the single most
expensive line in the new code — worth ~36% of the month path on its own.
Drop
Optionfrom helpers that cannot fail.month_indexandcivil_to_nanoscan't return
Nonefor any reachable input (source is bounded byi64::MAX * 10^9,and the stride by
i32viaIntervalMonthDayNano). Removing it leaves exactly onereal failure — whether the bin fits the output type — at the boundary where the caller
already checks it.
Document the
Ordinvariant. The derived ordering onCivilTimestampisload-bearing for
bin > source, and depends on field declaration order plus twonormalizations. Both are now stated on the type, and every constructor lives in the
module that maintains them.
date_bin.rsgoes 1920 -> 1820 lines (it was 1810 before your PR), anddate_trunc.rs1838 -> 1781.
What is the testing strategy for this PR?
Every test and slt expectation on your branch is unchanged — this PR touches no
test file and no
.sltfile, and your suite passes verbatim. That's the main evidencethat behavior is preserved.
cargo test -p datafusion-functions— 394 passed, 0 faileddate_bin_errors.slt,timestamps.slt,dates.slt,date_trunc.slt— all passcargo clippy --all-targets --all-features -- -D warnings— cleanOne caveat you should weigh. Because both widths now share a core,
test_date_bin_months_wide_matches_chrono_pathstill passes but no longer does whatits name says — it compares the two adapters, not civil-vs-chrono. If you take this,
that test is worth reworking to compute its expectation with chrono inside the test, so
chrono stays a test oracle pinning the semantics without being production machinery.
I left that out deliberately rather than quietly rewrite your test.
Benchmark.
benches/datetime_expressions/date_bin.rsonly covered day-time strides,so the month path had no coverage; I added
date_bin_months_1000. Measured with it,same machine, same session, this branch vs your branch:
date_bin_1000(nanos stride)date_bin_months_1000This is performance-neutral, not a speedup. The
days_in_monthlookup pays backthe cost of leaving chrono and no more. I'm claiming a structural improvement at no
runtime cost, not a faster
date_bin.Are there any user-facing changes?
No. No behavior change and no public API change — everything touched is either private
to
date_bin.rsor crate-internal, and the newcivilmodule ispub(crate). Noapi changelabel needed.🤖 Generated with Claude Code