adriangb commented on PR #25163:
URL: https://github.com/apache/datafusion/pull/25163#issuecomment-5626585948

   **Self-review: a QA pass on my own PR.** I checked every claim in the 
description against a real PostgreSQL 17.11 and a real DuckDB 1.5.2, and I 
re-ran the build and the tests. I found no correctness bug. The sign 
convention, the return type and the `EXTRACT` path are all correct. Below are 
one user-visible gap, three nits, and then the full list of what I checked.
   
   ---
   
   ## 1. `date_part('timezone', now())` fails out of the box
   
   This is the first thing a user tries, and it does not work with the default 
configuration:
   
   ```sql
   > SELECT arrow_typeof(now());
   Timestamp(ns)
   
   > SELECT date_part('timezone', now());
   Execution error: Date part 'timezone' is not supported for timezone-naive 
timestamps, got Timestamp(ns)
   ```
   
   `datafusion.execution.time_zone` defaults to `None`, so `now()` is 
timezone-naive. A session zone fixes it:
   
   ```sql
   > SET datafusion.execution.time_zone = 'Europe/Brussels';
   > SELECT arrow_typeof(now()), date_part('timezone', now());
   Timestamp(ns, "Europe/Brussels")   7200
   ```
   
   PostgreSQL answers `date_part('timezone', now())` directly, because its 
`now()` is always `timestamptz`.
   
   The root cause is https://github.com/apache/datafusion/issues/25166, not 
this PR, and the fix does not belong here. But the PR adds the exact function a 
user reaches for, so the dead end is new. Two cheap additions cover it:
   
   - a `.slt` case for `now()` with and without a session zone, which pins the 
gap and turns into a diff when 25166 lands;
   - one sentence in the `user_doc!` block that names 
`datafusion.execution.time_zone`.
   
   ## 2. The doc sentence over-states the sign rule
   
   The doc block says: "For a negative offset both parts carry the sign". That 
is not exact. When the absolute offset is under one hour, the hour is `0` and 
loses the sign, and only the minute carries it. The code is still right — 
PostgreSQL does the same thing:
   
   | engine | zone | instant | `timezone` | `timezone_hour` | `timezone_minute` 
|
   | --- | --- | --- | --- | --- | --- |
   | PostgreSQL 17.11 | `Africa/Monrovia` | 1900-01-01T12:00:00Z | -2588 | 0 | 
-43 |
   | this PR | `Africa/Monrovia` | 1900-01-01T12:00:00Z | -2588 | 0 | -43 |
   
   Truncation towards zero matches on the seconds too, so this is an exact 
match and not a near miss. No modern zone has a negative offset under one hour, 
so only historical LMT data reaches it.
   
   Two suggestions:
   
   - reword to "the minute carries the sign of the offset, and so does the hour 
when the offset is a whole hour or more";
   - add the `Africa/Monrovia` row to `timezone_parts_match_postgres`. It is 
the one case the current 15-row table does not reach, and both engines agree on 
it.
   
   ## 3. Move `to_lowercase` inside `TimezonePart::parse`
   
   The call site is correct today, and I confirmed mixed case works after the 
#24906 rebase. But the invariant is fragile:
   
   ```rust
   Err(_) => match TimezonePart::parse(&part_trim.to_lowercase()) {
   ```
   
   `part_normalization` strips quotes only; it does not lower the case. 
`DatePart::from_str` and `is_epoch` each lower the case *inside* themselves. 
`TimezonePart::parse` is the one function in the file that depends on its 
caller. A second caller would silently break `date_part('TIMEZONE_HOUR', ...)`, 
and no local test catches it. Move `.to_lowercase()` into `parse` so the type 
owns its own contract.
   
   ## 4. Two error shapes for one condition
   
   The two messages read as two different rules:
   
   - `Date part 'timezone' is not supported for timezone-naive timestamps, got 
Timestamp(ns)`
   - `Date part 'timezone' is only supported for timestamps with a timezone, 
got Date32`
   
   Both mean "this value has no UTC offset". One shape is easier to grep for 
and easier to document. Neither message tells the user what to do next. A hint 
helps: name `AT TIME ZONE` or `datafusion.execution.time_zone`.
   
   ## 5. Two small errors in the PR description
   
   - The count is `349 tests`; the real number is `353`. The lib suite goes 
from 344 to 353.
   - The "After this PR" block shows a column named `utc_offset_seconds`, but 
the query carries no such alias.
   
   I fix both in the rewritten description.
   
   ---
   
   ## What I checked, and what it measured
   
   **Sign convention, against PostgreSQL 17.11.** `SET TimeZone TO '<zone>'`, 
then `date_part('<part>', timestamptz '<instant>')`. All 10 rows of the table 
in the PR description reproduce exactly, and 6 more zone and instant pairs do 
too:
   
   | zone | instant | `timezone` | `timezone_hour` | `timezone_minute` |
   | --- | --- | --- | --- | --- |
   | `America/St_Johns` | 2024-01-01T12:00:00Z | -12600 | -3 | -30 |
   | `America/St_Johns` | 2024-07-01T12:00:00Z | -9000 | -2 | -30 |
   | `Pacific/Chatham` | 2024-01-01T12:00:00Z | 49500 | 13 | 45 |
   | `Pacific/Chatham` | 2024-07-01T12:00:00Z | 45900 | 12 | 45 |
   | `Asia/Kolkata` | both | 19800 | 5 | 30 |
   | `Asia/Kathmandu` | both | 20700 | 5 | 45 |
   | `Europe/Brussels` | Jan / Jul | 3600 / 7200 | 1 / 2 | 0 / 0 |
   | `America/Denver` | Jan / Jul | -25200 / -21600 | -7 / -6 | 0 / 0 |
   | `Pacific/Marquesas` | both | -34200 | -9 | -30 |
   | `UTC` | both | 0 | 0 | 0 |
   
   So the negative minute is confirmed, not assumed.
   
   **DuckDB 1.5.2 supports all three fields**, and it agrees with PostgreSQL on 
every zone above. Two useful differences:
   
   - DuckDB returns `BIGINT`, not `double precision`. So an integer return type 
has precedent, and `Int32` here is not an outlier.
   - DuckDB returns `0` for a timezone-naive timestamp; PostgreSQL raises `unit 
"timezone" not supported for type timestamp without time zone`. This PR follows 
PostgreSQL. That is the right call, but the description presents `0` as 
self-evidently wrong when a major engine ships it. Worth a mention rather than 
a silent omission.
   
   DuckDB rejects `DATE` and `INTERVAL` input, as this PR does.
   
   **Return type.** `return_field_from_args` gives `Float64` for `epoch`, 
`Int64` for `nanosecond` and `Int32` for everything else. So the claim in the 
description holds, and `Int32` is the consistent choice. The divergence from 
PostgreSQL's `double precision` is pre-existing and applies to `hour`, `minute` 
and the rest already. It does not need a call-out in this PR.
   
   **`EXTRACT` needs no planner change.** Confirmed end to end. sqlparser 0.62 
renders `DateTimeField::TimezoneHour` as `TIMEZONE_HOUR`, the planner forwards 
that string, and the `.slt` cases for all three fields pass.
   
   **Case insensitivity after the #24906 rebase.** `date_part('TIMEZONE', 
...)`, `date_part('TIMEZONE_HOUR', ...)` and `date_part('Timezone_Minute', 
...)` all work, in the unit tests and in the `.slt`. See nit 3 for the 
fragility, not for a defect. Leading and trailing whitespace fails 
(`date_part('  timezone  ', ...)`), exactly as it fails for every other part on 
`main`. Quoted spellings such as `'''timezone'''` work.
   
   **Nullability.** `unary_opt` can turn an out-of-range value into a `NULL` 
even when the input field is not nullable. `date_part('hour', ...)` on the same 
value does the same thing, so Arrow's own kernel already behaves this way. 
Pre-existing and consistent, so no action.
   
   **`preimage`.** `DatePart::from_str("timezone")` fails, so `preimage` 
returns `PreimageResult::None` and no filter rewrite fires. Constant folding 
still works: `EXPLAIN` shows `Projection: Int32(7200)`.
   
   **Both commits build green on their own.**
   
   - commit 1 (`refactor: move parse_tz`): `cargo check --workspace 
--all-targets` clean, `cargo clippy -p datafusion-functions --all-targets -- -D 
warnings` clean, 344 lib tests pass.
   - commit 2 (`feat: ... date_part`): `cargo clippy --all-targets -- -D 
warnings` clean on the whole workspace, 353 lib tests pass, 
`datetime/date_part.slt` green.
   
   **Generated docs are in sync.** I re-ran `print_functions_docs -- scalar` 
and diffed the `date_part` section against the committed file. The content 
matches; the only differences are the prettier reflow that 
`dev/update_function_docs.sh` applies. `./ci/scripts/doc_prettier_check.sh` and 
`typos` are both clean.
   
   **The doc example is real.** `SELECT date_part('timezone', TIMESTAMP 
'2024-07-01T12:00:00' AT TIME ZONE 'Europe/Brussels')` returns `7200`, and the 
input type is `Timestamp(ns, "Europe/Brussels")`.
   
   **Merge order.** https://github.com/apache/datafusion/pull/25175 pins `Date 
part 'timezone_hour' not supported` in its SECTION 9. Whichever PR merges 
second must update the other. That PR already records the collision in its own 
description.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to