Skip to content

TML-3224: encode timestamptz parameters as UTC ISO strings - #30093

Open
wmadden-electric wants to merge 1 commit into
mainfrom
fix/timestamptz-param-encode-utc
Open

TML-3224: encode timestamptz parameters as UTC ISO strings#30093
wmadden-electric wants to merge 1 commit into
mainfrom
fix/timestamptz-param-encode-utc

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Writing new Date('0120-01-01T00:00:00Z') to a timestamptz column from a TZ=Europe/Berlin process stored 0120-01-01T00:00:28Z — 28 seconds late. Same code, TZ=UTC: exact. This PR makes the timestamptz codec serialize the instant itself, so what gets stored no longer depends on the writer's timezone.

Linear: TML-3224

The bug

It is on the write side, in how a JavaScript Date becomes a query parameter — not in the session and not in decoding (a direct select under both timezones shows the same session TimeZone, identical wire text, and a correct parse).

pg/timestamptz@1.encode handed the Date straight to the pg driver, whose dateToString writes the local wall clock (getHours/getMinutes/getSeconds) next to an offset suffix built from getTimezoneOffset(), which is whole minutes. For year 120, Europe/Berlin's rule is local mean time, +00:53:28 — an offset with seconds. So the instant was serialized as:

0120-01-01T00:53:28+00:53
          └─ wall clock keeps the 28 s   └─ suffix cannot express them

Postgres reads that literally: 00:53:28 at +00:53 is 00:00:28Z. Any zone whose historical rule carries seconds reproduces it; UTC — and therefore CI — cannot, which is why this only surfaced as a "local-only" red in issues-28192-pg-historical-dates.

The fix

encode now returns value.toISOString() — a Z-suffixed UTC string with no offset to lose — through a new pgTimestamptzEncode helper whose doc comment records the mechanism. This mirrors the codec's existing JSON-lane encoder and the fix pg/date@1 already received for the same dateToString cause. The codec's wire type widens to Date | string; decode is unchanged.

Verification

  • New unit tests pin the exact encoded string for a modern and a historical instant. They are timezone-independent and fail on the old pass-through (run red before the change).
  • @internal/target-postgres: 1412 tests, lint, typecheck, build green. @internal/adapter-postgres and @internal/sql-orm-client tests and typecheck green; integration-tests typecheck green.
  • End to end: the issues-28192-pg-historical-dates integration file run from a TZ=Europe/Berlin shell against the rebuilt package — 5 passed, 4 expected-fail (the pre-existing 2-digit-year date cases), 0 failed. No timezone pinning anywhere.

Deliberately not in this PR

  • The naive pg/timestamp@1 codec's two lanes disagree on which wall clock a Date maps to (JSON lane: UTC components; parameter lane: local, via dateToString). Naive timestamps must not be implicitly forced to UTC; which wall clock they should use is a design decision, tracked in TML-3224's follow-up section.
  • CI has no non-UTC leg, so it cannot catch this class; also noted on the ticket.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • PostgreSQL timestamp-with-time-zone values are now encoded as consistent UTC ISO-8601 strings.
    • Timestamp encoding accepts both Date values and date strings.
    • Decoding continues to return Date values, preserving existing behavior.
  • Tests
    • Added coverage for timezone-independent encoding and historical dates.

…3224)

pg/timestamptz@1 handed the Date to the pg driver, whose dateToString
writes the local wall clock beside a whole-minute offset suffix. Zones
whose historical rule is local mean time carry a seconds component, so
the stored instant drifted by that remainder (Europe/Berlin, year 120:
+28s). Serializing the instant ourselves as a Z-suffixed ISO string has
no offset to lose, mirroring the JSON lane and the pg/date@1 fix.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric requested a review from a team as a code owner August 21, 2026 09:00
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 517b73bf-33a8-47c7-9b72-10e98f878e88

📥 Commits

Reviewing files that changed from the base of the PR and between f63e152 and c928be7.

📒 Files selected for processing (4)
  • packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts
  • packages/3-targets/3-targets/postgres/src/core/codecs.ts
  • packages/3-targets/3-targets/postgres/test/codecs-class.test.ts
  • packages/3-targets/3-targets/postgres/test/codecs.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The PostgreSQL timestamptz codec now encodes Date values as UTC ISO-8601 strings. Its JSON helper reuses the same encoder. Decoding continues to return Date values.

Changes

Timestamptz encoding

Layer / File(s) Summary
Encoding contract and codec integration
packages/3-targets/3-targets/postgres/src/core/codec-helpers.ts, packages/3-targets/3-targets/postgres/src/core/codecs.ts
Added pgTimestamptzEncode. The codec now accepts `Date
Encoding and decoding validation
packages/3-targets/3-targets/postgres/test/codecs.test.ts, packages/3-targets/3-targets/postgres/test/codecs-class.test.ts
Tests verify UTC ISO output for current and historical dates. Decode tests continue to verify Date results.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to c928b

This PR makes timestamptz parameters serialize as UTC ISO strings, preventing writer-timezone-dependent historical timestamp errors. No actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: tensordreams

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: encoding PostgreSQL timestamptz parameters as UTC ISO strings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 4 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/timestamptz-param-encode-utc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
postgres / no-emit 173.29 KB (+0.01% 🔺)
postgres / emit 150.5 KB (+0.02% 🔺)
mongo / no-emit 101.15 KB (0%)
mongo / emit 91 KB (0%)
cf-worker / no-emit 197.36 KB (+0.01% 🔺)
cf-worker / emit 172.05 KB (+0.03% 🔺)

@pkg-pr-new

pkg-pr-new Bot commented Aug 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma/orm-extension-arktype-json

npm i https://pkg.pr.new/@prisma/orm-extension-arktype-json@30093

@prisma/orm-extension-middleware-cache

npm i https://pkg.pr.new/@prisma/orm-extension-middleware-cache@30093

@prisma/orm-extension-paradedb

npm i https://pkg.pr.new/@prisma/orm-extension-paradedb@30093

@prisma/orm-extension-pgvector

npm i https://pkg.pr.new/@prisma/orm-extension-pgvector@30093

@prisma/orm-extension-postgis

npm i https://pkg.pr.new/@prisma/orm-extension-postgis@30093

@prisma/orm-extension-supabase

npm i https://pkg.pr.new/@prisma/orm-extension-supabase@30093

@prisma/orm-family-mongo

npm i https://pkg.pr.new/@prisma/orm-family-mongo@30093

@prisma/orm-family-sql

npm i https://pkg.pr.new/@prisma/orm-family-sql@30093

@prisma/orm-framework

npm i https://pkg.pr.new/@prisma/orm-framework@30093

@prisma/orm-mongo

npm i https://pkg.pr.new/@prisma/orm-mongo@30093

@prisma/orm-postgres

npm i https://pkg.pr.new/@prisma/orm-postgres@30093

@prisma/orm-sqlite

npm i https://pkg.pr.new/@prisma/orm-sqlite@30093

@prisma/orm-target-mongo

npm i https://pkg.pr.new/@prisma/orm-target-mongo@30093

@prisma/orm-target-postgres

npm i https://pkg.pr.new/@prisma/orm-target-postgres@30093

@prisma/orm-target-sqlite

npm i https://pkg.pr.new/@prisma/orm-target-sqlite@30093

@prisma/orm-toolchain

npm i https://pkg.pr.new/@prisma/orm-toolchain@30093

commit: c928be7

async encode(value: Date, _ctx: CodecCallContext): Promise<string> {
return pgTimestamptzEncode(value);
}
async decode(wire: Date, _ctx: CodecCallContext): Promise<Date> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically unsound if typescript accepts this, our types say that TWire is now Date | string without distinguishing between the directions in which the value travels. The first argument of this method is supposed to accept TWire. So in principle this should now be

Suggested change
async decode(wire: Date, _ctx: CodecCallContext): Promise<Date> {
async decode(wire: Date | string, _ctx: CodecCallContext): Promise<Date> {

with the corresponding branch for handling string values, based on the types.

Whether this is possible at run time is a separate issue. I'd prefer the implementation to match the types, otherwise this can blow up later when composing the abstractions in a different way, even if this branch is not going to be reachable at run time right now. The codec might be used for something other than decoding the values that come from the pg driver, and having it declare that it can parse string values but not actually parse them is not ideal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants