Skip to content

feat(postgres)!: return an interval as an iso 8601 duration - #93

Merged
colinhacks merged 3 commits into
mainfrom
interval-iso8601
Oct 23, 2026
Merged

colinhacks merged 3 commits into
mainfrom
interval-iso8601

Conversation

@colinhacks

Copy link
Copy Markdown
Owner

A Postgres interval came back as Postgres's own text (1 day 02:03:04), which nothing in JavaScript parses. It now comes back as an ISO 8601 duration with its sign in front, which Temporal.Duration.from reads:

Interval Before Now
1 day 02:03:04 "1 day 02:03:04" "P1DT2H3M4S"
1 year 2 mons "1 year 2 mons" "P1Y2M"
0 "00:00:00" "PT0S"
-1 day "-1 days" "-P1D"
1 mon -1 day "1 mon -1 days" "P1M-1D"
infinity "infinity" "infinity"

Breaking: code that reads an interval's text gets the new form. The TypeScript type stays string.

The value is what Postgres itself writes in IntervalStyle = iso_8601, with one change. Postgres signs each part (P-1D), which ISO 8601 and Temporal refuse, so an interval whose parts are all negative takes a single sign in front (-P1D). An interval whose months, days and time differ in sign has no ISO 8601 form, so it keeps Postgres's per-part signs (P1M-1D). Postgres reads that form back; Temporal refuses it.

Values still round-trip: Postgres refuses a sign before the P, so an interval parameter's -P… is rewritten to the per-part form before binding, in a plain parameter, an array element and a structured parameter's member. Postgres's own text is still accepted as a parameter.

  • src/values.ts: the interval kind, duration (Postgres's text to ISO 8601) and postgresInterval (a parameter's -P… to the form Postgres reads).
  • src/decode.ts: oid 1186 is an interval, and Bun.sql's interval text is converted too.
  • src/execute.ts, src/rows.ts: parameters.
  • src/schema.ts: the output schema describes the form. It doesn't claim JSON Schema's duration format, because RFC 3339's duration has no sign and no fraction.
  • The Postgres docs page, the tests, and the driver matrix's expected typed row.

Checked against Postgres 18 on 22 intervals, including negatives, mixed signs, fractions, hours past 24 and both extremes. Each value equals Postgres's own ISO 8601 output with the sign moved to the front, binds back to the identical interval part for part, and, where its parts share a sign, is read by Node 26's Temporal.Duration.from. The loader tests (none skipped) and every driver matrix job (4,524 rows) pass.

@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 23, 2026 10:14pm UTC

Request Review

Copilot AI balanced review requested due to automatic review settings October 23, 2026 21:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — two rough edges inline, both about where Postgres writes an interval in a form other than its default.

Reviewed changes

Reviewed the single commit that switches a Postgres interval's run-time representation from Postgres's text to an ISO 8601 duration, and the parameter rewrite that makes it round-trip.

  • interval kind: src/values.ts adds the kind, duration (Postgres's text to ISO 8601, with a leading sign when every part is negative and per-part signs when they differ) and postgresInterval (-P… to the per-part form Postgres reads).
  • Decoding: src/decode.ts maps oid 1186 to interval and sends Bun.sql's interval text through coerce.
  • Parameters: src/execute.ts (plain and array) and src/rows.ts (structured member) rewrite a leading-sign duration before binding.
  • Output schema: src/schema.ts describes the form without claiming JSON Schema's duration format.
  • Docs, tests, driver matrix: the Postgres page, the value/decode/execute/representation tests, and the matrix's expected typed row follow the new form.

I traced duration against Postgres's postgres-style output (the + after a negative part, hours past 24, fractions with no whole seconds, both extremes) and postgresInterval against Temporal's output (fractions, W). Both handle these correctly.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread src/schema.ts Resolved
Comment thread site/content/docs/postgres.mdx Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. Both earlier findings are fixed.

Reviewed changes

Reviewed the two commits pushed since the last review. Both fix what that review raised.

  • Interval inside JSON: in src/schema.ts, the inJson switch in scalar() now describes an interval inside a document as pg type interval, which is Postgres's own text, so it no longer gets the ISO 8601 description. A test in tests/values.test.ts pins this.
  • Docs caveats: the Postgres page now says the ISO 8601 form holds only in the default IntervalStyle. Its "Values inside a document" list also gains the interval, with a jsonb_build_object pair.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

An interval was Postgres's own text, which nothing in JavaScript
parses. It is now an ISO 8601 duration with its sign in front, which
Temporal.Duration.from reads; an interval whose parts differ in sign
keeps Postgres's per-part signs. A parameter takes either form.
@colinhacks
colinhacks merged commit 04b5142 into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the interval-iso8601 branch October 23, 2026 22:15
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.

2 participants