Skip to content

sql(postgres): reject only the query whose row the client cannot decode - #43187

Merged
Jarred-Sumner merged 5 commits into
mainfrom
robobun/8073a72b/postgres-row-decode-error-scope
Sep 24, 2026
Merged

Jarred-Sumner merged 5 commits into
mainfrom
robobun/8073a72b/postgres-row-decode-error-scope

Conversation

@robobun

@robobun robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Bun.SQL (postgres): a reply holds a value the client cannot decode, such as '{{1,2},{3,4}}'::int4[] in a parameterised query. Every other query on that connection then rejects with that query's error, ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET (Failed to read data). With pipelining, the server already executed them.
  • The DataRow arm of PostgresSQLConnection::on (src/sql_jsc/postgres/PostgresSQLConnection.rs) returned the decode error with ?. on_data then calls fail(): the socket closes and the whole queue rejects.

Fix

  • DataRow::decode skips the rest of the row when a cell fails. The arm rejects only the current request: the same error, plus a hint that the query may have run. A Putter::to_js error takes the same path.
  • The rejected request stays in flight at the queue head until its ReadyForQuery. The new discard_response flag skips the rest of its reply.
  • Correct because the server still sends that request's remaining rows. Marked Fail at once, advance() popped it early and the next query resolved with its leftover row. Three tests cover this.
  • Verified: test/js/sql/postgres-row-decode-error.test.ts (5 tests on a real postgres, 4 on a mock, all fail without the fix), and every test/js/sql/postgres-*.test.ts. Self-reviewed: 6 concerns raised, 6 addressed (Notes).

Background

  • Bun.SQL pipelines: it writes several prepared queries before the first reply arrives. Each reply message goes to the request at the head of a FIFO queue.
  • A reply ends with ReadyForQuery. advance() then pops finished requests and writes queued ones.
  • With pipelining, advance() can also run mid-reply, and it pops a head with status Fail. After an ErrorResponse that is harmless: the server sends nothing more.
Notes

Reproduction against a real postgres, max: 1, before the fix:

query 1: resolved [{"v":"A"}]
query 2: REJECTED: ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET Failed to read data
query 3: REJECTED: ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET Failed to read data
query 4: REJECTED: ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET Failed to read data

After the fix, only query 2 rejects, and pg_backend_pid() is the same before and after. The same happens with .simple() queries and '[0:2]={1,2,3}'::int4[] (ERR_POSTGRES_UNSUPPORTED_ARRAY_FORMAT), and with a jsonb text that is not JSON (JSON Parse error: Unexpected identifier "not", mock only).

Values from a healthy server that reach this path (each one fails only its own query now): pg_index.indoption::int2[] and other int2vector casts, arrays with explicit bounds, bytea with bytea_output = escape in a simple query, binary int4[]/float4[] with a NULL element or two dimensions.

postgres.js does the same: a parser that throws in DataRow rejects the current query at once, and the query stays current until ReadyForQuery. The MySQL adapter has the same defect in another form: it rejects only the request, but then hands the rest of that request's response to the next one, and a to_js error closes the connection. #43323 is the same fix there, and the two PRs are a pair.

What this does not change: the query that owns the bad row still rejects, also when it is an INSERT ... RETURNING that the server already stored. Its message is the same Failed to read data as before, so the rejection now has a hint: "The query may have run on the server. The client could not decode a value in its result. Cast that column to text, or use .raw()." docs/runtime/sql.mdx says the same under Data Type Errors, and one test pins it: a rejected INSERT ... RETURNING is stored. The write side is separate: a Bind parameter that fails to encode still makes the server close the connection for the pipelined neighbours, and #34732 fixes that.

Other shapes I ran by hand on the ASAN debug build against a real postgres:

  • A 200000 row result whose first row cannot be decoded, with Bun.gc(true) in the rejection handler while the rest streams. The same with the bad row second to last.
  • sql.begin(): the callback throws on the rejection and ROLLBACK runs on the same connection. A callback that catches the rejection continues in the same transaction.
  • .values() rejects the same way. .raw() does not decode and resolves.
  • A server ErrorResponse (division by zero) that arrives after the local rejection, for the same query. The query rejects once, with the decode error.

A framing error (a DataRow whose lengths do not agree with the message length) still fails the connection. A pending termination exception still propagates as a connection failure, as before (undecodable_row_error returns the error when has_pending_termination_exception() is true).

A cell can fail with a JS exception pending (Date.parse of a 1 GiB text throws out of memory). decode returns at once on a cell error, and undecodable_row_error then takes the exception, so no exception stays pending.

The cell loop is the same as before on the success path: two monomorphized DataRow::decode calls, and a failed cell is marked on the error edge only (inspect_err). An earlier revision of this PR added two branches per cell, and the self-review measured about 5% more client CPU on a wide select with many NULLs.

Self-review, 6 concerns, all addressed: (1) a real-server test where the rest of the rejected query's rows arrives in later reads, with the query wrapper collected in that window (added). (2) and (3) the MySQL adapter has the same defect: named here, fixed in #43323. (4) no test had a cell after the bad one: the undecodable rows are now multi-column, and there is a row wider than the inline cell buffer. (5) the per-cell cost above. (6) the failing query can itself have run: hint, docs and the INSERT ... RETURNING test.

Mutation checks on the debug build: with the skip in decode removed, the four multi-column tests fail. With the request marked Fail at once, the real-server multi-read test and the text[] hold test fail.

The accounting stays as it was: pipelined_requests and nonpipelinable_requests are released by finish_request at ReadyForQuery, so can_pipeline() and can_prepare_query() see the request as in flight while the rest of its reply streams.

The mock tests exist because a healthy server validates json on input and does not stop in the middle of a reply on demand. They run with two column kinds, because a row can fail in two places: a text[] cell with bounds fails while the DataRow is read (Putter::put), and a jsonb cell that is not JSON fails when the row becomes a JS object (Putter::to_js). The mock holds back the rest of the rejected query's reply until the test's rejection handler has enqueued a statement that is not prepared yet. That enqueue calls advance() at that point.

Test runs on the debug build: test/js/sql/postgres-*.test.ts gives 184 pass and 2 fail. Both failures are the RSS leak tests in postgres-string-leak.test.ts, which exceed the 5 s default timeout under ASAN. Their fixtures pass when run directly (9.2 s and 5.9 s), and the file passes on the release build. A local run of sql.test.ts against the local postgres has the same failures as the unfixed build, plus tests that time out under the debug build (reserve connection fails the same way on the unfixed debug build).


[human-review] gate passed · iteration 0 · 7 files touched

fails on main (without fix)
ASAN without fix: 9 FAILED
$ 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-row-decode-error.test.ts
bun test v1.4.3 (b52d51348)

test/js/sql/postgres-row-decode-error.test.ts:
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
135 |     const text = "select " + names.map((name, i) => (i === 35 ? "$1::int4[]" : i) + " as " + name).join(", ");
136 |     const wide = (array: string) => sql.unsafe(text, [array]);
137 |     const row = (array: Int32Array) => Object.fromEntries(names.map((name, i) => [name, i === 35 ? array : i]));
138 |     await wide("{0}");
139 | 
140 |     expect(await settle([wide("{1}"), wide("{{1,2},{3,4}}"), wide("{2}")])).toEqual([
                                                                                  ^
error: expect(received).toEqual(expected)

@@ -75,86 +75,16 @@
        "c9": 9,
      },
    ],
    {
      "code": "ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET",
-     "hint": "The query may have run on the server. The client could not decode a value in its result. Cast that column to text, or use .raw().",
+     "hint": undefined,
... (truncated)

release without fix: 7 FAILED
bun test v1.4.3-canary.1 (b52d51348)

test/js/sql/postgres-row-decode-error.test.ts:
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
135 |     const text = "select " + names.map((name, i) => (i === 35 ? "$1::int4[]" : i) + " as " + name).join(", ");
136 |     const wide = (array: string) => sql.unsafe(text, [array]);
137 |     const row = (array: Int32Array) => Object.fromEntries(names.map((name, i) => [name, i === 35 ? array : i]));
138 |     await wide("{0}");
139 | 
140 |     expect(await settle([wide("{1}"), wide("{{1,2},{3,4}}"), wide("{2}")])).toEqual([
                                                                                  ^
error: expect(received).toEqual(expected)

@@ -75,11 +75,11 @@
        "c9": 9,
      },
    ],
    {
      "code": "ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET",
-     "hint": "The query may have run on the server. The client could not decode a value in its result. Cast that column to text, or use .raw().",
+     "hint": "The query may have run on the server. The client could not decode a value in its result.",
      "message": "Failed to read data",
      "name": "PostgresError",
    },
    [
   
... (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-row-decode-error.test.ts
bun test v1.4.3 (b52d51348)

test/js/sql/postgres-row-decode-error.test.ts:
Container ready via docker-compose: postgres_plain at 127.0.0.1:5432
(pass) postgres > a wide row with a cell the client cannot decode rejects alone [814.43ms]
(pass) postgres > a simple query with a row the client cannot decode rejects alone [2875.09ms]
(pass) postgres > a rejected INSERT ... RETURNING is stored [2880.98ms]
(pass) postgres > a pipelined query with a row the client cannot decode rejects alone [4849.63ms]
(pass) postgres > the rest of a rejected query's rows arrives in later reads [3580.80ms]
(pass) postgres mock, text[] with explicit bounds > a pipelined query with a row the client cannot decode rejects alone [2945.39ms]
(pass) postgres mock, jsonb that is not JSON > a pipelined query with a row the client cannot decode rejects alone [1665.07ms]
(pass) postgres mock, text[] with explicit bounds > rows that arrive after the rejection stay with the rejected query [1892.15ms]
(pass) postgres mock, jsonb
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 3629ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/145] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[2/143] gen NodeModuleModule.lut.h
Generating /workspace/bun/build/release/codegen/NodeModuleModule.lut.h from /workspace/bun/src/jsc/modules/NodeModuleModule.cpp
[3/143] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (15 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts
  - ResourceUsage (8 fields)
  - Subprocess (20 fields)
Found 1 classes from /workspace/bun/src/runtime/api/cron.classes.ts
  - CronJob (5 fields)
Found 3 classes from /workspace/bun/src/runtime/api/filesystem_router.classes.ts
  - FileSystemRouter (5 fields)
  - FrameworkFileSystemRouter (2 fields)
  - MatchedRoute (8 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Glob.classes.ts
  - Glob (5 
... (truncated)
diff hotspot
docs/runtime/sql.mdx                          |   2 +
 src/sql/postgres/protocol/DataRow.rs          |  34 ++--
 src/sql_jsc/postgres/PostgresSQLConnection.rs |  60 ++++--
 src/sql_jsc/postgres/PostgresSQLQuery.rs      |  19 +-
 src/sql_jsc/postgres/error_jsc.rs             |  10 +
 test/js/sql/postgres-row-decode-error.test.ts | 281 ++++++++++++++++++++++++++
 test/js/sql/wire-frames.ts                    |  44 +++-
 7 files changed, 413 insertions(+), 37 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                           reads  edits  tests
docs/runtime/sql.mdx                               0      0     26
src/sql/postgres/protocol/DataRow.rs               1      0     24
src/sql_jsc/postgres/PostgresSQLConnection.rs     11      9     24
src/sql_jsc/postgres/PostgresSQLQuery.rs           4      2     25
src/sql_jsc/postgres/error_jsc.rs                  2      2     24
test/js/sql/postgres-row-decode-error.test.ts      3      3     24
test/js/sql/wire-frames.ts                         1      1     32

A DataRow can be framed correctly and still hold a value the client
cannot turn into a JS value (a binary int4[] with two dimensions, a text
array with explicit bounds, a jsonb text that is not JSON). The DataRow
handler returned that error to on_data, which failed the connection and
rejected every queued query with the first query's error. Pipelined
queries had already been executed by the server at that point.

The handler now reads the frame to its end and rejects the current
request alone. The request stays in flight at the head of the queue
until its ReadyForQuery, and the rest of its response is skipped, so
advance() cannot pop it early and hand its remaining rows to the next
request. Framing errors still fail the connection.
@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

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: a33c9b3f-a507-43a8-be2b-8598101703f5

📥 Commits

Reviewing files that changed from the base of the PR and between 727caab and aa08aa1.

📒 Files selected for processing (4)
  • docs/runtime/sql.mdx
  • src/sql/postgres/protocol/DataRow.rs
  • src/sql_jsc/postgres/PostgresSQLConnection.rs
  • test/js/sql/postgres-row-decode-error.test.ts

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


Walkthrough

PostgreSQL row decoding now separates client decode failures from protocol errors. Affected queries reject while remaining in flight, skip their remaining results, and finish at ReadyForQuery. Tests cover pipelined, multi-result, delayed, wide-row, and write-returning responses.

Changes

PostgreSQL row decode rejection

Layer / File(s) Summary
Query rejection state and error hints
src/sql_jsc/postgres/PostgresSQLQuery.rs, src/sql_jsc/postgres/error_jsc.rs
Queries track discarded responses and expose is_rejected(). PostgreSQL errors can include optional hints.
Connection decode and protocol completion
src/sql/postgres/protocol/DataRow.rs, src/sql_jsc/postgres/PostgresSQLConnection.rs
Row decoding skips remaining row bytes after callback errors. Connection handling converts client decode failures, rejects affected requests, skips their remaining protocol results, and completes discarded responses at ReadyForQuery.
Decode failure test coverage and guidance
test/js/sql/postgres-row-decode-error.test.ts, test/js/sql/wire-frames.ts, docs/runtime/sql.mdx
Tests cover isolated, pipelined, multi-result, delayed, wide-row, and INSERT ... RETURNING failures. Wire helpers parse bind parameters and hold frames for controlled release. Documentation describes query failure behavior and workarounds.

Suggested reviewers: dylan-conway, jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to aa08a

A PostgreSQL query whose result row cannot be decoded now fails only that query and leaves the connection usable for pipelined and subsequent queries, with documentation noting that the query may already have executed on the server. The earlier concern about a crash on a split network read does not apply, since rows are only decoded once fully received. No merge-blocking risk remains.

🚥 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 and concisely describes the main change: PostgreSQL row-decoding failures reject only the affected query.
Description check ✅ Passed The description explains the problem, implementation, behavior, scope, verification steps, test results, and limitations. It does not use the template headings exactly, but it provides the required in…

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

@robobun

robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: fix and tests are pushed.

How I reproduced it, on main b52d513 and on canary 1.4.3 (c6b7fcb), against a real postgres with max: 1:

const q = v => sql`select ${v}::int4[] as v`;
await q("{0}"); // prepares the statement, so the next four are pipelined
await Promise.allSettled([q("{1}"), q("{{1,2},{3,4}}"), q("{2}"), q("{3}")]);
  • Before the fix, queries 2, 3 and 4 all reject with ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET (Failed to read data), and the connection closes.
  • With the fix, only query 2 rejects. Queries 3 and 4 resolve with their own rows, and pg_backend_pid() stays the same.
  • The same happens with .simple() queries and select '[0:2]={1,2,3}'::int4[] (ERR_POSTGRES_UNSUPPORTED_ARRAY_FORMAT).

Test: bun bd test test/js/sql/postgres-row-decode-error.test.ts (9 tests: 5 against a real postgres, 4 against a mock. All fail without the src/ change).

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

Code review found no issues

No high-confidence issues detected in this change.

One verified lower-impact observation (a convention, logging or cleanup point) was not posted.

@robobun

robobun commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:42 PM PT - Sep 18th, 2026

✅ @robobun, your commit aa08aa17961ef1936d2f0a2bc75271e0fcdaf1c6 passed in Build #118144! 🎉


🧪   To try this PR locally:

bunx bun-pr 43187

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

bun-43187 --bun

…h failure paths in the mock tests

The rejected query may be an INSERT ... RETURNING that the server already
stored, and the message is the same "Failed to read data" a dead socket
gives. The rejection now carries a hint that says only the decoding of
the result failed.

The mock tests run once with a text[] cell (fails while the DataRow is
read) and once with a jsonb cell (fails when the row becomes a JS
object).
Comment thread src/sql_jsc/postgres/PostgresSQLConnection.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLConnection.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLConnection.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLConnection.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLConnection.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLQuery.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLQuery.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLQuery.rs Outdated

@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 note, I also checked two paths that could double-settle a discard_response request: a server ErrorResponse (or clean_up_requests on socket close) arriving after the local rejection goes through on_js_error -> reject, which is a no-op once get_target(.., true) has cleared the cached target, and finish_request is idempotent via RequestCounter::None. The remaining ungated handlers (RowDescription, BindComplete, NoData, ParseComplete) only mutate statement/status state and never resolve the promise, so they need no is_rejected() gate.

Extended reasoning...

This run's inline finding is a test-helper duplication nit; the native change itself is a query-lifecycle state-machine change in the Postgres driver (a rejected request kept at the queue head until ReadyForQuery), which is not simple enough to approve without a human look, and one further verified finding is unposted. I traced the new discard_response flag through every consumer (DataRow, CommandComplete, CloseComplete, EmptyQueryResponse, ReadyForQuery), confirmed advance() treats the still-Running request as in flight, confirmed a second rejection via ErrorResponse or clean_up_requests is harmless because reject early-returns once the cached target is zeroed and this_value is downgraded, and confirmed postgres_error_to_js_with_hint on JSError takes the pending exception before decode_result? can propagate a framing error. Nothing concrete beyond the inline finding turned up.

Comment thread test/js/sql/postgres-row-decode-error.test.ts Outdated
… error tests

The echo mock of postgres-row-decode-error.test.ts repeated the startup
and frame loop of pgMockServer to be able to keep back part of a reply.
pgMockServer now accepts pgHold in a reply and returns release(), and
the test mock is a responder on top of it.

@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 latest revision (including the pgHold/release() refactor that replaced the duplicated mock loop) and found no bugs, but because this changes the request lifecycle of the Postgres state machine, a human look is still worthwhile before merging.

What was reviewed:

  • Every former status == Fail skip site now goes through is_rejected(); ReadyForQuery is the only consumer that clears the in-flight request, and finish_request stays idempotent there (counter reset to None, Fail arm is a no-op) if an ErrorResponse for the same query already ran it.
  • Double-rejection paths (server ErrorResponse after the local rejection, clean_up_requests on close): reject clears target on the first call, so the second run_callback is skipped and the counters are only decremented once.
  • undecodable_row_error takes the JSError cell's pending exception before decode_result? can return a framing error; the cell-cleanup guard still runs on both new return Ok(()) exits.
  • Mock tests: pgBindParameters offsets match the Bind body layout; release() only writes frames that were actually held.
Extended reasoning...

Overview

The PR changes how PostgresSQLConnection::on handles a DataRow whose cell cannot be decoded (src/sql_jsc/postgres/PostgresSQLConnection.rs): instead of propagating the error with ? (which made on_data call fail() and close the socket, rejecting every pipelined query), the cell error is captured in an Option, DataRow::decode finishes consuming the frame, and only the current request is rejected via the new PostgresSQLQuery::on_undecodable_row, which sets a new Flags::discard_response bit while leaving status in flight. is_rejected() (Fail or discard_response) replaces the four status == Fail checks. ReadyForQuery finishes such a request and flips it to Fail so advance() can pop it. error_jsc.rs gains a hint-carrying variant of postgres_error_to_js. Tests add a container-backed file plus mock fault-injection cases, and wire-frames.ts gains pgBindParameters and a hold/release facility on pgMockServer.

Security risks

None specific to this change. The wire parsing itself (DataRow::decode, framing checks) is unchanged; framing errors still propagate and fail the connection. The new code only changes which promise is rejected and when the request leaves the queue. No new allocations from untrusted lengths, and the JSError path consumes the pending exception through the existing take_exception route in postgres_error_to_js_with_hint.

Level of scrutiny

High: this is a request-lifecycle change in a pipelined protocol state machine where a wrong pop order silently hands one query's rows to the next. I traced: (1) advance() and clean_up_requests treat the discard_response request as Running, so it is not popped early and, on connection close, gets finish_request + a no-op second reject (target already cleared by get_target(.., true)); (2) ErrorResponse arriving after the local rejection calls finish_request (decrements counters, sets counter to None) and on_js_error (no-op reject), and the later ReadyForQuery discard branch calls finish_request again on a now-Fail request, which is a no-op — counters are balanced once; (3) simple multi-statement queries in PartialResponse take the discard branch before the PartialResponse branch, so on_result(is_last) is not fired for an already-rejected query; (4) the on_js_error refactor moves status.set(Fail) before ref_guard() acquisition, which is a plain Cell write and cannot run JS, so it is behavior-preserving; (5) the scopeguard::defer! cell cleanup is declared before both new early returns, and putter.count is read after decode returns as before.

Other factors

The mock tests use an in-process net server, port: 0, describe.concurrent, and await using/finally for cleanup; the pgBindParameters parser reads portal, statement, format-code count, and parameter count in the documented Bind order. The container tests exercise the real binary int4[] and text-bounds decoders. I could not run the suite in this environment (no debug build and test execution was unavailable), so correctness of the test expectations rests on reading; the PR author reports all six new tests fail without the fix. The earlier duplicated startup-loop nit was addressed in the last commit by moving hold/release into pgMockServer. The MySQL sibling defect is explicitly deferred to a paired PR, which the description names.

…ecode error tests

The cell loop of the DataRow handler is the same as before again: two
monomorphized DataRow::decode calls, and a failed cell is marked with
inspect_err. DataRow::decode skips the rest of the row when its
callback fails, so the frame is still consumed whole.

Tests: the undecodable rows now have a cell after the bad one (without
the skip these fail), a row that is wider than the inline cell buffer,
a real-server case where the rest of the rejected query's rows arrives
in later reads while the client walks its queue and collects the query
wrapper, and an INSERT ... RETURNING that is stored although it
rejects. The hint names the workaround, and the docs say the same.

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

Code review found no issues

No high-confidence issues detected in this change.

@Jarred-Sumner
Jarred-Sumner merged commit 6d67b14 into main Sep 24, 2026
11 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the robobun/8073a72b/postgres-row-decode-error-scope branch September 24, 2026 01:30
robobun added a commit that referenced this pull request Sep 29, 2026
… and write nothing past a statement being parsed

The ErrorResponse arm released the counter of the failed request and marked
it Fail before the ReadyForQuery of its Sync. With IS_READY_FOR_QUERY still
set from an earlier pipelined query, the next enqueue popped the failed
request and wrote a Parse. The late ReadyForQuery then let advance() step
over that request and write the Bind of the next one first. Replies go to
the requests in queue order, so each of the two queries resolved with the
rows of the other.

The request now rejects through reject_in_flight, which is the
on_undecodable_row of #43187 with a new name. It keeps its status, its
counter and the head of the queue until its ReadyForQuery. advance() stops
at a request whose statement is being parsed, as it did in #20986.
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