Skip to content

sql(postgres): route the six Bind encoder call sites through PostgresSQLConnection::encode_request; pin Bind wire bytes - #43892

Merged
alii merged 4 commits into
mainfrom
robobun/0313f824/pg-encode-request-seam
Sep 25, 2026
Merged

alii merged 4 commits into
mainfrom
robobun/0313f824/pg-encode-request-seam

Conversation

@robobun

@robobun robobun commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Behaviour change: none

Part 1 of 3 of the follow-up to #34732 (plan). Rebased on main after #43898 merged.

Problem

  • Six call sites encode a request's Bind parameters: four in PostgresSQLConnection::advance and two in PostgresSQLQuery::do_run. Each calls one of three batch writers in PostgresRequest.rs directly, and advance carries seven copies of the same error arm.
  • The next two PRs change what happens around that encode. With six entry points each change needs six edits, and a seventh call site can bypass them.

Fix

  • PostgresSQLConnection::encode_request(global, EncodeRequest) is now the only route to bind_and_execute, prepare_and_query_with_signature and parse_and_bind_and_execute. They and write_bind are private to PostgresRequest.rs.
  • advance rejects a request whose write failed through one function, reject_failed_write, at all seven arms. The one difference between the old arms (a statement's first write marks the statement failed) is its new_statement argument.
  • postgres-bind-wire.test.ts pins the exact frontend bytes for 13 kinds of parameter, a binary result column, the sql() helper, no parameters, and prepare: false. It passes on main without this PR. wire-frames.ts gets frontend builders with a layout self-test.
  • Verified: existing coverage is sql.test.ts, sql-prepare-false.test.ts, postgres-prepared-pipeline-reorder.test.ts, postgres-split-prepare-reorder.test.ts, postgres-simple-query-pipeline.test.ts, postgres-bytea-bind.test.ts, wire-frames.test.ts. All 255 tests in test/js/sql/postgres-* pass on the debug build.

Background

Downsides

  • Per batch: one call and one match on a 3-variant enum of 48 bytes, if encode_request is not inlined. No allocation, no syscall.

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

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/sql/postgres-bind-wire.test.ts test/js/sql/wire-frames.test.ts
ninja: Entering directory `/workspace/bun/build/debug'
[0/2] cargo plan → /workspace/bun/build/debug/rust-target/plan.json
FAILED: [code=1] rust-target/plan.json /workspace/bun/build/debug/rust-target/plan.json 
/workspace/bun/build/release/bun /workspace/bun/scripts/build/stream.ts cargo --console /workspace/bun/build/release/bun /workspace/bun/scripts/build/rust/plan.ts /workspace/bun/build/debug/rust-target/plan.input.json /workspace/bun/build/debug/rust-target/plan.json
�[1m�[91merror�[0m: cannot update the lock file /workspace/bun/Cargo.lock because --locked was passed to prevent this
help: to generate the lock file without accessing the network, remove the --locked flag and use --offline instead.
error: /root/.cargo/bin/cargo build -p bun_runtime --lib … exited with 101
ninja: error: rebuilding 'build.ninja': subcommand failed
error: script "bd" exited with code 1
__F:-1:S:0

release without fix: all passed
bun test v1.4.3-canary.1 (41e52b124)

test/js/sql/wire-frames.test.ts:
(pass) mysqlLenencInt encodes per page_protocol_basic_dt_integers.html [0.16ms]
(pass) pgErrorResponse encodes per §55.7 [0.17ms]
(pass) frontend message builders encode per §55.7 [0.55ms]
(pass) postgres: pgAuthenticationOk + pgReadyForQuery are accepted by Bun's parser [6.68ms]
(pass) postgres: COPY OUT response frames are consumed and the following result set decodes [3.62ms]
(pass) postgres: pgMinimalReadyServer satisfies connect() [0.88ms]
(pass) mysql: mysqlHandshakeV10 + mysqlOkPacket are accepted by Bun's parser [1.71ms]

test/js/sql/postgres-bind-wire.test.ts:
(pass) named statements > null [1.80ms]
(pass) named statements > boolean as bool [0.57ms]
(pass) named statements > integer as int4 [0.46ms]
(pass) named statements > BigInt as int8 [0.50ms]
(pass) named statements > double as float8 [0.42ms]
(pass) named statements > Date as timestamptz [0.50ms]
(pass) named statements > ASCII string as text [0.36ms]
(pass) named statements > non-ASCII string as text [0.50ms]
(pass) named statements > object as jsonb [0.62ms]
(pass) named statements > array as json [0.51ms]
(pass) named stateme
... (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/pr_gate.xml" test/js/sql/postgres-bind-wire.test.ts test/js/sql/wire-frames.test.ts
bun test v1.4.3 (367d939d9)

test/js/sql/wire-frames.test.ts:
(pass) mysqlLenencInt encodes per page_protocol_basic_dt_integers.html [12.75ms]
(pass) pgErrorResponse encodes per §55.7 [8.47ms]
(pass) frontend message builders encode per §55.7 [36.72ms]
(pass) postgres: pgAuthenticationOk + pgReadyForQuery are accepted by Bun's parser [380.29ms]
(pass) postgres: COPY OUT response frames are consumed and the following result set decodes [236.22ms]
(pass) postgres: pgMinimalReadyServer satisfies connect() [46.69ms]
(pass) mysql: mysqlHandshakeV10 + mysqlOkPacket are accepted by Bun's parser [69.90ms]

test/js/sql/postgres-bind-wire.test.ts:
(pass) named statements > null [79.26ms]
(pass) named statements > boolean as bool [23.18ms]
(pass) named statements > integer as int4 [20.74ms]
(pass) named statements > BigInt as int8 [24.29ms]
(pass) named statements > double as float8 [20.51ms]
(pass) named statements > Date as timestamptz [23.33ms]
(pass) named statements > AS
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     ec7e250da5
  features     lto, baseline

23 deps, 136 codegen, 1176 objects in 1214ms

ninja: Entering directory `/workspace/bun/build/release'
[1/4] fetch rust-argon2
[rust-argon2] up to date
[2/4] fetch lolhtml
[lolhtml] up to date
[2/4] cargo plan → /workspace/bun/build/release/rust-target/plan.json
244 units: 172 lib, 16 proc-macro (host), 19 custom-build (host), 15 run custom-build, 17 lib (host), 4 run custom-build (host), 1 rlib
[1/1493] install /workspace/bun
bun install v1.4.3-canary.1 (41e52b124)

Checked 26 installs across 65 packages (no changes) [42.00ms]
[2/1493] rustc unicode_ident 
[3/1493] rustc heck 
[4/1493] rustc build_script_build 
[5/1493] install /workspace/bun/src/node-fallbacks
bun install v1.4.3-canary.1 (41e52b124)

Checked 111 installs across 104 packages (no changes) [18.00ms]
[6/1493] build.rs build_script_build
[7/1493] rustc build_script_build 
[8/1493] gen generated_host_exports.rs
generated_host_exports.rs: 121 exports (host=5, lazy=10, generic=106, r
... (truncated)
diff hotspot
src/sql_jsc/postgres/PostgresRequest.rs       |  70 ++++++-
 src/sql_jsc/postgres/PostgresSQLConnection.rs | 162 +++++++----------
 src/sql_jsc/postgres/PostgresSQLQuery.rs      |  23 ++-
 test/js/sql/postgres-bind-wire.test.ts        | 253 ++++++++++++++++++++++++++
 test/js/sql/wire-frames.test.ts               |  36 ++++
 test/js/sql/wire-frames.ts                    |  68 +++++++
 6 files changed, 497 insertions(+), 115 deletions(-)

gate history · 1 passed · 0 rejected · iteration 3

evidence per changed file
file                                           reads  edits  tests
src/sql_jsc/postgres/PostgresRequest.rs            5      4     32
src/sql_jsc/postgres/PostgresSQLConnection.rs     10      1     30
src/sql_jsc/postgres/PostgresSQLQuery.rs           8      3     31
test/js/sql/postgres-bind-wire.test.ts             0      1     16
test/js/sql/wire-frames.test.ts                    0      0     21
test/js/sql/wire-frames.ts                         3      1     35

@robobun

robobun commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: rebased on main at 8884311 (after #43898 merged), head ec7e250. The rebase had no conflict.

bun bd test test/js/sql/postgres-bind-wire.test.ts test/js/sql/wire-frames.test.ts test/js/sql/postgres-bind-encode-throw.test.ts test/js/sql/postgres-bytea-bind.test.ts test/js/sql/sql-prepare-false.test.ts   # 52 pass
USE_SYSTEM_BUN=1 bun test test/js/sql/postgres-bind-wire.test.ts   # 18 pass on main without this PR: the corpus pins today's bytes

The full test/js/sql/postgres-* run (255 pass) was on the head before the rebase, f787b5c, with the same diff.

This PR changes no behaviour, so it has no failing-before test. Next in the stack: #43918 (a query dispatched while parameters are converted only enqueues), then conversion before the first write.

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked the refactor for behavior drift: the four advance() arms keep their old error-path side effects (statement marked Failed only at the two first-Parse sites, JS exception path unchanged), and the now-unused connection.writer() in do_run before encode_request is harmless — NewWriter's truncate-on-error guard captures the epoch after encode_request's own bump, so a failed batch still truncates correctly.

Extended reasoning...

The change routes the six Bind encoder call sites in src/sql_jsc/postgres/PostgresSQLConnection.rs and PostgresSQLQuery.rs through a new PostgresSQLConnection::encode_request in PostgresRequest.rs, makes the three batch writers private, extracts reject_failed_encode, and adds a mock-server test pinning Bind frontend bytes. It touches no auth, crypto, or injection surface; parameters are still encoded through the unchanged write_bind. The Rust side is behavior-preserving on inspection, but with inline findings posted and further verified findings withheld, this is not an approve.

Comment thread test/js/sql/postgres-bind-wire.test.ts Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLConnection.rs
Comment thread test/js/sql/postgres-bind-wire.test.ts Outdated
@robobun
robobun force-pushed the robobun/0313f824/pg-encode-request-seam branch from 71d61bc to 6fcff61 Compare September 24, 2026 14:59
Comment thread src/sql_jsc/postgres/PostgresRequest.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLConnection.rs Outdated
@robobun

robobun commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:58 AM PT - Sep 24th, 2026

✅ @robobun, your commit f787b5cb9d8cfd98c820e97ad648c55081259391 passed in Build #120331! 🎉


🧪   To try this PR locally:

bunx bun-pr 43892

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

bun-43892 --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.

I re-reviewed the updated PR and found no bugs; the earlier inline feedback (file header, pgRaw instead of a local frame builder, a reachable int8 case, and routing the execute_query/write_query/SYNC arms through the helper) is addressed in the code. Because this rewires every Bind encode path inside advance() and I could not run bun bd test here (no debug build in this checkout), a human look at the Rust side is still worthwhile.

What was reviewed:

  • encode_request obtains self.writer() (which bumps write_epoch) before atomically records the epoch, so the failed-batch rollback from the base commit still triggers; the extra writer() call left in PostgresSQLQuery::run for the simple-query/write_query path is harmless for the same reason.
  • reject_failed_write arguments match the old arms one-to-one (None for the two arms that never marked the statement, Some(statement) for the four that did); the swapped order of status/error_response assignment at the write_query/SYNC arms is unobservable.
  • Pinned frontend bytes in wire-frames.test.ts (Parse 0x17, Describe 0x08, Bind 0x20, Execute 0x09) check out against the §55.7 layouts.
  • The "Above i16::MAX" comment at test/js/sql/postgres-bind-wire.test.ts:35 is off by width (the client's cutoff in write_bind is Short::MAX, i.e. u16), but 70000 exceeds both, so the case still exercises the unknown-type path.
Extended reasoning...

The diff touches three Rust files in src/sql_jsc/postgres (a new EncodeRequest enum and encode_request method, a reject_failed_write helper used at seven error arms in advance(), and two call sites in PostgresSQLQuery::run) plus a new mock-server wire test and frontend frame builders; no security-sensitive surface (no auth, TLS, or user-input parsing changes) is involved. The Rust change is mechanical and I traced it as behavior-preserving, and all of my prior inline objections were addressed by commits 6fcff61 and d90e979. I deferred rather than approved because the change rewires the core Bind path of the Postgres driver state machine and the tests could not be executed in this checkout (no debug build), so the author's claim that all postgres tests pass is unverified here.

This review covers commit d90e979, which is no longer the latest commit on this pull request; later commits are not covered by it.

Base automatically changed from farm/0313f824/postgres-bind-torn-frame to main September 24, 2026 15:20
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: ecc39ce2-9841-4d5a-af1e-1012b35ca39b

📥 Commits

Reviewing files that changed from the base of the PR and between f787b5c and ec7e250.

📒 Files selected for processing (1)
  • src/sql_jsc/postgres/PostgresSQLConnection.rs

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


Walkthrough

PostgreSQL request encoding now uses connection-level dispatch, and request writers serialize protocol messages atomically. Connection write failures use shared handling. New tests check frontend frame bytes and named and unnamed statement batches.

Changes

PostgreSQL Request Encoding

Layer / File(s) Summary
Request encoding contract and dispatch
src/sql_jsc/postgres/PostgresRequest.rs
The crate-visible EncodeRequest enum defines three request forms. PostgresSQLConnection::encode_request dispatches them to private request writers.
Connection and query call sites
src/sql_jsc/postgres/PostgresSQLConnection.rs, src/sql_jsc/postgres/PostgresSQLQuery.rs
Connection and query call sites use encode_request. Connection write failures use reject_failed_write to forward pending exceptions or report failures to requests.
Frontend frame and request wire tests
test/js/sql/wire-frames.ts, test/js/sql/wire-frames.test.ts, test/js/sql/postgres-bind-wire.test.ts
Test helpers encode PostgreSQL frontend frames. Tests check frame bytes and the message batches sent for named and unnamed statements.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to ec7e2

No actionable behavior change was established; the PR is mergeable after normal checks.

🚥 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 identifies the main changes: routing six PostgreSQL Bind encoder call sites through PostgresSQLConnection::encode_request and adding wire-byte tests.
Description check ✅ Passed The description explains the problem, implementation, behavior-preserving intent, test coverage, and verification results. It does not use the template headings exactly, but it provides the required i…

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

@robobun
robobun force-pushed the robobun/0313f824/pg-encode-request-seam branch from c2d8b0c to f787b5c Compare September 24, 2026 15:30

@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 — the new commits address everything raised earlier (all seven advance() arms now go through reject_failed_write, the local frame builder is gone in favour of pgRaw, and the int8 case now declares the type it describes), and the refactor is behaviour-preserving.

What was reviewed:

  • Each of the seven reject_failed_write sites against its pre-diff arm: the None / Some(&mut *statement) choices match which arms previously set status = Failed + error_response, and try_take_exception still runs before any statement mutation.
  • The extra self.writer() inside encode_request on the do_run path: it bumps write_epoch before atomically captures the epoch, so the partial-Bind rollback from #34732 is unaffected; the writer local is still live for write_query/SYNC.
  • Grepped for other callers of the now-private batch writers — none outside PostgresRequest.rs (the MySQL bind_and_execute is a separate method).
  • Test isolation: the client sends no Terminate on close, so the shared received map cannot leak a frame from the first describe into the second's drain().
Extended reasoning...

The diff is a mechanical routing refactor in the Postgres client: three batch writers and write_bind become private to src/sql_jsc/postgres/PostgresRequest.rs, a new EncodeRequest enum plus PostgresSQLConnection::encode_request is the single entry point, six call sites in PostgresSQLConnection.rs advance() and PostgresSQLQuery.rs do_run switch to it, and seven identical error tails collapse into reject_failed_write. It touches no security-sensitive surface (no auth, TLS, or parsing of server bytes changes; parameter encoding itself is untouched). No CODEOWNERS entry covers the changed files, no CHANGES_REQUESTED review is outstanding, and the four new commits address every point from the two prior review rounds. The new test file pins frontend bytes for the named, unnamed, sql() helper and no-parameter paths against a local mock and passes on main, so it guards the refactor rather than a new behaviour. Approving because I traced each arm's old versus new side effects and found them identical.

…SQLConnection::encode_request; pin Bind wire bytes

The three batch writers that encode Bind parameters (bind_and_execute,
prepare_and_query_with_signature, parse_and_bind_and_execute) were called
from four places in advance() and two in do_run. They are now private to
PostgresRequest.rs and reached only through
PostgresSQLConnection::encode_request. advance() rejects a request whose
batch failed to encode through one function, reject_failed_encode, in place
of four copies of the same arm.

No behaviour change. test/js/sql/postgres-bind-wire.test.ts pins the exact
frontend bytes for each kind of parameter (named statements, prepare: false,
no parameters, the sql() helper), built from new frontend message builders
in wire-frames.ts.
…eachable int8 case, pgRaw, header

The simple-query arm and the two arms of the named Parse/Describe/Sync write
carried the same error tail as the four Bind arms. All seven now go through
reject_failed_write.

The corpus described an int8 parameter that Parse declared as int4, which a
real server cannot answer. It now binds a BigInt (declared and described as
int8). The file header says why a mock is used, and the recorder uses pgRaw.
@alii
alii merged commit b8066bd into main Sep 25, 2026
11 checks passed
@alii
alii deleted the robobun/0313f824/pg-encode-request-seam branch September 25, 2026 17:11
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