test: PostgreSQL differential coverage for timestamps with time zone - #25164
test: PostgreSQL differential coverage for timestamps with time zone#25164adriangb wants to merge 2 commits into
Conversation
Adds `test_files/pg_compat/pg_compat_timestamptz.slt`, the first pg_compat file
covering timestamps with time zones. Until now not one of the differential
files exercised `timestamptz` at all, so nothing in CI checked that DataFusion
and PostgreSQL agree on any of it.
The file contains only queries where the two engines genuinely agree. It is
organized into three blocks, each setting both engines' session zone explicitly
(`SET TimeZone` for PostgreSQL, `SET datafusion.execution.time_zone` for
DataFusion) and building its table in that same zone: UTC, `America/Denver`,
then `Asia/Kolkata` and `America/Phoenix`.
Two constraints shaped it, and are documented in the file header:
- The Postgres runner has no renderer for the `timestamptz` wire type, so no
query may *return* a tz-aware value. Every result is projected down to a
tz-naive `timestamp`, a `bigint`, a `boolean` or `text`. That turned out to
help rather than hurt: comparing via `date_part('epoch', ...)` tests the
instant directly, with no rendering in the way.
- PostgreSQL resolves a bare `timestamp` and renders a `timestamptz` using its
*session* `TimeZone`, whereas DataFusion uses the time zone carried by the
*value*. The two coincide only when the session zone equals the column's
zone, so each block aligns them deliberately.
Expected output was generated from real PostgreSQL with the sqllogictest
`--complete` mode, so PostgreSQL's answer is the expectation and DataFusion has
to match it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2c3caeb to
2b0a37f
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #25164 +/- ##
==========================================
+ Coverage 81.60% 81.91% +0.31%
==========================================
Files 1123 1132 +9
Lines 408898 421314 +12416
Branches 408898 421314 +12416
==========================================
+ Hits 333670 345130 +11460
- Misses 55625 55775 +150
- Partials 19603 20409 +806 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Self-review (QA pass) of our own PR, at head Result. The file passes against Findings (most important first)1. The license header fails the new
|
| Engine | Session time zone | Without the casts | With the casts (the file) |
|---|---|---|---|
| PostgreSQL 15 | UTC |
f |
f |
| DuckDB 1.5.2 | UTC |
false |
not measured |
DataFusion (datafusion-cli at the base of this PR) |
+00:00 |
true |
false |
| PostgreSQL 15 | America/Denver |
t |
not measured |
| DataFusion | America/Denver |
true |
not measured |
- DataFusion applies a day interval in the time zone of the value (Denver). PostgreSQL applies it in the session time zone (UTC).
- test: characterization suite for timestamps with time zone #25175 pins this divergence at lines 1027-1038.
- The comment above the query says that it tests DST offsets. With the casts, a change to DST behaviour cannot make this query fail.
Recommendation: remove the two casts and move the query to Block B. In Block B both engines use America/Denver, and both return true. If you keep the query in Block A, add a comment that names the divergence.
3. Lines 452-460: the comment describes the opposite case
The comment says: "the same UTC instant reads as a different local hour". Rows 3 and 4 are two different instants (19:00Z in January, 18:00Z in July). Both read as local hour 12. The query is valid, but the comment is wrong.
4. ::bigint removes the fractional part of epoch
The two engines convert a Float64 (DataFusion) or double precision (PostgreSQL) value to bigint in different ways:
| Expression | DataFusion | PostgreSQL 15 |
|---|---|---|
1.6 to bigint |
1 |
2 |
-1.6 to bigint |
-1 |
-2 |
DataFusion truncates and PostgreSQL rounds. Each value in the file is a whole second, so no query gives a different answer today. But the cast removes any sub-second part before the comparison. So the file cannot detect a sub-second error in epoch, and a future row with a fractional second can fail for a reason unrelated to time zones. Low priority: keep the data at whole seconds, and say so in the header.
What I checked and found correct
- PostgreSQL 15 run. I ran the full
pg_compatsuite withPG_COMPAT=trueagainst a freshpostgres:15container. All 7 files pass. - Proof that PostgreSQL really executes this file. I set
log_statement = 'all'and ran this file alone. The PostgreSQL log shows the harness session execute statementss3tos91, from the firstSET TimeZone = 'UTC'to the final one, with zero errors. - DataFusion-only mode. The file also passes without
PG_COMPAT. - CI. The "Run sqllogictest with Postgres runner" job passes at this head.
- Version sensitivity. On
postgres:15,extract(epoch ...)renders as1719792000.000000anddate_part('epoch', ...)renders as1719792000. Each query in the file casts both tobigint, so the result text does not depend on thenumericscale. - The harness constraint in the header is correct.
postgres_engine/mod.rsrendersType::TIMESTAMPand callsunimplemented!for other types,timestamptzincluded. - Audit of the other queries for a hidden divergence.
- Block A projects to
::timestamp. DataFusion renders an aware value in UTC, and PostgreSQL uses the session time zone. Block A sets UTC, so these projections agree by construction. The header states this constraint. - Blocks B and C do not use
::timestamp. Each result goes throughdate_part. DataFusion reads the time zone of the value, and PostgreSQL reads the session time zone. Each block sets the two to the same zone. - Filters, joins,
UNION,CASE,COALESCE,greatest,leastand thedate_binorigins use literals with an explicit offset. So no naive literal enters a comparison, and unwrap_cast_in_comparison drops the timezone shift when unwrapping CAST(timestamp AS timestamptz) = literal #25095 cannot affect them. - No query applies
AT TIME ZONEto an aware value. So fix:AT TIME ZONEon a timezone-aware timestamp returns a naive timestamp #25165 does not change this file.
- Block A projects to
I did not run the file against PostgreSQL 17. CI uses 15, so 15 is the version that matters.
🤖 Generated with Claude Code
Upstream apache#25182 now checks license headers in `.slt` files, and main uses `#` on header lines 8 and 10. This file had empty lines there, so the next CI run would fail the license check. Two comments did not describe their queries: - The winter/summer offset check casts both sides to `timestamp` before it subtracts `INTERVAL '168 days'`. The comment did not say why. The casts are required: without them DataFusion returns true and PostgreSQL returns false, because DataFusion applies calendar units in the value's own zone and PostgreSQL applies them in the session zone. Measured on PostgreSQL 15.19 with TimeZone=UTC. - The MST/MDT check said "the same UTC instant reads as a different local hour". The query shows the opposite: two instants with different UTC hours (19:00Z, 18:00Z) read as the same local hour, 12. Comments and header only. No query or expected output changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Which issue does this PR close?
This PR closes no issue. It adds tests only.
Related to these issues. Each one is a divergence from PostgreSQL, so this PR keeps its queries out of the new file:
datafusion.execution.time_zoneis not used for basic time zone inference #13212Timestamp(_, None)to a named timezone errors on DST boundaries #25084AT TIME ZONE '+05:30'uses the opposite sign convention from PostgreSQL #25170Companion PR: #25175 pins the current DataFusion behaviour, bugs included.
Rationale for this change
Set the session time zone to
America/Denverin both engines. Then this query gives the same answer in DataFusion and in PostgreSQL:plus_1_dayplus_24_hours12131213INTERVAL '1 day'keeps the local wall clock.INTERVAL '24 hours'adds 24 hours of elapsed time, so the local hour moves by one.What this PR guarantees. CI runs each query of the new file on DataFusion and on PostgreSQL 15. If the answers differ, the build fails. So a change cannot move DataFusion away from PostgreSQL on these queries without a red build.
Why time zone bugs recur
mainbefore this PR, the sqllogictest corpus has 40SET datafusion.execution.time_zonestatements, in 9 files.test_files/pg_compat/) has no time zone coverage. No file in it refers to a time zone or totimestamptz.What changes are included in this PR?
This PR adds one test file and changes no production code:
datafusion/sqllogictest/test_files/pg_compat/pg_compat_timestamptz.slt(about 700 lines).Sections
Each block sets the session time zone in both engines:
SET TimeZonefor PostgreSQL, andSET datafusion.execution.time_zonefor DataFusion. Each block builds its table in the same zone.UTC(DataFusion+00:00)timestamptzliterals with and without an offset;AT TIME ZONEon naive values; comparison by instant;DISTINCT,count(DISTINCT),min,max,GROUP BY,ORDER BY; filters with aware literals; a self join;UNION,COALESCE,CASE,greatest,least;date_partandextract;to_timestamp;date_binwith an explicit origin; interval arithmeticAmerica/Denverdate_partreadings;'1 day'against'24 hours'across both DST transitions; the repeated hour at fall back;date_truncat day, month and hour;date_bin; week, month and year arithmetic;min,maxandORDER BYby instantAsia/Kolkata, thenAmerica/PhoenixHow we made the expected output
--completemode against a real PostgreSQL (PG_COMPAT=true PG_URI=... --complete). So the answer of PostgreSQL is the expected output, and DataFusion must match it.Harness constraints
timestamptz:postgres_engine/mod.rscallsunimplemented!for that type. So each result is a naivetimestamp, abigint, abooleanortext.timestamptz, to apply a day interval and to rundate_trunc. DataFusion uses the time zone of the value. The two agree only when both zones are the same. So each block sets them to the same zone.::timestamp. That projection gives the UTC wall clock in both engines only because the session time zone is UTC. Blocks B and C read each value throughdate_part. The self-review found one query that breaks this rule (lines 112-118). A follow-up commit must fix it.What is the testing strategy for this PR?
This PR adds tests only. Checks at head
2b0a37fb91:PG_COMPAT=true PG_URI=... cargo test --profile ci -p datafusion-sqllogictest --features postgres --test sqllogictests -- pg_compatagainstpostgres:15: 7 of 7 files pass.log_statement = 'all'on the server, the PostgreSQL log shows the harness execute each statement of this file, with zero errors. So the run really compares the two engines.PG_COMPAT(DataFusion only): the file passes.*.sltto the license header check. The new file now has#on header lines 8 and 10, asmainexpects (fixed inaafa2e7df4).Field research
PostgreSQL version
postgres:15: see thesqllogictest-postgresjob in.github/workflows/rust.yml.postgres:15, not against a newer local server.extract(epoch ...). The expected output had1719792000, and CI produced1719792000.000000. The file now casts eachextractanddate_partresult tobigint.psqlon PostgreSQL 15.19 and 17.11. Both givenumeric1719792000.000000. Sopsqlshows no version difference for this value, and the cause of the earlier mismatch is not confirmed.Divergences that this file leaves out
We measured each answer. The DataFusion answers come from the pinned expectations in #25175, or from
datafusion-cliat the same base. PostgreSQL answers come frompostgres:15.arrow_typeof('2024-07-01 12:00:00Z'::timestamptz)(PostgreSQL:pg_typeof)UTCTimestamp(ns)(naive)timestamp with time zone'2024-07-01 12:00:00'::timestamp::timestamptz::timestampAmerica/Denver2024-07-01T18:00:002024-07-01 12:00:00ts AT TIME ZONE 'Europe/Brussels', wheretsis the aware value2024-07-01 12:00:00Z+00:00, PostgreSQLUTCTimestamp(ns, "Europe/Brussels")2024-07-01T14:00:00+02:00timestamp without time zone2024-07-01 14:00:00TIMESTAMP '2024-07-01 12:00:00' AT TIME ZONE '+05:30'UTC2024-07-01T12:00:00+05:30(06:30 UTC)2024-07-01 17:30:00+00WHERE ts > '2024-07-01 06:00:00'on rows at 00:00, 06:00, 12:00 and 18:00 UTC; DataFusion column zoneAmerica/DenverUTCWHERE ts = '2024-07-01T06:00:00Z'::timestamptzon the same rowsUTC= '2024-07-01T18:00:00Z'::timestamptz, for the naive row2024-07-01 12:00:00America/Denverdate_trunc('day', ts)for2024-07-01T00:00:00Z; DataFusion column zoneAmerica/DenverUTC2024-06-30T00:00:00-06:002024-07-01 00:00:00+00(TIMESTAMP '2024-01-15 12:00:00' AT TIME ZONE 'America/Denver') = (TIMESTAMP '2024-07-01 12:00:00' AT TIME ZONE 'America/Denver') - INTERVAL '168 days'+00:00, PostgreSQLUTCtruef'2024-11-03 01:30:00'::timestamptz(repeated hour)America/Denvererror computing timezone offset2024-11-03 01:30:00-07'2024-03-10 02:30:00'::timestamptz(hour that does not exist)America/Denvererror computing timezone offset2024-03-10 03:30:00-06date_part('timezone_hour', ts)in JulyAmerica/DenverDate part 'timezone_hour' not supported-6date_binwith a1 monthstrideUTCtimestamps cannot be binned into intervals containing months or yearsfrom_unixtime(1719792000)(PostgreSQL:to_timestamp(1719792000))America/Denver(PostgreSQLUTC)Timestamp(s)2024-07-01T00:00:00(naive)timestamp with time zoneRows 5 to 9 have one cause in common: DataFusion uses the time zone of the value, and PostgreSQL uses the session time zone. That is why each block of this file sets the two to the same zone.
Are there any user-facing changes?
No. This PR adds one test file. It changes no production code, no public API and no behaviour.
🤖 Generated with Claude Code