Skip to content

sql(postgres): decode 'infinity'::date/timestamp to the Number ±Infinity - #35121

Merged
Jarred-Sumner merged 4 commits into
mainfrom
farm/d27d4283/postgres-infinity-date
Jul 22, 2026
Merged

Jarred-Sumner merged 4 commits into
mainfrom
farm/d27d4283/postgres-infinity-date

Conversation

@robobun

@robobun robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Postgres represents unbounded dates and timestamps as the special values 'infinity' / '-infinity'. On every decode path Bun returned them as Invalid Date (a Date whose getTime() is NaN):

await sql`select 'infinity'::date as a, '-infinity'::date as b`
// [{ a: Invalid Date, b: Invalid Date }]   sign lost, indistinguishable from a parse failure

That is data loss: +infinity, -infinity, and a genuinely unparseable value all collapse to the same NaN, so user code cannot tell whether a row means "no upper bound" or "no lower bound".

Cause

Three independent paths, all ending at the same NaN:

  • scalar text (date always, timestamp/timestamptz on .simple()): routed through JS Date.parse('infinity') → NaN.
  • scalar binary (timestamp/timestamptz on the extended protocol): Postgres sends PG_INT64_MAX / PG_INT64_MIN (DT_NOEND / DT_NOBEGIN). from_binary does i64::MAX as f64 / 1000 + 946684800000 ≈ 9.224e15 ms, past JS Date's ±8.64e15 range, and DateInstance::create's timeClip maps it to NaN.
  • array text ({infinity,-infinity}::timestamp[]): the parser already produced SQLDataCell::date(f64::INFINITY), but SQLClient.cpp hands that to DateInstance::create, whose timeClip maps every non-finite input to NaN. So the existing code that tried to preserve ±Infinity never took effect.

Fix

Return the JS Number ±Infinity for these values, matching node-postgres (pg-types via postgres-date, for oids 1082/1114/1184). postgres.js does not special-case this and returns Invalid Date, but that loses information; node-postgres' behaviour is the one that round-trips.

  • types/date.rs from_binary: recognise i64::MAX / i64::MIN and return ±f64::INFINITY.
  • types/date.rs parse_infinity: shared helper for the infinity / -infinity text spellings.
  • types/date.rs from_js: map ±Infinity back to i64::MAX / i64::MIN so the encode/decode pair is symmetric and the (ms - epoch) * 1000 arithmetic never overflows on the value the decoder now produces.
  • DataCell.rs: check parse_infinity before Date.parse on the scalar text path and the unquoted-date[] array path (the timestamp[]/timestamptz[] array path already produced date(±INFINITY)).
  • SQLClient.cpp toJS: when the Date cell carries ±Infinity, return jsDoubleNumber(±Infinity) instead of a timeClip'd DateInstance.

Finite dates are unchanged and remain Date instances. MySQL shares the toJS path but never produces ±Infinity there (zero DATETIME is NaN, which std::isinf is false for).

This is a behaviour change (the result type for these two values changes from Date to number), but the previous value was unusable: .toISOString() throws on it, .getTime() is NaN, and the sign is gone.

Verification

test/js/sql/postgres-infinity-date.test.ts drives a scripted v3 backend and covers every path:

  • scalar text: date / timestamp / timestamptz
  • scalar binary: timestamp / timestamptz (PG_INT64_MAX / PG_INT64_MIN on the wire)
  • array text: date[] / timestamp[] / timestamptz[]
  • bind: ±Infinity bound to a timestamp / timestamptz parameter writes PG_INT64_MAX / PG_INT64_MIN in the Bind body

Each case asserts ±Infinity for the special values and a Date instance for an adjacent finite value. All ten fail on main; all pass here. The three sql.test.ts array tests and test/regression/issue/21311.test.ts that pinned the old Invalid Date behaviour are updated. sql-postgres-datetime-roundtrip.test.ts, postgres-datarow-overrun.test.ts, postgres-binary-float-nan-box.test.ts, and wire-frames.test.ts are unchanged and pass.

Follows on from the note at the bottom of #35112.

Postgres represents unbounded dates/timestamps as the special values
'infinity' / '-infinity'. On every decode path (scalar text, scalar
binary, array text) these came back as `new Date(NaN)`:

- scalar text went through JS `Date.parse('infinity')` → NaN
- scalar binary put `i64::MAX / 1000 + epoch` (~9.224e15 ms) through
  DateInstance::create, whose timeClip maps anything past ±8.64e15 to NaN
- array text already built `SQLDataCell::date(f64::INFINITY)`, but
  DateInstance::create's timeClip flattened that to NaN too

An Invalid Date cannot distinguish +infinity from -infinity from a
parse failure, so the value is unrepresentable in user code.
node-postgres (pg-types via postgres-date) returns the Number
±Infinity here; match that.

- types/date.rs from_binary: recognise DT_NOEND/DT_NOBEGIN (i64::MAX/MIN)
  and return ±f64::INFINITY
- types/date.rs parse_infinity: shared helper for the text spellings
- DataCell.rs: check parse_infinity before Date.parse on the scalar
  text path and the unquoted-date[] array path
- SQLClient.cpp toJS: when the Date cell carries ±Infinity, return
  jsDoubleNumber(±Infinity) instead of a timeClip'd DateInstance
@robobun

robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:35 AM PT - Jul 22nd, 2026

❌ @autofix-ci[bot], your commit 0d47616 has 3 failures in Build #77760 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35121

That installs a local version of the PR into your bun-35121 executable, so you can run:

bun-35121 --bun

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c5fd92c2-4db2-4b2a-8775-da375e2c2040

📥 Commits

Reviewing files that changed from the base of the PR and between 47597ab and 0d47616.

📒 Files selected for processing (6)
  • src/jsc/bindings/SQLClient.cpp
  • src/sql_jsc/postgres/DataCell.rs
  • src/sql_jsc/postgres/types/date.rs
  • test/js/sql/postgres-infinity-date.test.ts
  • test/js/sql/sql.test.ts
  • test/regression/issue/21311.test.ts

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

Comment thread src/sql_jsc/postgres/types/date.rs
robobun and others added 2 commits July 22, 2026 11:46
…nfinity-date tests

from_js is the sibling encoder of from_binary: now that the decoder
produces ±Infinity, map it back to i64::MAX/MIN so a
ParameterDescription that reports timestamp/timestamptz does not push
it through the (ms - epoch) * 1000 arithmetic (which overflows i64).

Also updates test/regression/issue/21311.test.ts, which inserted
'infinity'::date and asserted the old Invalid Date decode.

@claude claude 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.

The earlier from_js overflow is fixed and I found nothing new, but this is a user-visible behaviour change (infinity dates go from Invalid Date to the Number ±Infinity, and the PR picks node-postgres' semantics over postgres.js'), so a maintainer should sign off on the API choice.

What was reviewed:

  • from_js now short-circuits ±Infinity to i64::MAX/MIN before the (ms - epoch) * 1000 arithmetic, and the new bind test asserts the exact wire bytes.
  • The shared SQLClient.cpp toJS Date arm is also used by MySQL — checked that MySQL's date cell only ever carries finite values or NaN, so std::isinf never fires there.
  • Traced the date[] / timestamp[] / timestamptz[] unquoted-element paths in parse_array; each reaches a SQLDataCell::date(±INFINITY) for both signs.
Extended reasoning...

Overview

The PR changes how Bun's Postgres client decodes 'infinity' / '-infinity' for the date, timestamp, and timestamptz types (and their array forms). Previously all three decode paths — scalar text, scalar binary, and array text — collapsed to Invalid Date (a Date with NaN time), losing the sign. Now they return the JS Number ±Infinity. It touches src/sql_jsc/postgres/types/date.rs (from_binary, new parse_infinity, and from_js), two call sites in src/sql_jsc/postgres/DataCell.rs, one branch in src/jsc/bindings/SQLClient.cpp toJS, a new hermetic wire-protocol test file, and updates three existing tests that pinned the old Invalid Date behaviour.

My previous review flagged that the sibling encoder from_js would overflow i64 when round-tripping the newly-produced ±Infinity; f1f2afb addressed that by mapping ±Infinity → i64::MAX/MIN before the saturating cast, and added a bind-path test asserting the exact PG_INT64_MAX/MIN bytes in the Bind body. That thread is resolved.

Security risks

None identified. Input is server-provided Postgres wire data; the new checks are exact-match comparisons against i64::MAX/MIN and case-insensitive string equality against "infinity"/"-infinity", added before the existing parsing (no new allocation or arithmetic on untrusted lengths). The C++ change only branches on std::isinf of an already-decoded double.

Level of scrutiny

Medium-high. The code changes themselves are small, well-localised, and thoroughly tested with a scripted v3 backend covering every decode path plus the encode round-trip. However, this is a deliberate user-facing behaviour change: the result type for these two values changes from Date to number. The PR chooses node-postgres' semantics (which round-trip) over postgres.js' (which loses the sign), and while that reasoning is sound, picking which reference implementation to match is exactly the kind of API decision REVIEW.md's "API design" section says needs maintainer agreement. SQLClient.cpp's toJS is also shared with MySQL; I checked that MySQL never produces an infinite date cell (its zero-DATETIME path yields NaN, for which std::isinf is false), so no cross-driver leakage, but that's another reason a human should confirm.

Other factors

  • The bug-hunting system found nothing on this revision.
  • I traced the three array-text entry points in parse_array: date_array takes the text-array arm (now guarded by parse_infinity), while unquoted infinity/-infinity in timestamp_array/timestamptz_array reach the pre-existing b'I'|b'i' and b'-' number-parse arms that already emit SQLDataCell::date(±INFINITY) — the C++ std::isinf branch is what makes those finally observable.
  • The updated sql.test.ts and 21311.test.ts assertions correctly track the new behaviour without weakening what they protected (element identity and sign are now asserted more strongly, not less).
  • from_js's new == f64::INFINITY / == f64::NEG_INFINITY comparisons are IEEE-754-correct and leave NaN on its existing path, as the author noted.

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 0d47616: the SQL tests this PR touches (postgres-infinity-date.test.ts, sql.test.ts special-date arrays, 21311.test.ts) all pass on every lane. The remaining failures are unrelated: test-net-connect-memleak.js is red on main build 77601 at the same base commit, and the install/bundler/webview failures are pre-existing flakes that cleared on retry. Ready for review.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants