Skip to content

fix(Bun.SQL) fix postgres error handling when pipelining and state reset - #22505

Merged
Jarred-Sumner merged 11 commits into
mainfrom
ciro/fix-postgres-flags
Sep 9, 2025
Merged

Jarred-Sumner merged 11 commits into
mainfrom
ciro/fix-postgres-flags

Conversation

@cirospaciari

@cirospaciari cirospaciari commented Sep 8, 2025 •

Copy link
Copy Markdown
Member

What does this PR do?

Fixes: #22395

How did you verify your code works?

Test

@robobun

robobun commented Sep 8, 2025 •

Copy link
Copy Markdown
Collaborator
Updated 9:18 PM PT - Sep 8th, 2025

❌ @cirospaciari, your commit 085a565 has 2 failures in Build #25483:


🧪   To try this PR locally:

bunx bun-pr 22505

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

bun-22505 --bun

@cirospaciari cirospaciari changed the title WIP ifx(Bun.SQL) fix postgres error handling when pipelining and state reset Sep 8, 2025
@cirospaciari cirospaciari changed the title ifx(Bun.SQL) fix postgres error handling when pipelining and state reset fix(Bun.SQL) fix postgres error handling when pipelining and state reset Sep 8, 2025
@cirospaciari
cirospaciari marked this pull request as ready for review September 8, 2025 21:51
@cirospaciari
cirospaciari requested review from a team and pfgithub and removed request for a team September 8, 2025 21:51
@coderabbitai

coderabbitai Bot commented Sep 8, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Reworks Postgres connection into a statement-centric lifecycle with explicit statuses, centralizes per-request finalization via finishRequest, adds pub fn canPrepareQuery, changes error routing to prefer JS exceptions, adds Sync-based resynchronization for pipelined errors, adjusts error callback behavior, and adds a Docker-gated regression test (duplicated).

Changes

Cohort / File(s) Summary
Postgres driver lifecycle & pipeline
src/sql/postgres/PostgresSQLConnection.zig
Introduced finishRequest to centralize per-request finalization and counter updates; added pub fn canPrepareQuery(...) bool; reworked advance into a statement-centric lifecycle with explicit statuses and readiness gates (canPipeline, canPrepareQuery); consolidated cleanup to finishRequest; unified ReadyForQuery/partial handling; routed write failures through JS-exception-aware onJSError when present; added protocol Sync resynchronization for pipelined-error recovery.
Per-query error callback behavior
src/sql/postgres/PostgresSQLQuery.zig
Updated error values passed to rejection callbacks to prefer JS Error objects: use err.toError() orelse err (used in onJSError/onWriteFail paths).
Postgres error object construction
src/sql/postgres/AnyPostgresError.zig
Reduced initial capacity argument for JSValue.createEmptyObject in createPostgresError from 18 to 0 (no exported API change).
Regression tests (duplicate)
test/js/sql/sql.test.ts
Added a Docker-gated regression test "Minimal reproduction of Bun.SQL PostgreSQL hang bug (#22395)" that exercises an EXCLUDE constraint (23P01) and races a COUNT(*) query against a 200ms timeout; the test block appears twice (duplicate).

Suggested reviewers

  • Jarred-Sumner
  • dylan-conway

Pre-merge checks (3 passed, 2 warnings)

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning In addition to the targeted error handling updates, the changes to AnyPostgresError capacity initialization and the error object conversion in PostgresSQLQuery.zig are unrelated to the connection pipeline recovery objectives. Move the capacity change in AnyPostgresError and the JS error conversion in PostgresSQLQuery to a separate PR or justify their inclusion by linking them to the linked issue’s objectives.
Description Check ⚠️ Warning The description includes the required headings but lacks detail under both sections, providing only a link to the issue and the single word “Test” without explaining the PR’s changes or verification steps. Expand the “What does this PR do?” section with a concise summary of the key code changes and update “How did you verify your code works?” to describe the specific tests or procedures used for validation.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title accurately summarizes the primary change by describing the fix to PostgreSQL error handling during pipelining and state reset, clearly reflecting the main focus of the pull request.
Linked Issues Check ✅ Passed The pull request addresses the primary objective from issue #22395 by implementing error handling and state reset logic in the PostgresSQLConnection lifecycle and including a repro test, ensuring the driver recovers after constraint violation errors.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a2d4b90 and 085a565.

📒 Files selected for processing (1)
  • src/sql/postgres/PostgresSQLQuery.zig (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/sql/postgres/PostgresSQLQuery.zig
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ciro/fix-postgres-flags

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

@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

🧹 Nitpick comments (2)
src/sql/postgres/PostgresSQLConnection.zig (2)

1149-1165: Pipeline bookkeeping during Bind/Execute is sound; add defensive checks

The transition to .binding/.running, setting pipelined flag, and bumping pipelined_requests align with the new flow. Add the same underflow assert pattern here to guard increments/decrements symmetry over time.

 req.status = .binding;
 req.flags.pipelined = true;
-this.pipelined_requests += 1;
+bun.assert(this.flags.use_unnamed_prepared_statements == false);
+this.pipelined_requests += 1;

Also applies to: 1180-1205, 1206-1212


1749-1762: Reject subsequent pipelined requests after an ErrorResponse

After writing Sync (per PostgreSQL protocol), walk your pending request queue and immediately call onError(...) for any pipelined requests sent after the one that failed. The server will discard those commands until it sees Sync/ReadyForQuery, so proactively failing them prevents promises from hanging indefinitely.

Optionally, add an integration test: pipeline two extended queries where the first errors and assert the second is rejected immediately (before ReadyForQuery).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7a19927 and b330c54.

📒 Files selected for processing (2)
  • src/sql/postgres/PostgresSQLConnection.zig (10 hunks)
  • test/js/sql/sql.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
test/**

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place all tests under the test/ directory

Files:

  • test/js/sql/sql.test.ts
test/js/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

Place JavaScript and TypeScript tests under test/js/

Files:

  • test/js/sql/sql.test.ts
test/**/*.{js,ts}

📄 CodeRabbit inference engine (.cursor/rules/writing-tests.mdc)

test/**/*.{js,ts}: Write tests in JavaScript or TypeScript using Bun’s Jest-style APIs (test, describe, expect) and run with bun test
Prefer data-driven tests (e.g., test.each) to reduce boilerplate
Use shared utilities from test/harness.ts where applicable

Files:

  • test/js/sql/sql.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.ts: Name test files *.test.ts and use bun:test
Do not write flaky tests: never wait for arbitrary time; wait for conditions instead
Never hardcode port numbers in tests; use port: 0 to get a random port
When spawning Bun in tests, use bunExe() and bunEnv from harness
Prefer async/await in tests; for a single callback, use Promise.withResolvers()
Do not set explicit test timeouts; rely on Bun’s built-in timeouts
Use tempDir/tempDirWithFiles from harness for temporary files and directories in tests
For large/repetitive strings in tests, prefer Buffer.alloc(count, fill).toString() over "A".repeat(count)
Import common test utilities from harness (e.g., bunExe, bunEnv, tempDirWithFiles, tmpdirSync, platform checks, GC helpers)
In error tests, assert non-zero exit codes for failing processes and use toThrow for synchronous errors
Use describe blocks for grouping, describe.each for parameterized tests, snapshots with toMatchSnapshot, and lifecycle hooks (beforeAll, beforeEach, afterEach); track resources for cleanup in afterEach
Use using/await using with Bun resources (e.g., Bun.listen/connect/spawn/serve) to ensure cleanup in tests

Files:

  • test/js/sql/sql.test.ts
test/js/**

📄 CodeRabbit inference engine (test/CLAUDE.md)

Organize unit tests for specific features under test/js/ by module

Files:

  • test/js/sql/sql.test.ts
test/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.{ts,tsx}: Test files must be placed under test/ and end with .test.ts or .test.tsx
In tests, always use port: 0; do not hardcode ports or use custom random port functions
In tests, use normalizeBunSnapshot when asserting snapshots
Never write tests that merely assert absence of "panic" or "uncaught exception" in output
Avoid shell commands (e.g., find, grep) in tests; use Bun.Glob and built-ins instead
Prefer snapshot tests over exact stdout equality assertions

Files:

  • test/js/sql/sql.test.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format JavaScript/TypeScript files with Prettier (bun run prettier)

Files:

  • test/js/sql/sql.test.ts
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

**/*.zig: Format Zig files with zig-format (bun run zig-format)
In Zig, manage memory carefully with allocators and use defer for cleanup

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
🧬 Code graph analysis (1)
test/js/sql/sql.test.ts (3)
src/js/internal-for-testing.ts (1)
  • SQL (17-17)
src/js/internal/sql/query.ts (1)
  • reject (196-211)
src/js/internal/sql/postgres.ts (2)
  • err (366-395)
  • err (397-426)
🔇 Additional comments (7)
src/sql/postgres/PostgresSQLConnection.zig (7)

932-933: Good call: centralize state accounting via finishRequest()

Switching cleanup to finishRequest() avoids counter skew and keeps one source of truth for decrements. Consider adding asserts in finishRequest() to prevent accidental underflow if it’s ever called twice for the same item (see next comment).


1086-1088: canPrepareQuery guard looks right

Blocking prepares while not ready, while waiting_to_prepare, or while any pipeline is active prevents out-of-sync states.


1129-1144: Write-path error routing via tryTakeException() is correct

Falling back from JS exception to onWriteFail() is the right split and fixes misclassified write failures.


1215-1220: Prepare path + immediate Sync is a solid resync boundary

  • Waiting for pipeline to drain before preparing avoids the prior stuck state.
  • Emitting Sync after writeQuery() guarantees a ReadyForQuery to bracket the parse phase.

Minor: prefer consistent error handling when writeQuery() or Sync writes fail (you already do this—nice).

Also applies to: 1221-1304


1315-1323: Offset jump over running items: verify off-by-one behavior

total_requests_running = pipelined + nonpipelinable is correct; the conditional offset jump keeps scan O(1). Just ensure a request transitioning to .fail/.success mid-loop can’t be skipped indefinitely—your defer cleanup at Lines 1094–1112 covers this.

Would you like me to craft a focused unit test that enqueues N pipelined + 1 pending and asserts the scanner advances correctly after completions?


1458-1461: Finish partial on ReadyForQuery to release counters

Tying finalization to ReadyForQuery for partial_response is correct and prevents the post-error hang.


1773-1774: Standardizing onResult() for CloseComplete/EmptyQueryResponse

Unifying through onResult(..., ready=false) keeps state transitions consistent.

Also applies to: 1789-1790

Comment thread test/js/sql/sql.test.ts Resolved

@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: 0

♻️ Duplicate comments (1)
src/sql/postgres/PostgresSQLConnection.zig (1)

1070-1085: Prevent counter underflow in finishRequest() (add assertions + safe decrement)

nonpipelinable_requests/pipelined_requests are u32; double-finalization or unexpected states will wrap to 2^32−1. Add debug assertions and guard the decrements.

Apply:

 fn finishRequest(this: *@This(), item: *PostgresSQLQuery) void {
     switch (item.status) {
         .running, .binding, .partial_response => {
             if (item.flags.simple) {
-                this.nonpipelinable_requests -= 1;
+                bun.assert(this.nonpipelinable_requests > 0);
+                if (this.nonpipelinable_requests > 0) this.nonpipelinable_requests -= 1;
             } else if (item.flags.pipelined) {
-                this.pipelined_requests -= 1;
+                bun.assert(this.pipelined_requests > 0);
+                if (this.pipelined_requests > 0) this.pipelined_requests -= 1;
             }
         },
         .success, .fail, .pending => {
             if (this.flags.waiting_to_prepare) {
                 this.flags.waiting_to_prepare = false;
             }
         },
     }
 }
🧹 Nitpick comments (2)
src/sql/postgres/PostgresSQLConnection.zig (2)

1086-1088: Make canPrepareQuery receiver const

This doesn’t mutate; take *const to communicate intent and enable calls on const receivers.

-pub fn canPrepareQuery(this: *@This()) bool {
+pub fn canPrepareQuery(this: *const @This()) bool {
     return this.flags.is_ready_for_query and !this.flags.waiting_to_prepare and this.pipelined_requests == 0;
 }

1215-1304: Good: gated prepare with Sync resynchronization fixes pipeline-stuck states

  • canPrepareQuery() blocks new prepares until the pipeline fully drains.
  • Writing Parse/Describe (via writeQuery) followed by Sync, then flipping waiting_to_prepare=true, ensures server-side recovery after errors before proceeding.

One small suggestion: consider logging the prepared_statement_name and pipelined_requests at the decision point to aid field debugging.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b330c54 and ebbc302.

📒 Files selected for processing (1)
  • src/sql/postgres/PostgresSQLConnection.zig (10 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

**/*.zig: Format Zig files with zig-format (bun run zig-format)
In Zig, manage memory carefully with allocators and use defer for cleanup

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
🔇 Additional comments (8)
src/sql/postgres/PostgresSQLConnection.zig (8)

932-933: Good: decrement counters when aborting in-flight requests during cleanup

Calling finishRequest() here prevents leaked pipelined/non-pipelined counters when we tear down mid-flight. Looks correct.


1129-1134: Nice: distinguish JS exceptions from write failures

Using tryTakeException() cleanly routes to onJSError vs onWriteFail, preventing swallowed protocol errors.


1149-1165: Early fail on statement.failed is correct

Short-circuiting to onError() when the statement is already failed avoids spurious writes and keeps queue state consistent.


1180-1213: Correct lifecycle for prepared execution + counter bookkeeping

Setting binding→running, marking req.flags.pipelined = true, and incrementing pipelined_requests ensures ReadyForQuery drives completion. The canPipeline gate is applied at the right time.


1316-1322: Efficient skip over running region

Using global counters to jump past the currently running window avoids O(n) scans per tick. Looks good.


1750-1752: Good: finishRequest() on ErrorResponse avoids pipeline deadlocks

Decrementing counters and then calling onError() ensures we don’t remain “blocked” until the server’s ReadyForQuery arrives.


1764-1765: Uniform result signaling for CloseComplete/EmptyQueryResponse

Deferring finalization until ReadyForQuery via onResult(..., completed=false) aligns with the extended-query flow.

Also applies to: 1779-1781


1456-1461: No action needed: status transitions are correct
onResult(..., false) sets status to .partial_response and onResult(..., true) sets it to .success.

@cirospaciari
cirospaciari marked this pull request as draft September 8, 2025 23:09
@cirospaciari
cirospaciari marked this pull request as ready for review September 9, 2025 00:35

@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: 0

♻️ Duplicate comments (1)
src/sql/postgres/PostgresSQLConnection.zig (1)

1070-1081: Prevent counter underflow in finishRequest() and add diagnostics

Decrementing u32 counters without guards risks wrap-under on double-finalization or misclassified requests. Add debug asserts and trace the transition. This also makes cleanUpRequests() safe.

 fn finishRequest(this: *@This(), item: *PostgresSQLQuery) void {
     switch (item.status) {
         .running, .binding, .partial_response => {
-            if (item.flags.simple) {
-                this.nonpipelinable_requests -= 1;
-            } else if (item.flags.pipelined) {
-                this.pipelined_requests -= 1;
-            }
+            const was_simple = item.flags.simple;
+            const was_pipelined = item.flags.pipelined;
+            if (was_simple) {
+                bun.assertf(this.nonpipelinable_requests > 0,
+                    "finishRequest underflow: nonpipelinable_requests={d} (item={*}, status={s})",
+                    .{ this.nonpipelinable_requests, item, @tagName(item.status) });
+                this.nonpipelinable_requests -= 1;
+            } else if (was_pipelined) {
+                bun.assertf(this.pipelined_requests > 0,
+                    "finishRequest underflow: pipelined_requests={d} (item={*}, status={s})",
+                    .{ this.pipelined_requests, item, @tagName(item.status) });
+                this.pipelined_requests -= 1;
+            }
+            debug("finishRequest: simple={} pipelined={} -> nonpipe={d} pipe={d}",
+                .{ was_simple, was_pipelined, this.nonpipelinable_requests, this.pipelined_requests });
         },
         .success, .fail, .pending => {},
     }
 }
🧹 Nitpick comments (2)
src/sql/postgres/PostgresSQLConnection.zig (2)

1082-1084: Tighten canPrepareQuery() gate with status check

Guard on being connected to avoid accidental prepares during transient states.

-pub fn canPrepareQuery(this: *@This()) bool {
-    return this.flags.is_ready_for_query and !this.flags.waiting_to_prepare and this.pipelined_requests == 0;
+pub fn canPrepareQuery(this: *@This()) bool {
+    return this.status == .connected
+        and this.flags.is_ready_for_query
+        and !this.flags.waiting_to_prepare
+        and this.pipelined_requests == 0;
 }

1312-1322: Queue-walk logic is subtle; add a brief comment to justify the “jump”

The offset jump by pipelined_requests is correct but non-obvious. A short comment will prevent future regressions during maintenance.

-                const total_requests_running = this.pipelined_requests;
+                // Skip over the currently pipelined window; we only consider enqueuing after it.
+                const total_requests_running = this.pipelined_requests;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ebbc302 and 76e13b6.

📒 Files selected for processing (1)
  • src/sql/postgres/PostgresSQLConnection.zig (10 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

**/*.zig: Format Zig files with zig-format (bun run zig-format)
In Zig, manage memory carefully with allocators and use defer for cleanup

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
🔇 Additional comments (9)
src/sql/postgres/PostgresSQLConnection.zig (9)

932-940: Good: centralize counter cleanup via finishRequest() on shutdown paths

Calling finishRequest() here avoids leaking in-flight counters before propagating errors.


1125-1130: Good error classification: JS exception vs write failure

Using tryTakeException() to route onJSError vs onWriteFail improves diagnostics and correctness on synchronous throw paths.


1145-1161: Correct: early-surface statement failures while pending

Short-circuiting pending requests when their statement is already failed prevents futile retries and keeps the queue consistent.


1176-1201: Solid: bind+execute pipeline accounting and gating

Setting binary/result flags, transitioning to .binding, flipping pipelined=true, and incrementing pipelined_requests are in the right order. The canPipeline() check avoids overfilling the buffer.

Also applies to: 1202-1208


1211-1306: Nice: prepare path writes Sync to resynchronize after pipeline errors

Writing Query then Sync, flipping waiting_to_prepare, and deferring flush via flushDataAndResetTimeout() aligns with PG protocol recovery best practices.


1451-1451: Good: clear waiting_to_prepare on ReadyForQuery

Prevents deadlocks where prepares never re-open the pipeline.


1458-1461: Good: finalize partial responses on ReadyForQuery

finishRequest() before signaling completion ensures counters reflect the true terminal state.


1749-1751: Good: decrement counters on ErrorResponse before surfacing error

This keeps pipeline/non-pipeline counts accurate across error paths.


1764-1764: Verify onResult(..., false) semantics for Close/EmptyQuery

These paths now pass false; ensure onResult’s finalization relies on ReadyForQuery to close out the request and that tests cover both message types.

Would you like me to add a focused test asserting no counter leaks and no hangs on CloseComplete/EmptyQueryResponse?

Also applies to: 1779-1779

@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: 0

♻️ Duplicate comments (1)
src/sql/postgres/PostgresSQLConnection.zig (1)

1064-1074: Prevent counter underflow in finishRequest()

Decrementing u32 counters without guards can underflow and wedge pipelining logic. Add debug assertions before each decrement to catch double-finalization in dev builds.

 fn finishRequest(this: *@This(), item: *PostgresSQLQuery) void {
     switch (item.status) {
         .running, .binding, .partial_response => {
             if (item.flags.simple) {
-                this.nonpipelinable_requests -= 1;
+                bun.assert(this.nonpipelinable_requests > 0);
+                this.nonpipelinable_requests -= 1;
             } else if (item.flags.pipelined) {
-                this.pipelined_requests -= 1;
+                bun.assert(this.pipelined_requests > 0);
+                this.pipelined_requests -= 1;
             }
         },
         .success, .fail, .pending => {},
     }
 }
🧹 Nitpick comments (3)
src/sql/postgres/PostgresSQLConnection.zig (3)

1076-1078: canPrepareQuery(): signature and gating look good

Nice use of noalias and clear readiness gates.

Optional: assert the invariant during development that ready implies no outstanding requests:
bun.assert(!this.flags.is_ready_for_query or (this.pipelined_requests == 0 and this.nonpipelinable_requests == 0));


1119-1123: Deduplicate JS-exception vs write-failure routing

The tryTakeException/onWriteFail pattern repeats in several places. Centralize it to reduce drift.

Apply within the selected ranges:

-                        if (this.globalObject.tryTakeException()) |err_| {
-                            req.onJSError(err_, this.globalObject);
-                        } else {
-                            req.onWriteFail(err, this.globalObject, this.getQueriesArray());
-                        }
+                        this.routeWriteError(req, err);

Add this helper near other small fns:

fn routeWriteError(this: *PostgresSQLConnection, req: *PostgresSQLQuery, err: AnyPostgresError) void {
    if (this.globalObject.tryTakeException()) |js_err| {
        req.onJSError(js_err, this.globalObject);
    } else {
        req.onWriteFail(err, this.globalObject, this.getQueriesArray());
    }
}

Also applies to: 1174-1178, 1232-1240, 1263-1268, 1276-1282


1761-1761: Consistent completion signaling for CLOSE/EMPTY

Routing these via onResult (non-final) aligns with the ReadyForQuery-based finalization.

Consider a brief comment where onResult(false) is used to clarify it marks partial_response and that ReadyForQuery will finalize.

Also applies to: 1776-1777

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5456d0d and 562242c.

📒 Files selected for processing (1)
  • src/sql/postgres/PostgresSQLConnection.zig (10 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

**/*.zig: Format Zig files with zig-format (bun run zig-format)
In Zig, manage memory carefully with allocators and use defer for cleanup

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (6)
src/sql/postgres/PostgresSQLConnection.zig (6)

926-934: Good: call finishRequest() during cleanup

Ensures counters stay consistent when we tear down mid-flight requests.


1141-1155: Early-exit on failed statements is correct

Propagating the statement’s error to the request and continuing the scan avoids blocking the queue.


1170-1199: Pipeline bookkeeping and gating

  • Setting binary based on fields and marking requests pipelined before incrementing the counter is right.
  • Gating further pipelining when unnamed or canPipeline() fails prevents unbounded buffering.

Please run a quick stress to confirm counters reconcile under mixed load (N pipelined prepared execs followed by a simple query), i.e., nonpipelinable_requests remains 0 until ReadyForQuery and pipelined_requests drops to 0 at the corresponding ReadyForQuery boundary.

Also applies to: 1310-1318


1448-1456: ReadyForQuery cleanup ordering is right

Clearing waiting_to_prepare and finishing partial before signaling completion avoids stuck counters.


1746-1748: Finish before onError on ErrorResponse

Great: counters and refs are reconciled prior to surfacing the error.


1205-1294: Extended-query batches already end with Flush+Sync in both prepareAndQueryWithSignature and bindAndExecute; no further changes needed.

@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

♻️ Duplicate comments (1)
src/sql/postgres/PostgresSQLConnection.zig (1)

1070-1080: Add underflow guards when decrementing request counters

Double-finalization or mismatched flags can underflow u32 counters and break scheduling. Add debug assertions before decrements.

 fn finishRequest(this: *@This(), item: *PostgresSQLQuery) void {
     switch (item.status) {
         .running, .binding, .partial_response => {
             if (item.flags.simple) {
-                this.nonpipelinable_requests -= 1;
+                bun.assert(this.nonpipelinable_requests > 0);
+                this.nonpipelinable_requests -= 1;
             } else if (item.flags.pipelined) {
-                this.pipelined_requests -= 1;
+                bun.assert(this.pipelined_requests > 0);
+                this.pipelined_requests -= 1;
             }
         },
         .success, .fail, .pending => {},
     }
 }
🧹 Nitpick comments (3)
src/sql/postgres/PostgresSQLConnection.zig (3)

1201-1204: Defensive check before toggling pipelined flag and incrementing counter

Avoids accidental double-increment if the state machine re-enters unexpectedly.

-                                req.status = .binding;
-                                req.flags.pipelined = true;
-                                this.pipelined_requests += 1;
+                                req.status = .binding;
+                                bun.assert(!req.flags.pipelined);
+                                req.flags.pipelined = true;
+                                this.pipelined_requests += 1;

1315-1326: Minor: clarify offset jump logic with a short comment

The skip by pipelined_requests is subtle; a brief note will help future readers.

-                const total_requests_running = this.pipelined_requests;
+                // Jump past the contiguous block of running pipelined requests to find the next schedulable item.
+                const total_requests_running = this.pipelined_requests;

487-491: Optional aggressive pipelining during prepare
Verified that this.flags.waiting_to_prepare and this.pipelined_requests are defined and toggled around query‐prepare/parse, so the new condition is syntactically valid. Apply to keep the pipeline full between parse and ReadyForQuery:

-    if (!this.flags.has_backpressure and this.flags.is_ready_for_query) {
+    if (
+        !this.flags.has_backpressure and
+        (this.flags.is_ready_for_query or (this.pipelined_requests > 0 and !this.flags.waiting_to_prepare))
+    ) {
     // no backpressure yet so pipeline more if possible and flush again
     this.advance();
     this.flushData();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 562242c and 4141614.

📒 Files selected for processing (2)
  • src/sql/postgres/AnyPostgresError.zig (1 hunks)
  • src/sql/postgres/PostgresSQLConnection.zig (11 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/sql/postgres/AnyPostgresError.zig
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

**/*.zig: Format Zig files with zig-format (bun run zig-format)
In Zig, manage memory carefully with allocators and use defer for cleanup

Files:

  • src/sql/postgres/PostgresSQLConnection.zig
🔇 Additional comments (6)
src/sql/postgres/PostgresSQLConnection.zig (6)

932-933: Good: centralizing per-status cleanup via finishRequest() before error propagation

Ensures counters are reconciled before signaling errors.


1083-1085: noalias on canPrepareQuery() is correct and welcome

Signature and gating conditions look right.


1126-1131: Consistent JS-exception-aware error routing is solid

Preferring onJSError when tryTakeException() succeeds avoids masking JS exceptions behind protocol errors.

Also applies to: 1180-1185, 1239-1245, 1269-1275, 1283-1289


1212-1301: Sync after writeQuery() is the right resync for extended-protocol errors

Writing protocol.Sync post-Parse to force ReadyForQuery realigns state after server-side errors during pipelined prepare. Good fix for the hang.


1455-1463: Good: R4Q clears waiting_to_prepare and finishes partial responses before signaling completion

This ordering avoids late counter updates and matches PG readiness semantics.


1767-1768: OK: unify completion via onResult() for CloseComplete/EmptyQueryResponse

Assuming finalization happens on R4Q, this keeps the state machine consistent.

Also applies to: 1783-1784

Comment on lines +1753 to 1755
this.finishRequest(request);
this.updateRef();
request.onError(.{ .protocol = err }, this.globalObject);

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.

💡 Verification agent

🧩 Analysis chain

Finish current request on ErrorResponse before propagating error

Early counter reconciliation plus updateRef prevents leaked keep-alives. Looks correct given R4Q will follow.

To ensure all success paths also reconcile counters, please confirm either finishRequest() is invoked on successful completion or requests transition to partial_response and are finalized on R4Q:


🏁 Script executed:

#!/bin/bash
rg -nC2 'CommandComplete|EmptyQueryResponse|CloseComplete|finishRequest\(' src/sql/postgres

Length of output: 6514


🏁 Script executed:

#!/bin/bash
rg -nC3 'ReadyForQuery' src/sql/postgres/PostgresSQLConnection.zig

Length of output: 378


🏁 Script executed:

#!/bin/bash
nl -ba src/sql/postgres/PostgresSQLConnection.zig | sed -n '1450,1500p'

Length of output: 40


Call finishRequest before updateRef and onResult in all full-response branches
In the .CommandComplete, .CloseComplete, and .EmptyQueryResponse handlers, invoke this.finishRequest(request); immediately before this.updateRef(); and the subsequent request.onResult(…) call so that counters are reconciled on every successful completion.

🤖 Prompt for AI Agents
In src/sql/postgres/PostgresSQLConnection.zig around lines 1753 to 1755, the
handlers for full-response cases (.CommandComplete, .CloseComplete,
.EmptyQueryResponse) must call this.finishRequest(request) immediately before
calling this.updateRef() and request.onResult(…) (or request.onError/onResult
equivalents). Modify those handlers so the order is:
this.finishRequest(request); this.updateRef(); then the request callback,
ensuring finishRequest is invoked on every successful completion branch to
reconcile counters.

@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

Caution

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

⚠️ Outside diff range comments (1)
src/sql/postgres/PostgresSQLConnection.zig (1)

1472-1483: Finish pipelined request counters in .CommandComplete
pipelined_requests never decrements since finishRequest(request) isn’t called; insert it before updateRef:

         .CommandComplete => {
             var request = this.current() orelse return error.ExpectedRequest;
             var cmd: protocol.CommandComplete = undefined;
             try cmd.decodeInternal(Context, reader);
             defer { cmd.deinit(); }
             debug("-> {s}", .{cmd.command_tag.slice()});
-            defer this.updateRef();
-            request.onResult(cmd.command_tag.slice(), this.globalObject, this.js_value, false);
+            this.finishRequest(request);
+            defer this.updateRef();
+            request.onResult(cmd.command_tag.slice(), this.globalObject, this.js_value, false);
         },

Add a focused test issuing two prepared SELECTs back-to-back without awaiting, asserting both resolve and pipelined_requests returns to zero after the final ReadyForQuery.

♻️ Duplicate comments (2)
src/sql/postgres/PostgresSQLConnection.zig (2)

1780-1784: And for EmptyQueryResponse

Treat this as a full-response boundary for the head request and reconcile counters.

Apply:

         .EmptyQueryResponse => {
             try reader.eatMessage(protocol.EmptyQueryResponse);
             var request = this.current() orelse return error.ExpectedRequest;
-            defer this.updateRef();
+            this.finishRequest(request);
+            defer this.updateRef();
             request.onResult("", this.globalObject, this.js_value, false);
         },

1764-1768: Call finishRequest in CloseComplete before updateRef

Add this.finishRequest(request); immediately before defer this.updateRef(); in the .CloseComplete case to properly conclude the request cycle and avoid pending-activity leaks:

@@ src/sql/postgres/PostgresSQLConnection.zig:1763
-            defer this.updateRef();
+            this.finishRequest(request);
+            defer this.updateRef();
🧹 Nitpick comments (1)
src/sql/postgres/PostgresSQLConnection.zig (1)

1830-1833: Nit: debug vs. log helper

Guidelines mention const log = bun.Output.scoped(.<Scope>, false); while this file uses debug = bun.Output.scoped(.Postgres, .visible). If .visible is intentional for this subsystem, ignore. Otherwise consider aligning to the guideline-specified verbosity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4141614 and a2d4b90.

📒 Files selected for processing (2)
  • src/sql/postgres/PostgresSQLConnection.zig (12 hunks)
  • src/sql/postgres/PostgresSQLQuery.zig (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/sql/postgres/PostgresSQLQuery.zig
  • src/sql/postgres/PostgresSQLConnection.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

**/*.zig: Format Zig files with zig-format (bun run zig-format)
In Zig, manage memory carefully with allocators and use defer for cleanup

Files:

  • src/sql/postgres/PostgresSQLQuery.zig
  • src/sql/postgres/PostgresSQLConnection.zig
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (7)
src/sql/postgres/PostgresSQLQuery.zig (1)

118-120: Normalize JS exceptions via toError() when rejecting callbacks — good change

Passing a real Error object improves parity with other error paths and tooling. No further concerns here.

src/sql/postgres/PostgresSQLConnection.zig (6)

329-331: Also normalize connection-close errors to JS Error — good

Using value.toError() orelse value before invoking onclose ensures consistent error shapes for consumers.


487-492: Drain gating on is_ready_for_query looks correct

Only advancing when not backpressured and ready avoids writing into an extended-protocol window incorrectly. Looks good.


932-940: Cleanup path now reconciles counters before rejecting — good

Calling finishRequest(request) in connection teardown prevents counter leaks on mid-flight queries.


1083-1086: noalias canPrepareQuery signature — good

noalias on the receiver is appropriate here and the readiness predicate matches the intent.


1454-1466: Guard against double-finalization on ReadyForQuery
Add a check on request.flags.is_done before calling finishRequest:

             if (this.current()) |request| {
-                if (request.status == .partial_response) {
-                    this.finishRequest(request);
-                    // if is a partial response, just signal that the query is now complete
-                    request.onResult("", this.globalObject, this.js_value, true);
-                }
+                if (request.status == .partial_response) {
+                    if (!request.flags.is_done) {
+                        this.finishRequest(request);
+                    }
+                    // signal completion
+                    request.onResult("", this.globalObject, this.js_value, true);
+                }
             }

Re-run regression #22395 to confirm recovery after a server-side error isn’t broken.


1115-1213: Only Sync emits ReadyForQuery, not Execute
PostgreSQL’s extended protocol only sends ReadyForQuery upon processing a Sync (or at the end of a simple Query cycle); Execute alone does not emit R4Q. Relying on CommandComplete to close each request prevents rows for N+1 from being misattributed to N. Confirm your implementation expects ReadyForQuery only after Sync, not after Execute.

@Jarred-Sumner
Jarred-Sumner merged commit 1e4935c into main Sep 9, 2025
@Jarred-Sumner
Jarred-Sumner deleted the ciro/fix-postgres-flags branch September 9, 2025 04:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bun:sql Postgres Driver Hangs Indefinitely After Expected 23P01 Constraint Violation Error

3 participants