test: characterization suite for timestamps with time zone - #25175
Open
adriangb wants to merge 2 commits into
Open
test: characterization suite for timestamps with time zone#25175adriangb wants to merge 2 commits into
adriangb wants to merge 2 commits into
Conversation
Adds `test_files/datetime/timestamps_timezone.slt`, ~1500 lines in 18 labelled sections that record what DataFusion actually does today with timezone-aware timestamps. DataFusion has a long tail of timezone correctness bugs, and the reason they keep recurring is structural: almost nothing in the test suite pins the behaviour down, so a change to timezone semantics can land without producing a single test diff. `SET datafusion.execution.time_zone` appears roughly 40 times in the whole sqllogictest corpus, nearly all of it in two files, and DST-boundary dates are essentially absent. This file does not change any behaviour. It writes the current behaviour down, including the parts that are known wrong or known to disagree with PostgreSQL -- those carry a comment saying so and a link to the issue, rather than being "fixed" here. When a fix lands, the diff is the specification of what changed. Every case is exercised as a literal and, where it matters, as a real CTAS column, since several of these behaviours only reproduce on columns. Sections cover literal typing under five session zones, `AT TIME ZONE`, casts in all four directions, round trips, tz-aware/tz-naive comparison (with `EXPLAIN` of the rewritten filter), `date_bin`/`date_trunc`/`date_part`, `to_char` / `to_local_time` / `to_timestamp*` / `from_unixtime`, `now`/`current_date`, interval arithmetic across both DST transitions, aggregates, joins and coercion, the US and EU DST boundaries by all four routes into a named zone, and a no-DST zone plus a half-hour zone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 10, 2026
This was referenced Sep 10, 2026
Section 9 already had a `date_bin(..., origin)` case, but all of its data sits inside a single July day, so it never crosses a transition and the drift is invisible. Adds a case whose two rows are the same local time of day on either side of the America/Denver spring-forward transition, with the origin chosen so bins land on local midnight. The first row does; the second lands on 01:00 local. That is the bug in apache#25168 -- `date_bin` steps a fixed number of nanoseconds from a fixed instant, so it cannot track a local day that is 23 or 25 hours long. `date_trunc` is asserted alongside as a control: it gets both rows right, so the drift is a property of the origin arithmetic rather than of the data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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?
This PR closes no issue and fixes no bug. It adds regression-prevention coverage for the area the issues below live in, and records the current (in several places wrong) behaviour so that a fix for any of them shows up here as an exact diff.
date_binon timestamps with timezone, properly accounting for Daylight Savings Time #10602datafusion.execution.time_zoneis not used for basic time zone inference #13212Timestamp(_, None)to a named timezone errors on DST boundaries #25084date_binanddate_truncdisagree on timezone-aware timestamps #25167AT TIME ZONE '+05:30'uses the opposite sign convention from PostgreSQL #25170Rationale for this change
DataFusion has a long tail of timezone correctness bugs. They keep recurring, and the reason is structural: almost nothing in the test suite pins the behaviour down, so a change to timezone semantics can land without producing a single test diff.
Concretely, on today's
main:SET datafusion.execution.time_zoneappears roughly 40 times in the entire sqllogictest corpus, and nearly all of those are concentrated in two files (to_timestamp_timezone.sltandset_variable.slt).The result is that the semantics people actually hit in production — what
::timestamptzproduces, whether a naive literal in aWHEREis read in the session zone or the column's zone, whetherdate_binanddate_truncagree, what happens at 01:30 on the first Sunday in November — are undefined by the test suite. A refactor can change any of them silently, and a fix for one can regress another.This PR does not change any behaviour. It writes the current behaviour down, so that from now on any change to timezone semantics shows up as a test diff and has to be argued for rather than discovered by a user. Where today's behaviour is wrong, or disagrees with PostgreSQL, the expected output still records what DataFusion does — with a comment directly above saying so and linking the issue. When a fix lands, the diff is the specification of what changed.
This is the companion to #25164, which adds the first
pg_compatfile fortimestamptzand contains only the subset the two engines agree on. The two were split apart because they have very different risk profiles: a failure in the pg_compat file is always a real regression, whereas this file deliberately pins behaviour we expect to change.What changes are included in this PR?
One new test file. No production code is touched.
datafusion/sqllogictest/test_files/datetime/timestamps_timezone.slt(~1550 lines, 18 labelled sections)Every case is exercised both as a literal and, where it matters, as a real CTAS column — several of these behaviours only reproduce on columns.
arrow_typeofand value of'...'::timestamp,'...'::timestamptz,TIMESTAMP '...'andTIMESTAMP WITH TIME ZONE '...'under session zone unset /+00:00/+05:30/America/Denver/Europe/Brussels, for literals and for columns. Records that with the session zone unset,::timestamptzyields a tz-naiveTimestamp(ns)(TIMESTAMP WITH TIME ZONE can resolve to a timezone-naive type, and casting to it discards an existing timezone #25166).AT TIME ZONEon tz-naive and tz-aware input, type and value, including the fixed-offset-string sign convention (AT TIME ZONE '+05:30'uses the opposite sign convention from PostgreSQL #25170).t = t::timestamptz::timestampunder UTC and non-UTC session zones.EXPLAINof the filter so that any change to theunwrap_cast/simplify_expressionsrewrite is visible in the plan (unwrap_cast_in_comparison drops the timezone shift when unwrapping CAST(timestamp AS timestamptz) = literal #25095).date_bin,date_trunc,date_part,extract,to_char,from_unixtime,to_unixtime,to_timestamp*,to_local_time,now,current_date,current_time,make_dateon tz-aware input.INTERVAL '1 day'vsINTERVAL '24 hours'distinction in both directions.MIN/MAX/GROUP BY/ORDER BY/DISTINCTover a tz-aware column; joins on tz-aware keys including a mixed+00:00/America/Denverjoin;UNION/CASE/COALESCE/greatestcoercion across mixed zones.2024-11-03 01:30:00and spring-forward2024-03-10 02:30:00inAmerica/Denver, EU2024-10-27 02:30:00and2024-03-31 02:30:00inEurope/Brussels, each via column cast,AT TIME ZONE, string literal and::timestamptz.America/Phoenix) and a half-hour zone (Asia/Kolkata).#25165 changes behaviour that SECTION 5 of this file pins. Whichever of the two merges second has to update the other, and CI on the second one will go red until it does.
#25165 makes
AT TIME ZONEon an already timezone-aware input return a naiveTimestamp(ns)(matching PostgreSQL) instead of a relabelled tz-aware value. SECTION 5b is exactly that case, on thec_utcandc_denvercolumns. Once #25165 lands, these two queries change:Timestamp(ns, "Europe/Brussels") 2024-01-15T13:00:00+01:00Timestamp(ns) 2024-01-15T13:00:00Timestamp(ns, "Europe/Brussels") 2024-07-01T14:00:00+02:00Timestamp(ns) 2024-07-01T14:00:00Timestamp(ns, "America/Denver") 2024-01-15T12:00:00-07:00Timestamp(ns) 2024-01-15T12:00:00Timestamp(ns, "America/Denver") 2024-07-01T12:00:00-06:00Timestamp(ns) 2024-07-01T12:00:00The
DIVERGES FROM POSTGRESQL (type, not instant)comment above them should be deleted at the same time, since #25165 removes that divergence.No other
AT TIME ZONEcase in the file collides: 5a, 5c, the composed(… ::timestamptz AT TIME ZONE …)::timestampcase, the interval-arithmetic case in SECTION 12 and all of SECTION 16 apply the operator to a timezone-naive input, which #25165 leaves alone.Other in-flight PRs, checked
timezone,timezone_hourandtimezone_minuteindate_part#25163 (timezone/timezone_hour/timezone_minuteindate_part) also collides. SECTION 9 pins the current rejection:timezone,timezone_hourandtimezone_minuteindate_part#25163 lands, bothtimezone_hourandtimezone_minutestart returning values (-6and0for every row ofday_denver, which is MDT throughout), so the twoquery errorblocks become value queries and the "GAP vs PostgreSQL … no way to ask a tz-aware value for its own offset" comment above them should go.from_unixtimeshould respectdatafusion.execution.time_zone#25161 (from_unixtimerespects the session time zone) does not break any assertion here: SECTION 10'sfrom_unixtime(1719792000)case runs with the session zone unset, and fix:from_unixtimeshould respectdatafusion.execution.time_zone#25161 keepsTimestamp(Second, None)in that case. The comment above it sayingfrom_unixtime"ignoresdatafusion.execution.time_zone" does become stale and should be reworded when that PR lands.generate_series/range#25173 (generate_seriestimestamp precision) does not collide: the file uses nogenerate_series, and fix: accept all timestamp precisions ingenerate_series/range#25173 touches onlygenerate_series.rsandtable_functions.slt.I have left the file pinning today's behaviour, since that is what this PR is for. Happy to rebase it onto whichever of the above merges first.
What is the testing strategy for this PR?
This PR is tests. Results were generated with the sqllogictest
--completemode rather than hand-written, and then every generated result was read and sanity-checked; several were wrong and were rewritten (e.g. queries that tripped DataFusion's projection-name-uniqueness rule, and a- INTERVAL '1 day'case that did not actually cross a DST boundary). Cross-engine claims in the comments were checked against a real PostgreSQL.Verified locally:
cargo test -p datafusion-sqllogictest --test sqllogictests— full suite green (506 files)cargo fmt --all,typos datafusion/ docs/— cleanDivergences from PostgreSQL recorded in the file
Every one is recorded with both answers and a comment; none is fixed here.
::timestamptzwith the session zone unset is tz-naive. PostgreSQL always yieldstimestamp with time zone. (TIMESTAMP WITH TIME ZONE can resolve to a timezone-naive type, and casting to it discards an existing timezone #25166)timestamptz AT TIME ZONE zonereturns a tz-aware value, relabelled intozone; PostgreSQL drops the zone and returns a naivetimestampholding the wall clock inzone. This is what fix:AT TIME ZONEon a timezone-aware timestamp returns a naive timestamp #25165 fixes.AT TIME ZONE '+05:30'uses the opposite sign convention from PostgreSQL's string form. DataFusion reads'+05:30'as UTC+05:30; PostgreSQL reads the string POSIX-style (west positive) and only itsINTERVALform agrees with DataFusion. (AT TIME ZONE '+05:30'uses the opposite sign convention from PostgreSQL #25170)timestamptz::timestampalways renders in UTC and ignoresdatafusion.execution.time_zone; PostgreSQL renders in the sessionTimeZone. (Casting existing timestamp to timestamp again strips timezone information #12218)SELECT ts FROM t WHERE ts > '2024-07-01 06:00:00'over a Denver-typed column returns 1 row in DataFusion and 2 in PostgreSQL underTimeZone='UTC'. The session zone has no effect on this path at all. (unwrap_cast_in_comparison drops the timezone shift when unwrapping CAST(timestamp AS timestamptz) = literal #25095)WHERE ts = '2024-07-01T06:00:00Z'::timestamptzagainst a Denver column returns the 12:00Z row, not the 06:00Z one — theZis silently discarded and the wall clock re-read in the column's zone. Setting a session zone fixes it. Same root cause inCASE.date_trunctruncates in the value's zone; PostgreSQL truncates in the sessionTimeZone(or an explicit third argument, which DataFusion does not accept).date_bin, by contrast, agrees with PostgreSQL.date_binanddate_truncdisagree with each other inside DataFusion for the same input, whenever the zone offset is not a whole multiple of the stride. (date_binanddate_truncdisagree on timezone-aware timestamps #25167)date_part('timezone_hour')anddate_part('timezone_minute')are rejected —Execution error: Date part 'timezone_hour' not supported. PostgreSQL supports both. There is currently no way to ask a tz-aware value for its own offset. (part of Better timezone functionalities #10368)TimeZone. When the two are aligned the engines agree exactly, and that agreement is pinned in test: PostgreSQL differential coverage for timestamps with time zone #25164.now()is tz-naive with the session zone unset,current_timeisTime64(ns)where PostgreSQL hastime with time zone, andfrom_unixtime/to_timestamp*return naive values unless a session zone is set (Make from_unixtime aware of execution timezone #12892).The DST-boundary behaviour, precisely
DataFusion cannot represent an ambiguous or non-existent local time in a named zone by any route. Column cast,
AT TIME ZONE, string literal and::timestamptzunder a session zone all error. The column path and the literal path fail with different errors for the same value:Arrow error: Cast error: Cannot cast timezone to different timezonesimplify_expressions, thenArrow error: Parser error: ... error computing timezone offsetPostgreSQL resolves all four cases:
2024-11-03 01:30:00Denver →01:30:00-07(the second, standard-time occurrence);2024-03-10 02:30:00Denver →03:30:00-06(shifted forward out of the gap);2024-10-27 02:30:00Brussels →02:30:00+01;2024-03-31 02:30:00Brussels →03:30:00+02.This is #25084, and apache/arrow-rs#11038 is the arrow-rs fix. Note that the failure is not limited to columns — literals fail too, just with a different message. Both paths, and both error texts, are pinned in the file so the fix will show up as an exact diff.
Once the values are built from explicit UTC instants the boundary is navigable, and that behaviour is characterized too: both occurrences of 01:30 render with the correct differing offsets,
to_local_timecollapses them onto the same naive value, and+ INTERVAL '1 hour'walks through the repeated hour rather than over it.Are there any user-facing changes?
No. This PR adds a single test file. No production code, no public API, and no behaviour is changed.
Also pinned: the
date_binexplicit-origin DST driftSection 9 gained a case for #25168. The section already
had a
date_bin(..., origin)query, but all of its data sat inside one July day, so it never crosseda transition and the drift was invisible.
The new case uses two rows at the same local time of day on either side of the America/Denver
spring-forward transition, with the origin chosen so that bins land on local midnight:
tsdate_bin(1 day, ts, origin)date_trunc('day', ts)2024-03-09T06:30:00-07:002024-03-09T00:00:00-07:002024-03-09T00:00:00-07:002024-03-11T06:30:00-06:002024-03-11T01:00:00-06:002024-03-11T00:00:00-06:00date_truncis asserted alongside as a control. It gets both rows right, which shows the drift is aproperty of the origin arithmetic and not of the data.
🤖 Generated with Claude Code