Skip to content

Deduplicate SQL driver internals across postgres/mysql/sqlite#31994

Closed
alii wants to merge 10 commits into
mainfrom
claude/split/sql
Closed

Deduplicate SQL driver internals across postgres/mysql/sqlite#31994
alii wants to merge 10 commits into
mainfrom
claude/split/sql

Conversation

@alii

@alii alii commented Jun 8, 2026

Copy link
Copy Markdown
Member

What this does

Consolidates copy-pasted logic across the SQL drivers: the JS adapters' shared pool/connection/query plumbing moves into src/js/internal/sql/shared.ts; the Rust side gains src/sql/shared/ (StackReader, QueryStatus, StatementStatus) and src/sql_jsc/shared/ (connection/query ctor args, SQLDataCell) replacing per-driver copies. Net −3.7k lines.

Split from #31912 (whole-repo simplification pass; closing that PR in favor of module-scoped splits). This PR only moves and removes code — zero intended behavior change. Verified there by a per-file behavioral-equivalence audit and full CI (green on build 61383); verified here by a standalone full-workspace compile check.

Verification (adoption)

Since this is a behavior-preserving refactor, there is no input that fails before and passes after; the verification is equivalence coverage:

  • Added NoticeResponse and degenerate empty-notice cases to the mock-server framing tests in test/js/sql/postgres-multi-statement-fields.test.ts. These drive the one protocol path this PR rewires (NoticeResponse is now a type alias of ErrorResponse decoded via decode_notice_internal) and had no prior coverage anywhere in test/js/sql. They pass on this branch and on the released bun.
  • Hunk-by-hunk audit of the Rust diff and a full behavioral-equivalence audit of the JS diff against main: no observable differences (placeholders, escaping, helper commands, error message text, pool and transaction semantics all preserved).
  • test/js/sql on a debug ASAN build: 507 pass; the only failures are 2 sqlite fuzz tests that exceed their 5s timeout identically on a main debug build.
  • Live smoke run against real postgres 17 and mariadb 11.8 plus sqlite (helpers, transactions, error codes, RAISE NOTICE, duplicate columns, multi-statement, values mode): output identical to the released bun.

Merge with main (#32028 connect-failure retry)

Main landed #32028 while this PR was open, adding the same ~100 mirrored lines of connect-retry logic to both postgres.ts and mysql.ts, inside the exact region this PR deduplicates. The merge commit 47c520c resolves the conflict by hosting that machinery once in BasePooledConnection in shared.ts (connect-cycle budget fields, backoff scheduling in handleClose, #finishClose, cancelRetry, plus the two adapter hunks: the queue-drain completion check in release() and retry cancellation in #close()). The driver-specific piece, which error code marks a retryable connect failure, becomes an abstract isConnectFailureError hook implemented per driver, mirroring the existing isNonRetryableError pattern.

Verified on the merged branch: all 15 tests in test/js/sql/sql-connect-error-reporting.test.ts (the #32028 suite, mock TCP servers covering retry-while-waiting, retry cancellation during graceful close, onclose-once, and connectionTimeout: 0) pass; full test/js/sql is 518 pass with the same 2 pre-existing sqlite debug-build timeouts as before the merge.

@robobun

robobun commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator
Updated 5:02 PM PT - Jun 10th, 2026

@robobun, your commit 47c520c has some failures in Build #61819 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 31994

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

bun-31994 --bun

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. sql: deduplicate and remove dead code across the MySQL/Postgres drivers #31664 - Both PRs deduplicate shared SQL driver internals (SQLDataCell, connection/query constructor args) and consolidate copy-pasted logic across mysql/postgres drivers in src/sql_jsc/shared/

🤖 Generated with Claude Code

@alii alii marked this pull request as ready for review June 9, 2026 20:18
@alii

alii commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@robobun adopt

@alii alii force-pushed the claude/split/sql branch from 52c0f79 to 0f9bd60 Compare June 9, 2026 20:19
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b490ee98-cb95-4fca-8dba-d084111a2784

📥 Commits

Reviewing files that changed from the base of the PR and between 6bb44ca and 47c520c.

📒 Files selected for processing (5)
  • src/js/internal/sql/mysql.ts
  • src/js/internal/sql/postgres.ts
  • src/js/internal/sql/shared.ts
  • src/sql_jsc/mysql/JSMySQLConnection.rs
  • src/sql_jsc/postgres/PostgresSQLConnection.rs

Walkthrough

Centralizes SQL normalization, pooled-connection bases, and StackReader/statement-status into shared modules; adapters (MySQL/Postgres/SQLite) and JSC bindings now delegate to shared normalization, pooling, and ctor-parsing helpers.

Changes

SQL Infrastructure Consolidation

Layer / File(s) Summary
Shared SQL foundation
src/js/internal/sql/shared.ts, src/sql/shared/StackReader.rs, src/sql/shared/StatementStatus.rs
Adds SQLCommand detection, normalizeQuery/pushBindParam, QueryNormalizationAdapter, BasePooledConnection/BaseSQLAdapter, createPooledConnectionHandle, shared StackReader cursor and StatementStatus enum.
Rust module reorganization & capabilities
src/sql/lib.rs, src/sql/mysql/Capabilities.rs, src/sql/mysql/protocol/NewReader.rs
Rewires shared exports (move query_status/stack_reader), removes mysql::query_status, removes NewReaderOf and Decode.decode_allocator, and replaces Capabilities implementation with a capabilities! macro.
MySQL adapter consolidation
src/js/internal/sql/mysql.ts, src/sql/mysql/protocol/StackReader.rs
MySQL adapter now extends BaseSQLAdapter with PooledMySQLConnection, delegates unsafe-transaction checks to shared logic, adds getHelperCommand/isUpsertUpdate; Rust MySQL protocol re-exports shared StackReader with protocol trait glue.
Postgres adapter consolidation
src/js/internal/sql/postgres.ts, src/sql/postgres/protocol/*
Postgres adapter uses BaseSQLAdapter/PooledPostgresConnection, provides placeholder/bindParam hooks, unsafeTransactionError and delegated unsafe checks; Rust protocol: ErrorResponse.decode_notice_internal, FieldMessage.payload(), NoticeResponse → alias, shared StackReader glue.
SQLite adapter simplification
src/js/internal/sql/sqlite.ts
SQLiteAdapter delegates normalizeQuery to shared.normalizeQuery and provides adapter-specific placeholder/bindParam/getHelperCommand/isUpsertUpdate/throwIfUpdateEmpty hooks.
Shared constructor argument parsing
src/sql_jsc/shared/connection_ctor_args.rs, src/sql_jsc/shared/query_ctor_args.rs, src/sql_jsc/lib.rs
Adds ConnectionCtorArgs (with SSL/TLS guard and ssl_ctx cache) and QueryCtorArgs to centralize JS ctor parsing/validation; wired into sql_jsc lib.
MySQL JSC bindings
src/sql_jsc/mysql/JSMySQLConnection.rs, src/sql_jsc/mysql/JSMySQLQuery.rs, src/sql_jsc/mysql/MySQLStatement.rs
Bindings use shared ConnectionCtorArgs/QueryCtorArgs; MySQLStatement re-exports shared Status and uses dedupe_columns for duplicate-field detection.
Postgres JSC bindings
src/sql_jsc/postgres.rs, src/sql_jsc/postgres/PostgresSQLConnection.rs, src/sql_jsc/postgres/PostgresSQLQuery.rs, src/sql_jsc/postgres/PostgresSQLStatement.rs, src/sql_jsc/postgres/protocol/error_response_jsc.rs
Bindings adopt shared ctor parsers and guard_tls, switch to error_response_jsc path, use FieldMessage.payload() in JS serialization, and use dedupe_columns/shared Status in statements.
Shared deduplication helpers
src/sql_jsc/shared/SQLDataCell.rs
Adds dedupe_columns() to rewrite duplicate ColumnIdentifier entries and return Flags for caller use.
Tests
test/js/sql/postgres-multi-statement-fields.test.ts
Expands async-message cases to include NoticeResponse variants (non-empty and empty) to validate framing and consumption between result sets.

Possibly related PRs

  • oven-sh/bun#32028: Modifies MySQL/Postgres pooled-connection retry/backoff logic — related to pooled-connection base changes.
  • oven-sh/bun#31221: Overlaps on unsafe-transaction validation consolidation in SQL adapters.

Suggested reviewers

  • cirospaciari
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: deduplication of SQL driver internals across postgres/mysql/sqlite, which matches the core objective of consolidating copy-pasted logic across drivers.
Description check ✅ Passed The description comprehensively covers what the PR does, the consolidation strategy (JS shared layer, Rust shared modules), verification approach (behavioral audits, test coverage, live smoke runs), and merge resolution with #32028.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@robobun

robobun commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Adopted. Verified on a debug (ASAN) build: audited the Rust and JS diffs against main for behavioral drift (none found; the PR is a pure move, so the added NoticeResponse framing tests are equivalence guards), ran test/js/sql, and ran a live smoke check of helpers, transactions, error codes, notices, and duplicate columns against real postgres and mariadb servers plus sqlite, matching the released bun.

All review threads are resolved; the three pre-existing defects surfaced during review are tracked in #32035, #32037, and #32038. Latest: resolved the merge conflict with #32028 (connect-failure retry) by hosting the retry machinery once in BasePooledConnection in shared.ts with a per-driver isConnectFailureError hook, instead of the mirrored copies main added to each driver (details in the PR description). All 15 of the #32028 tests pass on the merged branch, and full test/js/sql is 518 pass with the same 2 pre-existing sqlite debug timeouts as before.

@alii

alii commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@claude review

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

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

Inline comments:
In `@src/js/internal/sql/mysql.ts`:
- Around line 268-270: The isUpsertUpdate function currently checks only for the
exact uppercase suffix; change its logic to perform a case-insensitive check
(e.g., normalize query by trimming and lowercasing or use a case-insensitive
regex) and then test for the suffix "on duplicate key update" so variants like
"on duplicate KEY Update" are correctly detected; update isUpsertUpdate to use
that case-insensitive comparison.

In `@src/js/internal/sql/shared.ts`:
- Around line 998-1013: The pool methods isConnected(), flush(), and `#close`()
currently assume each entry in this.connections is non-null and access
connection.state unguarded; mirror the defensive check used in
hasConnectionsAvailable() by verifying the slot isn't an unassigned hole before
reading its state (e.g. ensure connection is truthy and only then check
connection.state !== PooledConnectionState.closed), and apply the same guard in
every re-entrant scan path to avoid throws when onconnect/onclose callbacks
re-enter during startup; update isConnected, flush, and `#close` to skip
null/undefined slots the same way hasConnectionsAvailable does.
- Around line 607-635: handleConnected/handleClose currently invoke user hooks
(connectionInfo.onconnect / connectionInfo.onclose) before updating internal
bookkeeping, so exceptions from those hooks can abort and leave the pool in an
inconsistent state; modify both handleConnected and handleClose to either (1)
move all internal updates (this.storedError, this.flags adjustments, this.state,
this.queryCount reset, and the eventual this.adapter.release call) to occur
before calling the user hook, or (2) wrap the user hook invocation in a
try/catch so that any thrown error is caught and logged/ignored and does not
prevent the subsequent updates and release; ensure references to this.onFinish
and connection?.close are also executed regardless of hook exceptions so adapter
invariants are preserved.
- Around line 1097-1125: The guard `if (timeout)` treats 0 as falsy so
close({timeout: 0}) falls through; change the branch condition to detect
presence of the option instead (e.g., use `if (options?.timeout !== undefined)`
or `if (Object.prototype.hasOwnProperty.call(options, 'timeout'))`) so the code
that validates Number(timeout), handles `timeout === 0` immediate-close, and
sets up the timer/onAllQueriesFinished logic still runs for a provided 0 value;
keep the existing Number(timeout) conversion and NaN/range checks and leave uses
of `this.closed`, `this.hasPendingQueries()`, `this.#close()`,
`Promise.withResolvers`, `timer.unref()` and `this.onAllQueriesFinished`
unchanged.

In `@src/sql_jsc/mysql/JSMySQLConnection.rs`:
- Around line 473-476: The constructor currently calls
ConnectionCtorArgs::<SSLMode>::parse(...) directly, which causes tls: null or
tls: false to be rejected; update the shared parser ConnectionCtorArgs::parse to
treat JS values null and false as "no TLS" (equivalent to absent) when ssl_mode
!= Disable before validating true/object, and also update the sibling Postgres
constructor that uses the same helper so both MySQL and Postgres accept tls:
null/false consistently with the createConnection contract; ensure the parser
returns the same parsed structure for absent/null/false and that callers (e.g.,
the MySQL constructor and the Postgres constructor) handle that result without
special-casing.

In `@src/sql_jsc/shared/SQLDataCell.rs`:
- Around line 403-406: Replace the panic-on-OOM call in the dedupe code: instead
of calling seen_fields.get_or_put(name.slice()).expect("OOM") use the
OOM-handling helper (either .unwrap_or_oom() on the Result or call
bun_core::handle_oom) so allocation failures follow Bun’s controlled-OOM path;
update the expression around seen_fields.get_or_put(name.slice()) to propagate
or convert the AllocError via unwrap_or_oom()/handle_oom() before accessing
.found_existing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 32135b11-d4f1-49b9-a251-ce9a00ed22ea

📥 Commits

Reviewing files that changed from the base of the PR and between 88d48c2 and 99faaa7.

📒 Files selected for processing (28)
  • src/js/internal/sql/mysql.ts
  • src/js/internal/sql/postgres.ts
  • src/js/internal/sql/shared.ts
  • src/js/internal/sql/sqlite.ts
  • src/sql/lib.rs
  • src/sql/mysql/Capabilities.rs
  • src/sql/mysql/protocol/NewReader.rs
  • src/sql/mysql/protocol/StackReader.rs
  • src/sql/postgres/protocol/ErrorResponse.rs
  • src/sql/postgres/protocol/FieldMessage.rs
  • src/sql/postgres/protocol/NoticeResponse.rs
  • src/sql/postgres/protocol/StackReader.rs
  • src/sql/shared/QueryStatus.rs
  • src/sql/shared/StackReader.rs
  • src/sql/shared/StatementStatus.rs
  • src/sql_jsc/lib.rs
  • src/sql_jsc/mysql/JSMySQLConnection.rs
  • src/sql_jsc/mysql/JSMySQLQuery.rs
  • src/sql_jsc/mysql/MySQLStatement.rs
  • src/sql_jsc/postgres.rs
  • src/sql_jsc/postgres/PostgresSQLConnection.rs
  • src/sql_jsc/postgres/PostgresSQLQuery.rs
  • src/sql_jsc/postgres/PostgresSQLStatement.rs
  • src/sql_jsc/postgres/protocol/error_response_jsc.rs
  • src/sql_jsc/postgres/protocol/notice_response_jsc.rs
  • src/sql_jsc/shared/SQLDataCell.rs
  • src/sql_jsc/shared/connection_ctor_args.rs
  • src/sql_jsc/shared/query_ctor_args.rs
💤 Files with no reviewable changes (3)
  • src/sql_jsc/postgres.rs
  • src/sql_jsc/postgres/protocol/notice_response_jsc.rs
  • src/sql/mysql/protocol/NewReader.rs

Comment thread src/js/internal/sql/mysql.ts
Comment thread src/js/internal/sql/shared.ts
Comment thread src/js/internal/sql/shared.ts
Comment thread src/js/internal/sql/shared.ts
Comment thread src/sql_jsc/mysql/JSMySQLConnection.rs
Comment thread src/sql_jsc/shared/SQLDataCell.rs
robobun and others added 4 commits June 9, 2026 18:38
Resolve conflicts with #32028 (connect-failure retry) by moving the retry
machinery into BasePooledConnection in shared.ts with a per-driver
isConnectFailureError hook, replacing the mirrored copies that main added
to postgres.ts and mysql.ts.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

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

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

130-137: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the native handle during pending MySQL connects.

BaseSQLAdapter.#close() now treats pending connections as abortable by calling connection.connection?.close() before awaiting onFinish (src/js/internal/sql/shared.ts, Lines 1164-1177). This path drops the handle returned by createPooledConnectionHandle() and only assigns this.connection after handleConnected() succeeds, so close() cannot cancel an in-flight MySQL dial/handshake and can stall until the native connect timeout fires.

Suggested fix
-  protected startConnection() {
-    createPooledConnectionHandle(
+  protected async startConnection() {
+    this.connection = await createPooledConnectionHandle(
       createMySQLConnection,
       this.connectionInfo,
       this.handleConnected.bind(this),
       this.handleClose.bind(this),
       true,
     );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/internal/sql/mysql.ts` around lines 130 - 137, The current
startConnection calls createPooledConnectionHandle but drops its returned native
handle so BaseSQLAdapter.close cannot abort an in-flight MySQL dial; modify
startConnection (and related logic) to capture and preserve the handle returned
by createPooledConnectionHandle (call to createMySQLConnection) immediately
(e.g., assign to this.connection or this.pendingConnectionHandle) before
handleConnected runs, and ensure handleConnected/handleClose update/replace that
stored handle when the connection fully succeeds or closes so close() can call
connection.connection?.close() to cancel pending connects.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/sql_jsc/postgres/PostgresSQLConnection.rs`:
- Around line 779-782: The on_connect_error method is incorrectly collapsing all
socket/connect failures into AnyPostgresError::ConnectionRefused; update it to
preserve the original connect failure semantics by using
AnyPostgresError::ConnectionFailed (or explicitly propagate the real socket
error) inside on_connect_error when calling handle_socket_failure -> this.fail,
unless you also plumb the real OS/socket error through and map only true
ECONNREFUSED cases to ConnectionRefused; specifically modify
PostgresSQLConnection::on_connect_error (and where the connect error is dropped)
to either pass the actual error through to fail or change the enum variant used
to ConnectionFailed to avoid misclassifying DNS/timeouts/unix-socket-missing
errors.

---

Outside diff comments:
In `@src/js/internal/sql/mysql.ts`:
- Around line 130-137: The current startConnection calls
createPooledConnectionHandle but drops its returned native handle so
BaseSQLAdapter.close cannot abort an in-flight MySQL dial; modify
startConnection (and related logic) to capture and preserve the handle returned
by createPooledConnectionHandle (call to createMySQLConnection) immediately
(e.g., assign to this.connection or this.pendingConnectionHandle) before
handleConnected runs, and ensure handleConnected/handleClose update/replace that
stored handle when the connection fully succeeds or closes so close() can call
connection.connection?.close() to cancel pending connects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b490ee98-cb95-4fca-8dba-d084111a2784

📥 Commits

Reviewing files that changed from the base of the PR and between 6bb44ca and 47c520c.

📒 Files selected for processing (5)
  • src/js/internal/sql/mysql.ts
  • src/js/internal/sql/postgres.ts
  • src/js/internal/sql/shared.ts
  • src/sql_jsc/mysql/JSMySQLConnection.rs
  • src/sql_jsc/postgres/PostgresSQLConnection.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

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/js/internal/sql/mysql.ts (1)

130-137: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the native handle during pending MySQL connects.

BaseSQLAdapter.#close() now treats pending connections as abortable by calling connection.connection?.close() before awaiting onFinish (src/js/internal/sql/shared.ts, Lines 1164-1177). This path drops the handle returned by createPooledConnectionHandle() and only assigns this.connection after handleConnected() succeeds, so close() cannot cancel an in-flight MySQL dial/handshake and can stall until the native connect timeout fires.

Suggested fix
-  protected startConnection() {
-    createPooledConnectionHandle(
+  protected async startConnection() {
+    this.connection = await createPooledConnectionHandle(
       createMySQLConnection,
       this.connectionInfo,
       this.handleConnected.bind(this),
       this.handleClose.bind(this),
       true,
     );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/internal/sql/mysql.ts` around lines 130 - 137, The current
startConnection calls createPooledConnectionHandle but drops its returned native
handle so BaseSQLAdapter.close cannot abort an in-flight MySQL dial; modify
startConnection (and related logic) to capture and preserve the handle returned
by createPooledConnectionHandle (call to createMySQLConnection) immediately
(e.g., assign to this.connection or this.pendingConnectionHandle) before
handleConnected runs, and ensure handleConnected/handleClose update/replace that
stored handle when the connection fully succeeds or closes so close() can call
connection.connection?.close() to cancel pending connects.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/sql_jsc/postgres/PostgresSQLConnection.rs`:
- Around line 779-782: The on_connect_error method is incorrectly collapsing all
socket/connect failures into AnyPostgresError::ConnectionRefused; update it to
preserve the original connect failure semantics by using
AnyPostgresError::ConnectionFailed (or explicitly propagate the real socket
error) inside on_connect_error when calling handle_socket_failure -> this.fail,
unless you also plumb the real OS/socket error through and map only true
ECONNREFUSED cases to ConnectionRefused; specifically modify
PostgresSQLConnection::on_connect_error (and where the connect error is dropped)
to either pass the actual error through to fail or change the enum variant used
to ConnectionFailed to avoid misclassifying DNS/timeouts/unix-socket-missing
errors.

---

Outside diff comments:
In `@src/js/internal/sql/mysql.ts`:
- Around line 130-137: The current startConnection calls
createPooledConnectionHandle but drops its returned native handle so
BaseSQLAdapter.close cannot abort an in-flight MySQL dial; modify
startConnection (and related logic) to capture and preserve the handle returned
by createPooledConnectionHandle (call to createMySQLConnection) immediately
(e.g., assign to this.connection or this.pendingConnectionHandle) before
handleConnected runs, and ensure handleConnected/handleClose update/replace that
stored handle when the connection fully succeeds or closes so close() can call
connection.connection?.close() to cancel pending connects.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b490ee98-cb95-4fca-8dba-d084111a2784

📥 Commits

Reviewing files that changed from the base of the PR and between 6bb44ca and 47c520c.

📒 Files selected for processing (5)
  • src/js/internal/sql/mysql.ts
  • src/js/internal/sql/postgres.ts
  • src/js/internal/sql/shared.ts
  • src/sql_jsc/mysql/JSMySQLConnection.rs
  • src/sql_jsc/postgres/PostgresSQLConnection.rs
🛑 Comments failed to post (1)
src/sql_jsc/postgres/PostgresSQLConnection.rs (1)

779-782: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't collapse every connect-time failure into ConnectionRefused.

Line 781 now reports a specific refusal code, but Line 1352 drops the actual socket error before this method runs. That means DNS failures, timeouts, unreachable hosts, and missing Unix sockets will all be surfaced as ConnectionRefused, which is a behavior change and the wrong JS-visible error contract for many real failures. Keep ConnectionFailed here unless you also plumb the real connect error through and map only true refusal cases.

Suggested minimal fix
-            this.fail(b"Failed to connect", AnyPostgresError::ConnectionRefused);
+            this.fail(b"Failed to connect", AnyPostgresError::ConnectionFailed);

As per coding guidelines, "Platform-specific code: never assume OS/ABI facts are portable - validate errno meanings..."

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

In `@src/sql_jsc/postgres/PostgresSQLConnection.rs` around lines 779 - 782, The
on_connect_error method is incorrectly collapsing all socket/connect failures
into AnyPostgresError::ConnectionRefused; update it to preserve the original
connect failure semantics by using AnyPostgresError::ConnectionFailed (or
explicitly propagate the real socket error) inside on_connect_error when calling
handle_socket_failure -> this.fail, unless you also plumb the real OS/socket
error through and map only true ECONNREFUSED cases to ConnectionRefused;
specifically modify PostgresSQLConnection::on_connect_error (and where the
connect error is dropped) to either pass the actual error through to fail or
change the enum variant used to ConnectionFailed to avoid misclassifying
DNS/timeouts/unix-socket-missing errors.

Source: Coding guidelines

@robobun

robobun commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Checked both findings from the latest review against main (8df5916). Both describe code that is identical on main; neither is a change made by this PR.

on_connect_error using AnyPostgresError::ConnectionRefused: the function is byte-identical between this branch (PostgresSQLConnection.rs:779-783) and main (778-782), and this PR's diff in that file contains zero occurrences of on_connect_error or ConnectionRefused. The variant came from #32027, which deliberately distinguishes refused connections (instant, distinct error) from accepted-then-closed handshake failures (ConnectionFailed); #32028's description documents that split as the intended contract. If the routing of DNS/timeout failures through that callback needs revisiting, that is a question about #32027's merged design, not this refactor.

mysql startConnection not storing the handle during a pending dial: main has the same shape today. On main, #startConnection() (mysql.ts:415-420) drops the createConnection return value, the handle is assigned in #onConnected(err, connection) (mysql.ts:314-319), and #close() already calls connection.connection?.close() in the pending case (mysql.ts:859-870). The shared #close() consolidates that verbatim: for a mid-dial mysql slot connection.connection is null on both sides, so close() waits on onFinish identically. The suggested fix would make close() abort an in-flight mysql dial, which main does not do; that is a behavior change and out of scope for this zero-behavior-change PR.

alii pushed a commit that referenced this pull request Jun 11, 2026
…ection pool (#32041)

Fixes #32037.

### Problem

In the postgres and mysql pooled-connection handlers (`#onConnected` /
`#onClose` in `src/js/internal/sql/postgres.ts` and `mysql.ts`), the
user-provided `onconnect`/`onclose` callbacks ran before the pool
updated its own bookkeeping. A throwing callback aborted the handler
mid-way: `state` stayed `pending`, `storedError` was never recorded,
pending queries were never notified, `onFinish` never ran, and
`release()` never ran. Anything awaiting the pool hung forever:

```ts
const sql = new SQL({
  /* ... */, max: 1,
  onconnect() { throw new Error("boom"); },
});
await sql`SELECT 1`; // never settles
```

Same for `onclose`: pending queries were never failed on a connect
failure, and `sql.end()` never resolved.

### Fix

Run the callback in a `try`/`finally` so the bookkeeping always
completes, at all four sites (postgres/mysql x onconnect/onclose). The
exception still propagates out of the handler afterwards and is reported
as an uncaughtException through the same channel as before; only the
skipped bookkeeping changes. (sqlite.ts already guards its hooks.)

Secondary fix in the same re-entrancy family, also called out in the
issue: postgres's `createConnection` catch invoked `onClose`
synchronously, which could run the user's `onclose` while the adapter
was still filling `this.connections` during pool startup (reachable via
a `password` function that throws). Pool methods that scan that array
without the hole guard `hasConnectionsAvailable()` has (`flush()`,
`isConnected()`, the private `close()`) threw `TypeError: undefined is
not an object` when called from inside the callback. The catch now
defers via `process.nextTick`, exactly like the identical catch in
mysql.ts already did, so the callback never observes a half-filled pool.

### Verification

New `test/js/sql/sql-onconnect-onclose-throw.test.ts`. The
established-connection scenarios (throwing `onconnect`, throwing
`onclose` on `sql.end()`) run against the real docker-compose
`postgres_plain` and `mysql_plain` services via `describeWithContainer`,
like the other sql tests. The connection-refused scenarios use a real
closed port and the synchronous-failure scenario never dials, so those
run without docker. On the unfixed build every scenario fails (the
docker ones verified manually against real postgres and mariadb: the
fixture hangs without the fix and completes with it):

<details>
<summary>before the fix</summary>

```
(fail) postgres: pool calls from onclose are safe when connecting fails synchronously
  expected "reentry ok", got "reentry threw: TypeError"
(fail) postgres: a throwing onconnect callback does not leave the pool stuck [5000.46ms]
  ^ this test timed out after 5000ms.
(fail) mysql: a throwing onconnect callback does not leave the pool stuck [5000.10ms]
  ^ this test timed out after 5000ms.
(fail) postgres: a throwing onclose callback does not hang sql.end() [5000.06ms]
  ^ this test timed out after 5000ms.
(fail) mysql: a throwing onclose callback does not hang sql.end() [5000.06ms]
  ^ this test timed out after 5000ms.
(fail) postgres: a throwing onclose callback still rejects pending queries on connect failure [5000.06ms]
  ^ this test timed out after 5000ms.
(fail) mysql: a throwing onclose callback still rejects pending queries on connect failure [5000.06ms]
  ^ this test timed out after 5000ms.
```

</details>

With the fix all 7 pass, and the issue's original repro scripts against
real postgres and mariadb servers now settle (`query result: [{"x":1}]`,
`sql.end()` resolves) while the callback error still surfaces as an
uncaughtException. `sql-connect-error-reporting.test.ts`,
`sql-mysql-clean-reentry.test.ts` and `sql-mysql-cached-error.test.ts`
still pass.

Note: this predates #31994 (which dedupes these two files into a shared
base class); if that lands first, the same change applies once to its
`shared.ts`.

### Rebase note (after #32028 landed)

#32028 restructured `#onClose`: connect failures now retry with backoff
and the `onclose` callback moved into a new `#finishClose`, which the
retry timer also calls. Resolved by putting the `try`/`finally` around
the hook in `#finishClose` (covering both callers) and keeping
`#onClose`'s retry branch untouched (no user code runs there).
`#onConnected` keeps main's `connectStartedAt = 0` reset inside the
`finally`. #32028 also split refused connections into
`ERR_*_CONNECTION_REFUSED` (fail fast, not retried), so the two
refused-port tests assert that code now.
@alii alii closed this Jun 11, 2026
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.

2 participants