adriangb commented on PR #11054:
URL: https://github.com/apache/arrow-rs/pull/11054#issuecomment-5626606676

   Self-review QA pass — this is our own PR, so treat this as our own 
adversarial check, not an independent review.
   
   No blocking defect. The highest-risk part of this stack is the change of 
`resolve_local_offset` from `&Tz` to `<T: TimeZone>`, so I attacked that first. 
It is behaviour-preserving, and I verified that empirically rather than by 
reading. Four findings follow, in priority order.
   
   ## 1. The generalisation does not change the original cast call site — 
verified byte for byte
   
   This was the item that could regress #11038 silently. I ran the identical 
battery of cast-path readings on both branches and compared the raw output:
   
   * 15 readings in one array that straddles both 2024 `America/New_York` 
transitions.
   * 15 readings across `Australia/Sydney`, `Australia/Lord_Howe`, 
`Pacific/Chatham`, `America/Sao_Paulo`, `America/Havana`, `Europe/Lisbon`, 
`Europe/Dublin`, `Asia/Tehran`, `Pacific/Apia`, `America/Santiago` and 
`Asia/Kolkata`.
   * 13 readings inside the four 24-hour dateline gaps.
   
   43 rows, and the two files are identical. The cast path on this branch is 
bit-for-bit the cast path on #11038.
   
   The refactor moves `.fix()` from inside the function to the call site. 
`Option::map(|o| o.fix())` and then `?`, versus `?` and then `.fix()`, are the 
same operation in the same order. The `Single`, `Ambiguous` and `None` arms are 
otherwise unchanged. So the empirical result matches the reading of the diff.
   
   One note on the new wrapper. `resolve_local_datetime` builds its result with 
`tz.from_utc_datetime(&(*local - offset.fix()))`, not with 
`from_local_datetime`. That is deliberate and necessary — `from_local_datetime` 
would reject the two cases this function exists to resolve — but it is not 
obvious. A one-line comment at that call helps the next reader.
   
   ## 2. The stated safety argument for the 24-hour probe is not the 
load-bearing one
   
   This PR repeats #11038's justification: no two transitions are closer than 
167 hours. That figure is correct, and I reproduce it. But it is not the 
property that makes the probe resolvable.
   
   Two separate invariants are at work:
   
   * **Transition spacing above 24 hours.** This makes the probed offset the 
*correct* pre-transition offset. Margin: 167 hours.
   * **Maximum local gap at or below 24 hours.** This makes the probe land 
*outside* the gap at all. This PR does not state it, and it has no margin.
   
   Seven zones have a local gap of exactly 24.00 hours — the dateline changes: 
`Pacific/Apia`, `Pacific/Fakaofo`, `Pacific/Kiritimati`, `Pacific/Kanton`, 
`Pacific/Enderbury`, `Pacific/Kwajalein` and `Kwajalein`. The next largest gap 
in any zone is 10.00 hours. For a reading at the last second of a 24-hour gap 
the probe lands one second before it. The real margin is one second.
   
   The code is correct today. I tested the first instant, the midpoint and the 
last second of all four surviving dateline gaps through the cast path, and all 
13 readings match PostgreSQL 17.11 exactly. But the doc comment in 
`local_time.rs` now carries this claim for the whole crate, so please state 
both invariants there.
   
   ## 3. The give-up branch is unreachable, and this PR widens who depends on 
that
   
   `resolve_local_offset` returns `None` only when the probe itself lands in a 
gap. Both invariants above exclude that, so the branch is dead code with real 
tzdata. I could not construct an input that reaches it.
   
   This PR raises the stakes, because the branch now has four callers rather 
than one. I read all four. All handle `None` correctly:
   
   | Caller | `None` behaviour |
   | --- | --- |
   | `adjust_timestamp_to_timezone`, `safe: false` | `CastError("Cannot cast 
timezone to different timezone")` |
   | `adjust_timestamp_to_timezone`, `safe: true` | null |
   | `string_to_datetime` date-only site | `err("error computing timezone 
offset")` |
   | `string_to_datetime` naive-datetime site | `err("error computing timezone 
offset")` |
   | `string_to_datetime` named-zone site | `err("error computing timezone 
offset")` |
   
   The three parser sites return a `ParseError`, which the string cast then 
turns into a null under `safe: true` and propagates under `safe: false`. I 
confirmed both modes still behave that way, and that genuinely bad input is 
unaffected:
   
   ```
   str  safe=false ["2024-03-10 02:30:00", "2024-11-03 01:30:00"] -> 
[Some(1710055800), Some(1730615400)]
   str  safe=true  ["2024-03-10 02:30:00", "2024-11-03 01:30:00"] -> 
[Some(1710055800), Some(1730615400)]
   str  safe=false ["not a timestamp"]  -> Parser error: ... error parsing date
   str  safe=true  ["not a timestamp"]  -> [None]
   ```
   
   ## 4. The known divergence deserves the same prominence it has in #11038
   
   `arrow-array/src/types.rs:349` is untouched. I confirmed that: `git diff` 
against the base commit shows this branch changes nothing under `arrow-array/`, 
and the function still reads `Ambiguous(dt1, _)` and `None => None`. Measured 
in `America/New_York`:
   
   | `2024-11-03 01:30:00` | Result |
   | --- | --- |
   | `arrow_array::types::from_naive_datetime` | 1730611800 = `05:30Z`, the 
**earlier** instant |
   | `arrow_cast::local_time::resolve_local_offset` | 1730615400 = `06:30Z`, 
the **later** instant |
   
   `2024-03-10 02:30:00` gives `None` from the first and 1710055800 from the 
second.
   
   #11038 carries a full table and a paragraph on why the two must be 
reconciled. This PR mentions it in one parenthesis inside "What changes are 
included". I recommend the fuller disclosure here too. This is the PR that 
creates the single shared helper, so it is the PR that makes the surviving 
divergence look deliberate.
   
   ---
   
   ## What I checked and found correct
   
   **Both paths now share one policy — measured, not assumed.** I ran 5,012 
boundary readings across 226 zones through the cast path and the string path in 
the same run. Every DST transition of every zone from 2020 to 2025, sampled at 
the start and the midpoint of each gap and each repeated interval: 2,506 in a 
gap, 2,506 ambiguous.
   
   * The cast path and the string path agree on all 5,012 readings.
   * 0 unresolved readings under `safe: true`.
   
   That agreement is the central claim of this PR, and it holds across the 
whole tzdb, not just the tested zones.
   
   **Against PostgreSQL.** 3,908 of those readings use a zone PostgreSQL 17.11 
accepts. 16 disagree, on both paths equally. All 16 are `Europe/Chisinau` and 
its alias `Europe/Tiraspol`. That is the chrono-tz tzdata skew #11038 
discloses, and it predates both PRs:
   
   ```
   main             Europe/Chisinau 2024-03-31 03:30 -> 1711845000
   PostgreSQL 17.11                                  -> 1711848600
   DuckDB 1.5.2                                      -> 1711848600
   ```
   
   PostgreSQL and DuckDB agree with each other, so chrono-tz is the outlier. 
Please name `Europe/Tiraspol` alongside `Europe/Chisinau`.
   
   **Per-row resolution on the string path.** One `StringArray` that straddles 
both 2024 `America/New_York` transitions produces three distinct offsets, and 
every element matches PostgreSQL:
   
   | String | arrow | PostgreSQL 17.11 |
   | --- | --- | --- |
   | `2024-01-15 12:00:00` | 1705338000 | 1705338000 |
   | `2024-03-10 01:30:00` | 1710052200 | 1710052200 |
   | `2024-03-10 02:30:00` (gap) | 1710055800 | 1710055800 |
   | `2024-03-10 03:30:00` | 1710055800 | 1710055800 |
   | `2024-11-03 01:30:00` (ambiguous) | 1730615400 | 1730615400 |
   | `2024-11-03 02:30:00` | 1730619000 | 1730619000 |
   | `2024-12-15 12:00:00` | 1734282000 | 1734282000 |
   
   `Utf8`, `LargeUtf8` and `Utf8View` produce identical results on that array.
   
   **The four hard zones the PR names.** All verified against PostgreSQL 
independently:
   
   | Zone | Reading | arrow and PostgreSQL |
   | --- | --- | --- |
   | `Australia/Lord_Howe` | `2024-10-06 02:15` (30-minute step) | 1728143100 |
   | `Pacific/Chatham` | `2024-09-29 03:00` (+12:45 / +13:45) | 1727532900 |
   | `Australia/Sydney` | `2024-10-06 02:30` (southern gap) | 1728145800 |
   | `America/Sao_Paulo` | `2018-11-04` (midnight does not exist) | 1541300400 |
   | `America/Havana` | `2024-11-03` (midnight happens twice) | 1730610000 |
   
   **The site-3 argument holds.** I confirmed the PostgreSQL claim directly. 
`SET TimeZone='America/New_York'; SELECT '2024-03-10 02:30:00'::timestamptz;` 
and `SELECT timestamptz '2024-03-10 02:30:00 America/New_York';` both return 
`2024-03-10 07:30:00+00`. So PostgreSQL makes no distinction between the two 
spellings, and neither does this PR.
   
   **No call site is missed.** After this PR the only remaining local-time 
resolution in the tree outside `local_time.rs` is 
`arrow-array/src/types.rs:349`, which is the known divergence. 
`arrow-array/src/timezone.rs` only forwards to chrono.
   
   **Suite.** `cargo test -p arrow-cast` passes: 395 lib tests and 13 doc 
tests. The doc-test count rises from 12 to 13, which is the new 
`string_to_datetime` example.
   
   **The fixed-offset caveat is over-broad in the body.** I measured it. 
PostgreSQL uses POSIX west-positive signs only when `'+05:30'` acts as a *zone 
name* (`SET TimeZone`, or `AT TIME ZONE`), giving `17:30Z`. With the offset 
inside the literal, `timestamptz '2024-01-01 12:00:00 +05:30'` gives `06:30Z`, 
which agrees with arrow-rs. DuckDB 1.5.2 rejects the zone-name spelling 
outright. Please say "as a zone name".
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to