Skip to content

sql(postgres): hex-encode Uint8Array/DataView elements in sql.array; stop TypedArray.map coercion - #36241

Open
robobun wants to merge 12 commits into
mainfrom
farm/815597fb/sql-array-typedarray-bytea
Open

robobun wants to merge 12 commits into
mainfrom
farm/815597fb/sql-array-typedarray-bytea

Conversation

@robobun

@robobun robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator

What

sql.array(values, type) silently zeroed any non-Buffer ArrayBufferView element.

await sql`select ${sql.array([new Uint8Array([1, 2, 44])], "bytea")}`;
// Bind parameter on the wire: {{0,0,0}}
// same bytes via Buffer.from: {"\x01022c"}

No error; the row is written with the wrong data. Float32Array elements became {{NaN,NaN}} and BigInt64Array threw Failed to parse String to BigInt.

Why

arrayValueSerializer treated every non-Buffer ArrayBuffer.isView as a nested array dimension and recursed with value.map(...). TypedArray.prototype.map returns the same typed-array species, so each per-element string serialization was coerced back into the element type: '"1"' -> NaN -> 0 for Uint8Array, NaN for Float32Array. A DataView element has no .length, so it became {}. serializeArray had the same .map coercion for a top-level typed array.

Fix

arrayValueSerializer now checks ArrayBuffer.isView first. Any view element (Buffer, Uint8Array, DataView, Float32Array, ...) is hex-encoded in a BYTEA or JSON/JSONB array and rejected with ERR_INVALID_ARG_VALUE for every other element type, so the silent corruption becomes a clear error and Uint8Array matches Buffer byte for byte in bytea[]. The typed-array nested-dimension arm is gone from the element dispatch. serializeArray builds per-element strings into a plain Array via Array.from for a top-level typed array so nothing is coerced.

Tests

test/js/sql/postgres-array-typedarray-serialize.test.ts scripts a v3 backend that records the Bind parameter and asserts the exact literal for the BYTEA/JSON cases and the rejection for everything else (fails on main, passes here). A BYTEA round-trip with Uint8Array and Buffer elements is added to the container-backed sql.array suite in test/js/sql/sql.test.ts.


[review] gate passed · iteration 6 · 3 files touched

fails on main (without fix)
ASAN without fix: 6 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/sql/postgres-array-typedarray-serialize.test.ts test/js/sql/sql.test.ts
bun test v1.4.0 (c11121bdd)

test/js/sql/sql.test.ts:
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
(pass) text-format json[] with a malformed boolean literal returns an error instead of looping [2302.31ms]
(pass) rejects Postgres connection options containing null bytes [91.58ms]
(pass) shared createInstance validation (no server) > rejects username containing null bytes [144.00ms]
(pass) shared createInstance validation (no server) > rejects password containing null bytes [9.88ms]
(pass) shared createInstance validation (no server) > rejects database containing null bytes [6.63ms]
(pass) shared createInstance validation (no server) > SSL_CTX creation failure throws the structured BoringSSL error [18.05ms]
(pass) shared createInstance validation (no server) > postgres: rejects tls that is neither a boolean nor 
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (d876c5669)

test/js/sql/sql.test.ts:
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
(pass) text-format json[] with a malformed boolean literal returns an error instead of looping [70.20ms]
(pass) rejects Postgres connection options containing null bytes [3.59ms]
(pass) shared createInstance validation (no server) > rejects username containing null bytes [3.58ms]
(pass) shared createInstance validation (no server) > rejects password containing null bytes [0.24ms]
(pass) shared createInstance validation (no server) > rejects database containing null bytes [0.12ms]
(pass) shared createInstance validation (no server) > SSL_CTX creation failure throws the structured BoringSSL error [3.58ms]
(pass) shared createInstance validation (no server) > postgres: rejects tls that is neither a boolean nor an object [0.40ms]
(pass) shared createInstance validation (no server) > mysql: rejects tls that is neither a boolean nor an object [0.41ms]
(pass) shared createInstance validation (no server) > rejects simple 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/sql/postgres-array-typedarray-serialize.test.ts test/js/sql/sql.test.ts
bun test v1.4.0 (c11121bdd)

test/js/sql/sql.test.ts:
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
(pass) text-format json[] with a malformed boolean literal returns an error instead of looping [2419.81ms]
(pass) rejects Postgres connection options containing null bytes [101.06ms]
(pass) shared createInstance validation (no server) > rejects username containing null bytes [153.93ms]
(pass) shared createInstance validation (no server) > rejects password containing null bytes [10.93ms]
(pass) shared createInstance validation (no server) > rejects database containing null bytes [7.38ms]
(pass) shared createInstance validation (no server) > SSL_CTX creation failure throws the structured BoringSSL error [20.05ms]
(pass) shared createInstance validation (no server) > postgres: rejects tls that is neither a boolean no
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 827ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/21] gen JS modules (bundle-modules)
Preprocess modules (11539ms)
Bundle modules (42ms)
Postprocesss modules (263ms)
Bundle Functions (932ms)
Generate Code (37ms)

[12.84s] Bundled "src/js" for production
  2570 kb
  193 internal modules
  13 native modules
  90 internal functions across 19 files
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[
... (truncated)
diff hotspot
src/js/internal/sql/postgres.ts                    |  46 +++---
 .../postgres-array-typedarray-serialize.test.ts    | 171 +++++++++++++++++++++
 test/js/sql/sql.test.ts                            |   9 ++
 3 files changed, 207 insertions(+), 19 deletions(-)

gate history · 5 passed · 0 rejected · iteration 6

evidence per changed file
file                                                     reads  edits  tests
src/js/internal/sql/postgres.ts                             11     14      0
test/js/sql/postgres-array-typedarray-serialize.test.ts      6     10      0
test/js/sql/sql.test.ts                                      1      1      0

@robobun

robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: diff is green; ready for review.

Fail-before ({{0,0,0}} / {{}} / {{NaN,NaN}}) reproduced with the scripted-backend test on both the 1.4.0 canary and a debug build of current main; all six cases pass on this branch. The docker round-trip in sql.test.ts caught a second, pre-existing under-escaping in the BYTEA hex path ({"\x..."} was reaching bytea_in as x...); the \x prefix now routes through arrayEscape. Bare ArrayBuffer / SharedArrayBuffer elements get the same hex-or-reject treatment per review.

CI: #84215 and #84406 both green on every docker-backed lane that exercises sql.test.ts (debian x64/asan, ubuntu, alpine). No sql/postgres failures anywhere; every test-level annotation is [flaky] and passed on retry. The only job-level red across both builds is CI infra (darwin queue backlog, a freebsd-aarch64 build-agent timeout); none touch this diff. #84335 was a fleet-wide queue-expiration cascade that never reached the test stage.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

PostgreSQL array serialization now handles ArrayBuffer views as binary values where supported, preserves typed-array element representations, rejects unsupported binary element types, and adds Bind-payload and BYTEA round-trip tests.

Changes

PostgreSQL array serialization

Layer / File(s) Summary
Byte-view array serialization
src/js/internal/sql/postgres.ts
ArrayBuffer values and views serialize as BYTEA or JSON hex payloads; unsupported binary element types now raise ERR_INVALID_ARG_VALUE.
Array input-shape serialization
src/js/internal/sql/postgres.ts
Non-Buffer ArrayBuffer views are accepted as array inputs and converted with Array.from to preserve element representations.
Serialization regression coverage
test/js/sql/postgres-array-typedarray-serialize.test.ts, test/js/sql/sql.test.ts
Tests capture Bind payloads for binary views, validate DataView bounds and rejected types, preserve numeric typed-array strings, and verify BYTEA round trips.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly matches the main change: fixing Postgres sql.array serialization for typed-array and DataView inputs.
Description check ✅ Passed The description covers what changed and how it was verified, though it uses different headings than the template.

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

Comment thread src/js/internal/sql/postgres.ts Outdated
Comment thread src/js/internal/sql/postgres.ts Outdated

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/internal/sql/postgres.ts`:
- Around line 241-252: Update the top-level array handling around $isArray and
arrayValueSerializer to explicitly detect DataView inputs before the
values.length check. Either serialize the DataView as one binary element using
its exact byte offset and length, or reject it with a catchable error; do not
allow it to fall through to "{}". Add a regression case covering a DataView with
a non-zero offset and verify its bytes are preserved or the documented rejection
occurs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5d629a35-7252-4ba5-b290-bdfd5cddb0e6

📥 Commits

Reviewing files that changed from the base of the PR and between 789be97 and 209e6b8.

📒 Files selected for processing (3)
  • src/js/internal/sql/postgres.ts
  • test/js/sql/postgres-array-typedarray-serialize.test.ts
  • test/js/sql/sql.test.ts

Comment thread src/js/internal/sql/postgres.ts

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/internal/sql/postgres.ts (1)

17-18: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use realm-independent byte-view detection. instanceof is realm-sensitive here, so cross-realm Uint8Array/Uint8ClampedArray/DataView values are misclassified: typed arrays serialize as nested numbers, and DataView can even hit the empty-array fast path. Switch to a brand/prototype-independent check and add cross-realm coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/internal/sql/postgres.ts` around lines 17 - 18, Update isByteView to
detect Uint8Array, Uint8ClampedArray, and DataView values without instanceof,
using realm-independent brand or prototype-independent checks so cross-realm
values are classified correctly. Preserve the existing supported-type scope and
add coverage for cross-realm typed arrays and DataView serialization, including
the empty-array path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/js/internal/sql/postgres.ts`:
- Around line 17-18: Update isByteView to detect Uint8Array, Uint8ClampedArray,
and DataView values without instanceof, using realm-independent brand or
prototype-independent checks so cross-realm values are classified correctly.
Preserve the existing supported-type scope and add coverage for cross-realm
typed arrays and DataView serialization, including the empty-array path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9405e41e-c21e-4989-8d2f-a820876fb347

📥 Commits

Reviewing files that changed from the base of the PR and between 209e6b8 and 06d85c3.

📒 Files selected for processing (2)
  • src/js/internal/sql/postgres.ts
  • test/js/sql/postgres-array-typedarray-serialize.test.ts

Comment thread src/js/internal/sql/postgres.ts Outdated
Comment thread src/js/internal/sql/postgres.ts
Comment thread src/js/internal/sql/postgres.ts Outdated
Comment thread src/js/internal/sql/postgres.ts
Comment thread src/js/internal/sql/postgres.ts

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/internal/sql/postgres.ts`:
- Line 145: Add BYTEA serialization test coverage for arrayValueSerializer using
a populated SharedArrayBuffer, alongside the existing ArrayBuffer test, and
assert that Buffer.from(value) preserves the exact byte contents.

In `@test/js/sql/postgres-array-typedarray-serialize.test.ts`:
- Around line 1-6: Condense the header comment in the postgres array typed-array
serialization test to a brief statement of the durable coercion invariant:
ArrayBufferView elements are hex-encoded in BYTEA or JSON arrays and rejected
elsewhere. Remove the implementation-history details, examples, and workaround
explanation while preserving the test-purpose context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: edbbfe25-b5bf-4877-9594-b086544d7df7

📥 Commits

Reviewing files that changed from the base of the PR and between 06d85c3 and c71d6ec.

📒 Files selected for processing (2)
  • src/js/internal/sql/postgres.ts
  • test/js/sql/postgres-array-typedarray-serialize.test.ts

Comment thread src/js/internal/sql/postgres.ts
Comment thread test/js/sql/postgres-array-typedarray-serialize.test.ts

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

LGTM — all prior review threads are resolved and the fix now correctly double-escapes the \x prefix, covers bare ArrayBuffer/SharedArrayBuffer, and names JSONB in the rejection message.

What was reviewed:

  • BYTEA element escaping: arrayEscape("\\x" + hex) produces "\\\\x…" on the wire so array_in hands \x… to byteain; the docker round-trip in sql.test.ts exercises this against a real server.
  • serializeArray top-level TypedArray path uses Array.from(values, serialize) so per-element strings are not coerced back through the typed-array species; the removed isTypedArray helper has no remaining callers.
  • The nested-numeric-TypedArray → ERR_INVALID_ARG_VALUE change was discussed and intentionally kept strict; it converts previously accidental/inconsistent behavior into a loud pre-I/O error with a clear remedy.
Extended reasoning...

Overview

The PR fixes silent data corruption in sql.array(values, type) for PostgreSQL: non-Buffer ArrayBufferView elements were treated as nested array dimensions and recursed via TypedArray.prototype.map, which coerced each per-element string serialization back into the element type ('"1"' → NaN → 0), so a Uint8Array([1,2,44]) in a bytea[] went on the wire as {{0,0,0}}. Changes are confined to src/js/internal/sql/postgres.ts (arrayValueSerializer and serializeArray), a new scripted-backend test file, and one docker-gated round-trip test in sql.test.ts.

Security risks

None. The change is client-side array-literal formatting for a text-format bind parameter; no new parsing of untrusted input, no auth/crypto/permission code paths. The arrayEscape helper is reused for the \x prefix rather than hand-doubling, so escaping stays centralized.

Level of scrutiny

Medium — this is user-facing runtime behavior in the SQL driver, but the change is small (~46 lines net in one internal JS module), replaces silent corruption with either correct output or a typed error, and is backed by wire-literal assertions plus a real-server round-trip. It went through two prior review rounds here; every finding (under-escaped \x, bare-ArrayBuffer fall-through, JSONB missing from the error text) was addressed with a follow-up commit and test.

Other factors

  • The one intentional behavioral tightening (nested numeric TypedArray now throws instead of accidentally serializing via lossless .map coercion) was raised, and the author declined with a solid rationale: the old output was an undocumented byproduct of the exact bug being removed, disagreed with the Buffer path for identical bytes, and now fails loud pre-I/O with a named remedy. That is the safe direction and does not need maintainer sign-off.
  • The removed isTypedArray helper has no remaining references in src/js/internal/sql/.
  • Tests follow the established test/js/sql/wire-frames scripted-backend pattern used by ~30 sibling files, cover Uint8Array/Uint8ClampedArray/DataView/ArrayBuffer/SharedArrayBuffer, byteOffset/byteLength sub-views, the JSON path, the rejection path across TEXT/INT/REAL, and the top-level TypedArray → TEXT non-coercion case.
  • All CodeRabbit and github-actions threads are resolved; the bug hunting system found nothing on the current revision.

robobun and others added 11 commits July 28, 2026 21:01
…stop TypedArray.map coercion

sql.array([new Uint8Array([1,2,44])], "bytea") treated the Uint8Array as a
nested dimension and serialized each element with TypedArray.prototype.map,
which coerces the per-element string ("\"1\"") back into the typed array's
element type (NaN -> 0 for Uint8Array). The literal {{0,0,0}} went on the
wire with no error. The same map coercion zeroed any non-numeric top-level
typed array.

Uint8Array, Uint8ClampedArray and DataView (and Buffer, a Uint8Array
subclass) now take the Buffer hex path as a single binary element. Other
typed arrays remain nested dimensions but build their per-element strings
into a plain Array so nothing is coerced.
… of nesting

Every ArrayBufferView element now hex-encodes in BYTEA and JSON arrays and
throws ERR_INVALID_ARG_VALUE for any other element type, so the silent
{{0,0,0}} / {{NaN,NaN}} corruption becomes a clear error and Uint8Array is
byte-identical to Buffer in bytea[]. Drops the typed-array nested-dimension
arm from arrayValueSerializer; top-level typed arrays still iterate via
Array.from in serializeArray.
The array-literal parser consumes one level of backslash escaping before the
element reaches bytea_in, so {"\x01022c"} was being stored as the ASCII
bytes 'x01022c'. Emit {"\\x01022c"} instead. The pre-existing Buffer path
had the same under-escaping; the new docker round-trip test surfaced it.
…ArrayBuffer

ArrayBuffer.isView(new ArrayBuffer(n)) is false, so a bare ArrayBuffer
element was still falling through to JSON.stringify and serializing as
{"{}"}. Give it (and SharedArrayBuffer) the same hex-or-reject treatment as
every other binary container.
@robobun
robobun force-pushed the farm/815597fb/sql-array-typedarray-bytea branch from d876c56 to 080b5f8 Compare July 28, 2026 21:01

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

LGTM — all four items from earlier passes are addressed (the \x prefix now goes through arrayEscape so the wire literal has the doubled backslash, bare ArrayBuffer/SharedArrayBuffer are covered, the error message names JSONB, and the nested-numeric-TypedArray throw is a reasoned choice).

Checked that Buffer.from(value.buffer, value.byteOffset, value.byteLength) is only taken for views so offset/length are honoured, and that Array.from(values, serialize) receives the element as the bound serializer's value arg with the index harmlessly ignored. The scripted-backend expectations now assert {"\\\\x…"} (two backslashes on the wire), matching what array_in needs to hand \x… to byteain, and the docker round-trip in sql.test.ts covers a real server.

Extended reasoning...

Overview

~30 lines of runtime change confined to arrayValueSerializer / serializeArray in src/js/internal/sql/postgres.ts, plus a new scripted-backend test file and one docker-gated round-trip case in sql.test.ts. The fix replaces the isTypedArray nested-dimension arm (which used TypedArray.prototype.map and coerced per-element strings back into the typed array) with an ArrayBuffer.isView || instanceof ArrayBuffer || instanceof SharedArrayBuffer branch that hex-encodes for BYTEA, JSON-wraps the hex for JSON/JSONB, and throws ERR_INVALID_ARG_VALUE for every other element type. serializeArray now uses Array.from(values, serialize) for a top-level typed array so nothing round-trips through a numeric coercion.

Security risks

None. Output is a text-format Bind parameter, not interpolated SQL; the only string construction is arrayEscape over hex digits and a literal \x prefix (no user-controlled characters reach the escape path in the new branch). No auth, crypto, or filesystem code touched.

Level of scrutiny

Moderate — it's a data-correctness fix in a database driver where the failure mode was silent corruption, so the wire literal has to be exactly right. Two earlier passes here surfaced a real under-escaping bug (single \x backslash), a same-class gap (bare ArrayBuffer), and a message wording nit; all are fixed in the current diff and every thread is resolved. The one deliberate behavior change — throwing for binary elements in non-BYTEA/JSON arrays, including the previously-accidental nested-Int32Array-in-INT case — was discussed and the author's reasoning (undocumented, inconsistent with the Buffer path on main, fails loud pre-I/O, trivial Array.from workaround) is sound.

Other factors

Test coverage is thorough: exact wire-literal assertions for Uint8Array/Uint8ClampedArray/DataView/ArrayBuffer/SharedArrayBuffer in BYTEA, byteOffset/byteLength honouring, JSON hex-wrap parity with Buffer, the rejection matrix across four element kinds × three types, the top-level-TypedArray-no-coercion case, and a real-Postgres round-trip. The new code matches local conventions ($ERR_INVALID_ARG_VALUE, unprotected Buffer/JSON/instanceof Date already used throughout this file). The removed Buffer.isBuffer arm in the default: case is fully subsumed by the new top-of-function guard. No outstanding reviewer comments.

@robobun

robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:18 PM PT - Jul 28th, 2026

❌ @robobun, your commit c11121b has some failures in Build #84406 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 36241

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

bun-36241 --bun

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

LGTM — all four items from the earlier review rounds are addressed (\x prefix now runs through arrayEscape, bare ArrayBuffer/SharedArrayBuffer covered, JSONB named in the rejection message, SAB test added), and the deliberate throw for binary elements in non-binary array types was discussed and justified.

Checked that the BYTEA literal now reaches byteain as \x… after array_in unescapes the doubled backslash, and that the docker round-trip in sql.test.ts asserts the actual bytes.
Confirmed serializeArray's top-level path is behavior-preserving for Buffer (still returned unchanged) and plain arrays; only non-Buffer views switch from .map to Array.from.
Verified Buffer.from(value.buffer, value.byteOffset, value.byteLength) is exercised by the offset/length subarray + DataView test.

Extended reasoning...

Overview

Targeted fix in src/js/internal/sql/postgres.ts to arrayValueSerializer and serializeArray: any ArrayBufferView / ArrayBuffer / SharedArrayBuffer element is now hex-encoded for BYTEA / JSON / JSONB arrays and rejected with ERR_INVALID_ARG_VALUE otherwise, replacing the old path that recursed via TypedArray.prototype.map and coerced serialized strings back into the element type (silently zeroing the bytes). The \x prefix is routed through arrayEscape so array_in unescapes back to \x for byteain. serializeArray uses Array.from for a top-level TypedArray so per-element strings are not coerced. New scripted-backend test file asserts the exact Bind literal for each view/buffer variant plus a docker-gated round-trip in sql.test.ts.

Security risks

None. sql.array output is a text-format bind parameter, not interpolated SQL; arrayEscape still handles \ and " for the array-literal syntax and hex output is [0-9a-f]*. No new external input parsing.

Level of scrutiny

Medium — user-facing serialization on a data-integrity path. The PR has been through two prior review passes here plus CodeRabbit; every thread is resolved. The one behavioral tightening (Buffer/TypedArray elements now throw in TEXT/INT/etc. arrays instead of emitting a bare hex string or accidentally-correct nested numerics) was explicitly raised and the author gave a reasoned justification: the old output was an undocumented byproduct of the exact coercion bug being removed, disagreed with the sibling Buffer path, and now fails loud before I/O with the type named in the message.

Other factors

The scripted-backend tests assert the exact wire literal and were shown to fail on main and pass on the branch in both debug+ASAN and release. The docker round-trip is what caught the original single-backslash bug. Buffer.from(view.buffer, view.byteOffset, view.byteLength) is covered by a dedicated offset/length test, and bare ArrayBuffer/SharedArrayBuffer each have a positive BYTEA case and a rejection case. Top-level serializeArray behavior is unchanged for plain arrays and top-level Buffer (still short-circuits to return values). SharedArrayBuffer is always defined in Bun so the unguarded instanceof is fine.

@robobun

robobun commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

#41301 overlaps with this PR in arrayValueSerializer and serializeArray. It calls Array.prototype.map at both sites, so it also fixes the TypedArray map coercion. It does not hex-encode Uint8Array, DataView, or ArrayBuffer elements. Only this PR does that.

If #41301 merges first, this PR needs a rebase. In #41301 an untyped array has type === undefined, and a Buffer element in it binds as \x hex. The check here that throws for other types must accept undefined. If it does not, an untyped sql.array([buffer]) throws.

This branch has not been deployed

No deployments
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.

2 participants