Skip to content

test: PostgreSQL differential coverage for timestamps with time zone - #25164

Open
adriangb wants to merge 2 commits into
apache:mainfrom
pydantic:test-timezone-characterization-suite
Open

test: PostgreSQL differential coverage for timestamps with time zone#25164
adriangb wants to merge 2 commits into
apache:mainfrom
pydantic:test-timezone-characterization-suite

Conversation

@adriangb

@adriangb adriangb commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

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:

Companion PR: #25175 pins the current DataFusion behaviour, bugs included.

Rationale for this change

Set the session time zone to America/Denver in both engines. Then this query gives the same answer in DataFusion and in PostgreSQL:

-- ts is 2024-03-09 12:00 MST, one day before the spring-forward transition
SELECT date_part('hour', ts + INTERVAL '1 day')::bigint    AS plus_1_day,
       date_part('hour', ts + INTERVAL '24 hours')::bigint AS plus_24_hours
FROM tstz_den WHERE id = 1;
Engine plus_1_day plus_24_hours
DataFusion 12 13
PostgreSQL 15 12 13

INTERVAL '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

  • Time zone rules are hard to check by reading code. Two engines can each look correct and still give different answers.
  • On main before this PR, the sqllogictest corpus has 40 SET datafusion.execution.time_zone statements, in 9 files.
  • The PostgreSQL differential harness (test_files/pg_compat/) has no time zone coverage. No file in it refers to a time zone or to timestamptz.
  • So no test compares the time zone behaviour of DataFusion with a reference engine. A change can alter an answer, and no test fails.

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 TimeZone for PostgreSQL, and SET datafusion.execution.time_zone for DataFusion. Each block builds its table in the same zone.

Block Session time zone Coverage
A UTC (DataFusion +00:00) timestamptz literals with and without an offset; AT TIME ZONE on 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_part and extract; to_timestamp; date_bin with an explicit origin; interval arithmetic
B America/Denver local date_part readings; '1 day' against '24 hours' across both DST transitions; the repeated hour at fall back; date_trunc at day, month and hour; date_bin; week, month and year arithmetic; min, max and ORDER BY by instant
C Asia/Kolkata, then America/Phoenix a half-hour offset, and a zone with no DST

How we made the expected output

  1. We ran the sqllogictest --complete mode against a real PostgreSQL (PG_COMPAT=true PG_URI=... --complete). So the answer of PostgreSQL is the expected output, and DataFusion must match it.
  2. We read each result.
  3. We removed each query where the two engines disagree. The list is in "Field research" below.

Harness constraints

  1. No aware value in a result. The PostgreSQL runner cannot render timestamptz: postgres_engine/mod.rs calls unimplemented! for that type. So each result is a naive timestamp, a bigint, a boolean or text.
  2. Session zone against value zone. PostgreSQL uses the session time zone to read a naive literal, to render a timestamptz, to apply a day interval and to run date_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.
  3. A naive projection must not hide a divergence. Block A projects with ::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 through date_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_compat against postgres:15: 7 of 7 files pass.
  • With 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.
  • The same command without PG_COMPAT (DataFusion only): the file passes.
  • CI on 2026-09-10: all 38 checks pass, the "Run sqllogictest with Postgres runner" job included.
  • On 2026-09-11, chore: check license headers in SQL logic tests #25182 added *.slt to the license header check. The new file now has # on header lines 8 and 10, as main expects (fixed in aafa2e7df4).

Field research

PostgreSQL version

  • CI runs the harness against postgres:15: see the sqllogictest-postgres job in .github/workflows/rust.yml.
  • The harness compares text. If a value renders differently in another PostgreSQL version, CI fails, even when the instant is correct. So generate and check the expected output against postgres:15, not against a newer local server.
  • An earlier revision of this PR failed in CI on extract(epoch ...). The expected output had 1719792000, and CI produced 1719792000.000000. The file now casts each extract and date_part result to bigint.
  • We measured that value in psql on PostgreSQL 15.19 and 17.11. Both give numeric 1719792000.000000. So psql shows 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-cli at the same base. PostgreSQL answers come from postgres:15.

# Query Session time zone DataFusion PostgreSQL 15 Issue
1 arrow_typeof('2024-07-01 12:00:00Z'::timestamptz) (PostgreSQL: pg_typeof) DataFusion unset, PostgreSQL UTC Timestamp(ns) (naive) timestamp with time zone #25166
2 '2024-07-01 12:00:00'::timestamp::timestamptz::timestamp America/Denver 2024-07-01T18:00:00 2024-07-01 12:00:00 #12218
3 ts AT TIME ZONE 'Europe/Brussels', where ts is the aware value 2024-07-01 12:00:00Z DataFusion +00:00, PostgreSQL UTC Timestamp(ns, "Europe/Brussels") 2024-07-01T14:00:00+02:00 timestamp without time zone 2024-07-01 14:00:00 #12218
4 TIMESTAMP '2024-07-01 12:00:00' AT TIME ZONE '+05:30' DataFusion unset, PostgreSQL UTC 2024-07-01T12:00:00+05:30 (06:30 UTC) 2024-07-01 17:30:00+00 #25170
5 WHERE ts > '2024-07-01 06:00:00' on rows at 00:00, 06:00, 12:00 and 18:00 UTC; DataFusion column zone America/Denver DataFusion unset, PostgreSQL UTC 1 row (18:00 UTC) 2 rows (12:00 and 18:00 UTC) closest: #13212
6 WHERE ts = '2024-07-01T06:00:00Z'::timestamptz on the same rows DataFusion unset, PostgreSQL UTC the 12:00 UTC row the 06:00 UTC row #25166
7 naive column = '2024-07-01T18:00:00Z'::timestamptz, for the naive row 2024-07-01 12:00:00 America/Denver 0 rows 1 row #25095
8 date_trunc('day', ts) for 2024-07-01T00:00:00Z; DataFusion column zone America/Denver DataFusion unset, PostgreSQL UTC 2024-06-30T00:00:00-06:00 2024-07-01 00:00:00+00 no issue
9 (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' DataFusion +00:00, PostgreSQL UTC true f no issue
10 '2024-11-03 01:30:00'::timestamptz (repeated hour) America/Denver error: error computing timezone offset 2024-11-03 01:30:00-07 #25084
11 '2024-03-10 02:30:00'::timestamptz (hour that does not exist) America/Denver error: error computing timezone offset 2024-03-10 03:30:00-06 #25084
12 date_part('timezone_hour', ts) in July America/Denver error: Date part 'timezone_hour' not supported -6 fix in #25163
13 date_bin with a 1 month stride DataFusion unset, PostgreSQL UTC returns a bin error: timestamps cannot be binned into intervals containing months or years no issue
14 from_unixtime(1719792000) (PostgreSQL: to_timestamp(1719792000)) America/Denver (PostgreSQL UTC) Timestamp(s) 2024-07-01T00:00:00 (naive) timestamp with time zone #12892

Rows 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

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>
@adriangb
adriangb force-pushed the test-timezone-characterization-suite branch from 2c3caeb to 2b0a37f Compare September 10, 2026 19:34
@adriangb adriangb changed the title test: characterization and PostgreSQL differential coverage for timestamps with time zone test: PostgreSQL differential coverage for timestamps with time zone Sep 10, 2026
@codecov-commenter

codecov-commenter commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.91%. Comparing base (da89c7c) to head (aafa2e7).
⚠️ Report is 155 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@adriangb

adriangb commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Self-review (QA pass) of our own PR, at head 2b0a37fb91. I tried to break the main claim of this file: "each query is a real agreement between DataFusion and PostgreSQL".

Result. The file passes against postgres:15, the image that CI uses. I found two problems. The license header fails a check that main added today. One query uses a naive projection that hides a real divergence. I found no other serious problem.

Findings (most important first)

1. The license header fails the new .slt check on main

Fix: put # on lines 8 and 10 of the header.

2. Lines 112-118: a ::timestamp cast hides a real divergence

SELECT (TIMESTAMP '2024-01-15 12:00:00' AT TIME ZONE 'America/Denver')::timestamp
     = (TIMESTAMP '2024-07-01 12:00:00' AT TIME ZONE 'America/Denver')::timestamp - INTERVAL '168 days'
  • The query returns a boolean. So the harness does not need a naive projection here.
  • The two casts move - INTERVAL '168 days' onto naive values. A naive value has no DST, so the query cannot see a DST difference.
  • Without the casts, the engines disagree in Block A:
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_compat suite with PG_COMPAT=true against a fresh postgres:15 container. 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 statements s3 to s91, from the first SET 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 as 1719792000.000000 and date_part('epoch', ...) renders as 1719792000. Each query in the file casts both to bigint, so the result text does not depend on the numeric scale.
  • The harness constraint in the header is correct. postgres_engine/mod.rs renders Type::TIMESTAMP and calls unimplemented! for other types, timestamptz included.
  • Audit of the other queries for a hidden divergence.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants