Repository navigation
Conversation
cac7dac to
b6ab86b
Compare
b6ab86b to
c5569d4
Compare
|
@MaxGekk @cloud-fan friendly ping when you have a moment - this is a follow-up to SPARK-33404 that you originally fixed and merged. The slow path you introduced for correctness is preserved as the fallback; the new fast path applies the offset before/after the floorMod and verifies the offset is unchanged at the truncated candidate, so the LMT minute case (and the SPARK-30766 / SPARK-30857 hour/day cases) all still go through the safe path. ~2× speedup on MINUTE/HOUR/DAY locally, which recovers most of the regression flagged in #30338. Would appreciate your eyes. |
MaxGekk
left a comment
There was a problem hiding this comment.
DateTimeBenchmark Truncation results, wholestage on, ns/row on a 12th Gen Intel i7-1260P:
@Licht-T Could you regenerate benchmark results:
./sql/core/benchmarks/DateTimeBenchmark-results.txt
./sql/core/benchmarks/DateTimeBenchmark-jdk25-results.txt
./sql/core/benchmarks/DateTimeBenchmark-jdk21-results.txt
…meBenchmark (JDK 21, Scala 2.13, split 1 of 1)
…meBenchmark (JDK 17, Scala 2.13, split 1 of 1)
…meBenchmark (JDK 25, Scala 2.13, split 1 of 1)
|
Thanks @MaxGekk. Just ran the benchmarks on CI and pushed the results. |
cloud-fan
left a comment
There was a problem hiding this comment.
Summary
What: Re-introduces a fast path in DateTimeUtils.truncTimestamp for TRUNC_TO_MINUTE / HOUR / DAY via offset arithmetic + floorMod, falling back to the existing ZonedDateTime.truncatedTo-based path when the truncated candidate crosses an offset transition. WEEK/MONTH/QUARTER/YEAR + trunc(date, ...) are deliberately deferred.
Why: SPARK-33404 (Nov 2020) routed these levels through microsToInstant().atZone(zoneId).truncatedTo(unit) to fix a correctness bug where the prior fast path floor-modded micros in UTC, silently assuming the offset was a whole multiple of the unit (so any sub-minute LMT offset was rounded away). The fix carried a ~5.5x regression that has stood ~5 years. Benchmark deltas in the result files match the PR description: ~125ns/row to ~56ns/row wholestage on for MINUTE/HOUR/DAY/DD; WEEK/MONTH/QUARTER/YEAR unchanged.
Approach — the key design move: the new algorithm shifts the boundary grid into the wall-clock frame, instead of leaving it in UTC. Concretely: resolve O0 = getOffset(micros) once; compute local = micros + O0 (the wall-clock at micros re-labelled as a UTC instant — a "shadow frame"); truncatedLocal = local - floorMod(local, unitMicros) is the previous wall-clock unit boundary; candidate = truncatedLocal - O0, then re-check getOffset(candidate) == O0 (skipped for fixed-offset zones). On mismatch — or Math.addExact/subtractExact overflow — fall back to the slow path.
This is the right structural move: because epoch (1970-01-01 00:00:00) is itself a wall-clock unit boundary and wall-clock unit boundaries recur exactly every unitMicros in the wall-clock frame, floorMod(local, unitMicros) is true wall-clock alignment and is independent of how O0 shifts the shadow zero relative to UTC zero. That is precisely why sub-second LMT offsets (e.g. -07:52:58) and 30/45-minute zones (Kolkata +05:30, Kathmandu +05:45) need no offset-alignment guard — the failure mode of the old fast path is structurally absent here.
Correctness guarantee of the offset re-check (traced by hand for LMT / LA spring-forward / LA fall-back / Sao_Paulo midnight gap / Pacific/Apia 2011 calendar shift):
- Stable offset (single valid offset for the truncated wall-clock
W, equal toO0):candidate = W - O0is exactly the slow path's result; re-check passes. - Unambiguous
Wwith a valid offsetO_W != O0:O(candidate) = O0would implywallClock(candidate) = W, contradictingWhaving sole valid offsetO_W. So re-check necessarily fails -> fallback. (LA spring-forward DAY case.) - Overlap
W(fall-back DST):O0is necessarily one of the two ambiguous offsets (sinceWis withinunitMicrosofwall(micros)), andZonedDateTime.ofLocal(W, zone, preferredOffset=O0)picks the same instant ascandidate. - Gap
W(forward DST):candidate = W - O0always lands in a segment with offset !=O0(regardless of whetherO0is the pre- or post-offset), so re-check fails -> fallback, where the slow path's gap resolution kicks in. (Sao_Paulo midnight gap.)
So whenever the fast path returns directly, it returns the slow path's value byte-for-byte.
Implicit precondition: the algorithm is valid only for unitMicros that divide MICROS_PER_DAY (true for MINUTE/HOUR/DAY); it doesn't extend to WEEK/MONTH/QUARTER because those aren't fixed-width in micros. Worth stating in the Scaladoc — see inline comment.
LGTM modulo the inline comments (test coverage extension and a Scaladoc precondition note).
| === expectedDay2) | ||
| } | ||
|
|
||
| test("SPARK-30766/30857: truncTimestamp before the epoch in HOUR/DAY") { |
There was a problem hiding this comment.
The existing SPARK-33404 test at line 692 loops over ALL_TIMEZONES with the 1769-10-17 LMT input but only exercises TRUNC_TO_MINUTE/SECOND/MILLI/MICRO. With this PR, TRUNC_TO_HOUR and TRUNC_TO_DAY also flow through the same offset-arithmetic path, and the LMT (sub-minute offset) case is exactly the bug class that motivated the slow path. The new test here only covers LA at 1960 (no LMT). Adding two lines to the SPARK-33404 loop would extend coverage to every available zone at near-zero cost — the most direct regression guard against re-introducing the SPARK-33404 bug:
testTrunc(DateTimeUtils.TRUNC_TO_HOUR, "1769-10-17T17:00:00", inputTS.get, zid)
testTrunc(DateTimeUtils.TRUNC_TO_DAY, "1769-10-17T00:00:00", inputTS.get, zid)There was a problem hiding this comment.
Added these two lines into the SPARK-33404 test.
| * skipped for fixed-offset zones. Sub-minute offsets (e.g. America/Los_Angeles LMT | ||
| * -07:52:58, see SPARK-33404) and 30/45-minute offsets (Asia/Kolkata +05:30, Asia/Kathmandu | ||
| * +05:45) are handled correctly by this path because the offset is applied as part of | ||
| * the arithmetic; no offset-alignment guard is needed. |
There was a problem hiding this comment.
The algorithm depends on unitMicros dividing MICROS_PER_DAY — that's what makes wall-clock unit boundaries align to multiples of unitMicros in the shifted-local frame (since epoch is itself a unit boundary). It's true for all three call sites today, but if someone later extends truncToUnitFast to a non-dividing unit (or a unit that isn't fixed-width in micros) it would silently misalign. Worth one sentence in the Scaladoc, e.g.:
unitMicrosmust evenly divideMICROS_PER_DAY; otherwise wall-clock unit boundaries do not align to multiples ofunitMicrosin the shifted-local frame and thefloorModtruncation is unsafe.
There was a problem hiding this comment.
Added the sentence into the Scaladoc.
| // Spring-forward in LA: 2024-03-10 02:00 PDT does not exist; 02:30 local maps to | ||
| // 2024-03-10 03:30 PDT in wall-clock terms. Use an instant just after the transition | ||
| // so HOUR/DAY truncation candidate falls into the pre-transition offset window. |
There was a problem hiding this comment.
Minor: "2024-03-10 02:00 PDT does not exist" mislabels the gap. It's the wall-clock reading 02:00–02:59 that doesn't exist on 2024-03-10 in LA, regardless of label; 2024-03-10T02:00-07:00 is itself a well-formed UTC instant (08:00 UTC) — one hour before the transition, actually in PST.
| // Spring-forward in LA: 2024-03-10 02:00 PDT does not exist; 02:30 local maps to | |
| // 2024-03-10 03:30 PDT in wall-clock terms. Use an instant just after the transition | |
| // so HOUR/DAY truncation candidate falls into the pre-transition offset window. | |
| // Spring-forward in LA: local 02:00-02:59 doesn't exist on 2024-03-10 | |
| // (01:59 PST jumps to 03:00 PDT). Pick 03:30 PDT just after the transition | |
| // so the HOUR/DAY truncation candidate falls into the pre-transition window. |
There was a problem hiding this comment.
Changed the comment as per your suggestion.
…on for truncToUnitFast
…be the gap precisely
|
Thanks, @cloud-fan. Just addressed your comments! |
|
thanks, merging to master/4.x/4.2! |
Closes [SPARK-56663](https://issues.apache.org/jira/browse/SPARK-56663). ### What changes were proposed in this pull request? This PR re-introduces a fast path in `DateTimeUtils.truncTimestamp` for `TRUNC_TO_MINUTE`, `TRUNC_TO_HOUR`, and `TRUNC_TO_DAY`. A new private `truncToUnitFast(micros, zoneId, unitMicros, fallbackUnit)` helper: 1. Resolves the zone offset for the input instant once (`zoneId.getRules.getOffset(...)`). 2. Truncates by `Math.floorMod` in local time: `local = micros + offsetMicros`, then `truncatedLocal = local - floorMod(local, unitMicros)`, then back: `candidate = truncatedLocal - offsetMicros`. 3. Re-checks the offset at `candidate` and falls back to the existing `truncatedTo()`-based slow path if it differs from the original offset (the SPARK-30766 / SPARK-30857 DST guard). 4. The DST re-check is skipped for `ZoneRules.isFixedOffset` zones. 5. `Math.addExact` / `subtractExact` guard the offset arithmetic; on overflow we also fall back to the slow path. The arithmetic also handles sub-minute LMT offsets (e.g. America/Los_Angeles `-07:52:58` pre-1883 - the SPARK-33404 case) and 30/45-minute zones (Asia/Kolkata `+05:30`, Asia/Kathmandu `+05:45`) without an offset-alignment guard, because the offset is applied as part of the floorMod. Date-level units (WEEK/MONTH/QUARTER/YEAR + `trunc(date, ...)`) are not touched in this PR; they still go through `microsToDays` / `daysToMicros`. Those can follow up in a separate ticket. ### Why are the changes needed? SPARK-33404 (Nov 2020) fixed a correctness bug - `date_trunc('minute', '1769-10-17 17:10:02')` in `America/Los_Angeles` returned the original timestamp because the existing fast path ignored the time-zone offset and the LMT offset has second granularity. The fix routed MINUTE/HOUR/DAY through `microsToInstant().atZone(zoneId).truncatedTo(unit)`, which allocates `ZonedDateTime` per row. The author of that fix flagged the regression on the follow-up benchmark PR: > "Slow down is around 5.5 times" - MaxGekk, #30338 That regression has stood for ~5.5 years with no follow-up ticket. This PR restores most of the lost throughput without re-introducing the LMT bug, by applying the offset before/after the floorMod and verifying the offset is unchanged at the truncated candidate (handling DST transitions that motivated SPARK-30766 / SPARK-30857). `DateTimeBenchmark` Truncation results, wholestage on, ns/row on a 12th Gen Intel i7-1260P: | level | baseline | this PR | speedup | |---------|---------:|--------:|--------:| | MINUTE | 136.0 | 69.5 | 1.96× | | HOUR | 136.2 | 69.8 | 1.95× | | DAY | 136.5 | 69.8 | 1.96× | | DD | 136.7 | 70.1 | 1.95× | Other levels are unchanged (within noise). ### Does this PR introduce _any_ user-facing change? No. The output of `date_trunc` for `MINUTE`/`HOUR`/`DAY` is identical to master in all cases; only the internal implementation changes. ### How was this patch tested? - `DateTimeUtilsSuite` - all 63 tests pass, including: - `SPARK-33404: test truncTimestamp when time zone offset from UTC has a granularity of seconds`, which loops MINUTE/SECOND/MILLI/MICRO truncation across **every** available zone (covers historical LMT zones). - The existing `truncTimestamp` test, which loops HOUR/DAY/MINUTE/etc. across every available zone. - Five new tests added in this PR: - `truncTimestamp with sub-hour zone offsets` - Asia/Kolkata `+05:30` and Asia/Kathmandu `+05:45` HOUR/DAY truncation. - `truncTimestamp across DST transitions` - America/Los_Angeles spring-forward (2024-03-10) and fall-back (2024-11-03) for HOUR/DAY, exercising the offset-equality fallback. - `SPARK-30766/30857: truncTimestamp before the epoch in HOUR/DAY` - pre-1970 instant in America/Los_Angeles for HOUR and DAY. - `truncTimestamp at America/Sao_Paulo midnight DST gap` - 2018-11-04 (the last Brazilian DST start, jumping from -3 to -2 at exactly midnight local), exercising the case where the local day boundary itself falls in a gap and DAY truncation must resolve forward. - `truncTimestamp at Pacific/Apia after the 2011 calendar shift` - mid-2012 instant in a +13:00 zone whose history skipped a calendar date, verifying the fast path under a non-DST historical offset change. - `DateExpressionsSuite` - all 68 tests pass. - `DateTimeBenchmark` re-run locally with the numbers shown above. Per the contributor guide, the official `benchmarks/DateTimeBenchmark-results.txt` should be regenerated on the standard CI runner - happy to do that via the GitHub Actions benchmark workflow on request. ### Was this patch authored or co-authored using generative AI tooling? Yes, co-authored by Claude Code. Closes #55610 from Licht-T/date-trunc-fastpath. Authored-by: Rito Takeuchi <licht-t@outlook.jp> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit d6a39df) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
Closes [SPARK-56663](https://issues.apache.org/jira/browse/SPARK-56663). ### What changes were proposed in this pull request? This PR re-introduces a fast path in `DateTimeUtils.truncTimestamp` for `TRUNC_TO_MINUTE`, `TRUNC_TO_HOUR`, and `TRUNC_TO_DAY`. A new private `truncToUnitFast(micros, zoneId, unitMicros, fallbackUnit)` helper: 1. Resolves the zone offset for the input instant once (`zoneId.getRules.getOffset(...)`). 2. Truncates by `Math.floorMod` in local time: `local = micros + offsetMicros`, then `truncatedLocal = local - floorMod(local, unitMicros)`, then back: `candidate = truncatedLocal - offsetMicros`. 3. Re-checks the offset at `candidate` and falls back to the existing `truncatedTo()`-based slow path if it differs from the original offset (the SPARK-30766 / SPARK-30857 DST guard). 4. The DST re-check is skipped for `ZoneRules.isFixedOffset` zones. 5. `Math.addExact` / `subtractExact` guard the offset arithmetic; on overflow we also fall back to the slow path. The arithmetic also handles sub-minute LMT offsets (e.g. America/Los_Angeles `-07:52:58` pre-1883 - the SPARK-33404 case) and 30/45-minute zones (Asia/Kolkata `+05:30`, Asia/Kathmandu `+05:45`) without an offset-alignment guard, because the offset is applied as part of the floorMod. Date-level units (WEEK/MONTH/QUARTER/YEAR + `trunc(date, ...)`) are not touched in this PR; they still go through `microsToDays` / `daysToMicros`. Those can follow up in a separate ticket. ### Why are the changes needed? SPARK-33404 (Nov 2020) fixed a correctness bug - `date_trunc('minute', '1769-10-17 17:10:02')` in `America/Los_Angeles` returned the original timestamp because the existing fast path ignored the time-zone offset and the LMT offset has second granularity. The fix routed MINUTE/HOUR/DAY through `microsToInstant().atZone(zoneId).truncatedTo(unit)`, which allocates `ZonedDateTime` per row. The author of that fix flagged the regression on the follow-up benchmark PR: > "Slow down is around 5.5 times" - MaxGekk, #30338 That regression has stood for ~5.5 years with no follow-up ticket. This PR restores most of the lost throughput without re-introducing the LMT bug, by applying the offset before/after the floorMod and verifying the offset is unchanged at the truncated candidate (handling DST transitions that motivated SPARK-30766 / SPARK-30857). `DateTimeBenchmark` Truncation results, wholestage on, ns/row on a 12th Gen Intel i7-1260P: | level | baseline | this PR | speedup | |---------|---------:|--------:|--------:| | MINUTE | 136.0 | 69.5 | 1.96× | | HOUR | 136.2 | 69.8 | 1.95× | | DAY | 136.5 | 69.8 | 1.96× | | DD | 136.7 | 70.1 | 1.95× | Other levels are unchanged (within noise). ### Does this PR introduce _any_ user-facing change? No. The output of `date_trunc` for `MINUTE`/`HOUR`/`DAY` is identical to master in all cases; only the internal implementation changes. ### How was this patch tested? - `DateTimeUtilsSuite` - all 63 tests pass, including: - `SPARK-33404: test truncTimestamp when time zone offset from UTC has a granularity of seconds`, which loops MINUTE/SECOND/MILLI/MICRO truncation across **every** available zone (covers historical LMT zones). - The existing `truncTimestamp` test, which loops HOUR/DAY/MINUTE/etc. across every available zone. - Five new tests added in this PR: - `truncTimestamp with sub-hour zone offsets` - Asia/Kolkata `+05:30` and Asia/Kathmandu `+05:45` HOUR/DAY truncation. - `truncTimestamp across DST transitions` - America/Los_Angeles spring-forward (2024-03-10) and fall-back (2024-11-03) for HOUR/DAY, exercising the offset-equality fallback. - `SPARK-30766/30857: truncTimestamp before the epoch in HOUR/DAY` - pre-1970 instant in America/Los_Angeles for HOUR and DAY. - `truncTimestamp at America/Sao_Paulo midnight DST gap` - 2018-11-04 (the last Brazilian DST start, jumping from -3 to -2 at exactly midnight local), exercising the case where the local day boundary itself falls in a gap and DAY truncation must resolve forward. - `truncTimestamp at Pacific/Apia after the 2011 calendar shift` - mid-2012 instant in a +13:00 zone whose history skipped a calendar date, verifying the fast path under a non-DST historical offset change. - `DateExpressionsSuite` - all 68 tests pass. - `DateTimeBenchmark` re-run locally with the numbers shown above. Per the contributor guide, the official `benchmarks/DateTimeBenchmark-results.txt` should be regenerated on the standard CI runner - happy to do that via the GitHub Actions benchmark workflow on request. ### Was this patch authored or co-authored using generative AI tooling? Yes, co-authored by Claude Code. Closes #55610 from Licht-T/date-trunc-fastpath. Authored-by: Rito Takeuchi <licht-t@outlook.jp> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit d6a39df) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
…ath. ### What changes were proposed in this pull request? Fixes the bug introduced by #55610. When `local` is a very large negative value, `Math.floorMod(local, unitMicros)` is positive because `unitMicros` is positive, and the subtraction can overflow to positive values. This produces a inconsistent result from the slow path. In most common cases, this optimization still takes effect. The performance loss is ignorable. ### Why are the changes needed? Fix incorrect result. ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? New unit tests. It passes before #55610, or after this change. But it fails on the current master. ### Was this patch authored or co-authored using generative AI tooling? No. Closes #56313 from chenhao-db/fix_trunc_overflow. Authored-by: chenhao-db <chenhao.li@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
…ath. ### What changes were proposed in this pull request? Fixes the bug introduced by #55610. When `local` is a very large negative value, `Math.floorMod(local, unitMicros)` is positive because `unitMicros` is positive, and the subtraction can overflow to positive values. This produces a inconsistent result from the slow path. In most common cases, this optimization still takes effect. The performance loss is ignorable. ### Why are the changes needed? Fix incorrect result. ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? New unit tests. It passes before #55610, or after this change. But it fails on the current master. ### Was this patch authored or co-authored using generative AI tooling? No. Closes #56313 from chenhao-db/fix_trunc_overflow. Authored-by: chenhao-db <chenhao.li@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit 94d23c3) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
…ath. ### What changes were proposed in this pull request? Fixes the bug introduced by #55610. When `local` is a very large negative value, `Math.floorMod(local, unitMicros)` is positive because `unitMicros` is positive, and the subtraction can overflow to positive values. This produces a inconsistent result from the slow path. In most common cases, this optimization still takes effect. The performance loss is ignorable. ### Why are the changes needed? Fix incorrect result. ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? New unit tests. It passes before #55610, or after this change. But it fails on the current master. ### Was this patch authored or co-authored using generative AI tooling? No. Closes #56313 from chenhao-db/fix_trunc_overflow. Authored-by: chenhao-db <chenhao.li@databricks.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com> (cherry picked from commit 94d23c3) Signed-off-by: Wenchen Fan <wenchen@databricks.com>
Closes SPARK-56663.
What changes were proposed in this pull request?
This PR re-introduces a fast path in
DateTimeUtils.truncTimestampforTRUNC_TO_MINUTE,TRUNC_TO_HOUR, andTRUNC_TO_DAY. A new privatetruncToUnitFast(micros, zoneId, unitMicros, fallbackUnit)helper:zoneId.getRules.getOffset(...)).Math.floorModin local time:local = micros + offsetMicros, thentruncatedLocal = local - floorMod(local, unitMicros), then back:candidate = truncatedLocal - offsetMicros.candidateand falls back to the existingtruncatedTo()-based slow path if it differs from the original offset (the SPARK-30766 / SPARK-30857 DST guard).ZoneRules.isFixedOffsetzones.Math.addExact/subtractExactguard the offset arithmetic; on overflow we also fall back to the slow path.The arithmetic also handles sub-minute LMT offsets (e.g. America/Los_Angeles
-07:52:58pre-1883 - the SPARK-33404 case) and 30/45-minute zones (Asia/Kolkata+05:30, Asia/Kathmandu+05:45) without an offset-alignment guard, because the offset is applied as part of the floorMod.Date-level units (WEEK/MONTH/QUARTER/YEAR +
trunc(date, ...)) are not touched in this PR; they still go throughmicrosToDays/daysToMicros. Those can follow up in a separate ticket.Why are the changes needed?
SPARK-33404 (Nov 2020) fixed a correctness bug -
date_trunc('minute', '1769-10-17 17:10:02')inAmerica/Los_Angelesreturned the original timestamp because the existing fast path ignored the time-zone offset and the LMT offset has second granularity. The fix routed MINUTE/HOUR/DAY throughmicrosToInstant().atZone(zoneId).truncatedTo(unit), which allocatesZonedDateTimeper row.The author of that fix flagged the regression on the follow-up benchmark PR:
That regression has stood for ~5.5 years with no follow-up ticket. This PR restores most of the lost throughput without re-introducing the LMT bug, by applying the offset before/after the floorMod and verifying the offset is unchanged at the truncated candidate (handling DST transitions that motivated SPARK-30766 / SPARK-30857).
DateTimeBenchmarkTruncation results, wholestage on, ns/row on a 12th Gen Intel i7-1260P:Other levels are unchanged (within noise).
Does this PR introduce any user-facing change?
No. The output of
date_truncforMINUTE/HOUR/DAYis identical to master in all cases; only the internal implementation changes.How was this patch tested?
DateTimeUtilsSuite- all 63 tests pass, including:SPARK-33404: test truncTimestamp when time zone offset from UTC has a granularity of seconds, which loops MINUTE/SECOND/MILLI/MICRO truncation across every available zone (covers historical LMT zones).truncTimestamptest, which loops HOUR/DAY/MINUTE/etc. across every available zone.truncTimestamp with sub-hour zone offsets- Asia/Kolkata+05:30and Asia/Kathmandu+05:45HOUR/DAY truncation.truncTimestamp across DST transitions- America/Los_Angeles spring-forward (2024-03-10) and fall-back (2024-11-03) for HOUR/DAY, exercising the offset-equality fallback.SPARK-30766/30857: truncTimestamp before the epoch in HOUR/DAY- pre-1970 instant in America/Los_Angeles for HOUR and DAY.truncTimestamp at America/Sao_Paulo midnight DST gap- 2018-11-04 (the last Brazilian DST start, jumping from -3 to -2 at exactly midnight local), exercising the case where the local day boundary itself falls in a gap and DAY truncation must resolve forward.truncTimestamp at Pacific/Apia after the 2011 calendar shift- mid-2012 instant in a +13:00 zone whose history skipped a calendar date, verifying the fast path under a non-DST historical offset change.DateExpressionsSuite- all 68 tests pass.DateTimeBenchmarkre-run locally with the numbers shown above. Per the contributor guide, the officialbenchmarks/DateTimeBenchmark-results.txtshould be regenerated on the standard CI runner - happy to do that via the GitHub Actions benchmark workflow on request.Was this patch authored or co-authored using generative AI tooling?
Yes, co-authored by Claude Code.