Deduplicate SQL driver internals across postgres/mysql/sqlite - #31994
Deduplicate SQL driver internals across postgres/mysql/sqlite#31994alii wants to merge 10 commits into
Conversation
|
Updated 5:02 PM PT - Jun 10th, 2026
❌ @robobun, your commit 47c520c has some failures in 🧪 To try this PR locally: bunx bun-pr 31994That installs a local version of the PR into your bun-31994 --bun |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
@robobun adopt |
…ice response binding
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
WalkthroughCentralizes 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. ChangesSQL Infrastructure Consolidation
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
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. |
|
@claude review |
There was a problem hiding this comment.
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
📒 Files selected for processing (28)
src/js/internal/sql/mysql.tssrc/js/internal/sql/postgres.tssrc/js/internal/sql/shared.tssrc/js/internal/sql/sqlite.tssrc/sql/lib.rssrc/sql/mysql/Capabilities.rssrc/sql/mysql/protocol/NewReader.rssrc/sql/mysql/protocol/StackReader.rssrc/sql/postgres/protocol/ErrorResponse.rssrc/sql/postgres/protocol/FieldMessage.rssrc/sql/postgres/protocol/NoticeResponse.rssrc/sql/postgres/protocol/StackReader.rssrc/sql/shared/QueryStatus.rssrc/sql/shared/StackReader.rssrc/sql/shared/StatementStatus.rssrc/sql_jsc/lib.rssrc/sql_jsc/mysql/JSMySQLConnection.rssrc/sql_jsc/mysql/JSMySQLQuery.rssrc/sql_jsc/mysql/MySQLStatement.rssrc/sql_jsc/postgres.rssrc/sql_jsc/postgres/PostgresSQLConnection.rssrc/sql_jsc/postgres/PostgresSQLQuery.rssrc/sql_jsc/postgres/PostgresSQLStatement.rssrc/sql_jsc/postgres/protocol/error_response_jsc.rssrc/sql_jsc/postgres/protocol/notice_response_jsc.rssrc/sql_jsc/shared/SQLDataCell.rssrc/sql_jsc/shared/connection_ctor_args.rssrc/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
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.
There was a problem hiding this comment.
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 winPreserve the native handle during pending MySQL connects.
BaseSQLAdapter.#close()now treatspendingconnections as abortable by callingconnection.connection?.close()before awaitingonFinish(src/js/internal/sql/shared.ts, Lines 1164-1177). This path drops the handle returned bycreatePooledConnectionHandle()and only assignsthis.connectionafterhandleConnected()succeeds, soclose()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
📒 Files selected for processing (5)
src/js/internal/sql/mysql.tssrc/js/internal/sql/postgres.tssrc/js/internal/sql/shared.tssrc/sql_jsc/mysql/JSMySQLConnection.rssrc/sql_jsc/postgres/PostgresSQLConnection.rs
There was a problem hiding this comment.
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 winPreserve the native handle during pending MySQL connects.
BaseSQLAdapter.#close()now treatspendingconnections as abortable by callingconnection.connection?.close()before awaitingonFinish(src/js/internal/sql/shared.ts, Lines 1164-1177). This path drops the handle returned bycreatePooledConnectionHandle()and only assignsthis.connectionafterhandleConnected()succeeds, soclose()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
📒 Files selected for processing (5)
src/js/internal/sql/mysql.tssrc/js/internal/sql/postgres.tssrc/js/internal/sql/shared.tssrc/sql_jsc/mysql/JSMySQLConnection.rssrc/sql_jsc/postgres/PostgresSQLConnection.rs
🛑 Comments failed to post (1)
src/sql_jsc/postgres/PostgresSQLConnection.rs (1)
779-782:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon'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. KeepConnectionFailedhere 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
|
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.
mysql |
…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.
…l traits (#32128) ## What this does Smaller, lower-risk replacement for #31994 (closed as too big). Scope is the `src/sql/` Rust protocol crate only — no JS adapters, no `src/sql_jsc/` bindings. Net −686 lines across 12 files. Five independent changes, each its own commit: - **Drop dead `DecoderWrap`/`WriteWrap`** (200fcea, a0e184e) — postgres protocol trait + struct with zero implementors and zero callers anywhere in `src/` (vestigial Zig-port wrappers). Removed both files plus their re-exports, the three unreferenced `_DecoderWrapTarget` aliases, and the stale comment in `CommandComplete.rs` that pointed at the deleted file. - **Collapse mysql `Capabilities` 5× flag list into one macro** (8c1e6c7) — the same 32 `(name, bit)` pairs were hand-maintained five times (struct fields, private bit consts, `to_int` if-chain, `from_int` literal, `Display` emit list). One `capabilities! { NAME = bit, ... }` macro now generates all of it. `to_int` becomes a branchless `(bool as u32) << bit` OR-fold; `from_int`/`Display` expand to byte-identical code. `reject`/`intersect`/`get_default_capabilities` are unchanged in a separate `impl`. - **Generate mysql `CharacterSet` consts + `label()` from one list** (0fcb0b8) — 223 `pub const NAME = CharacterSet(N)` lines and a separate 99-arm `label()` match re-pairing the same `N => name`. One `character_sets! { name = id, ... }` macro now emits both from a single list. `DEFAULT` alias and `Default` impl unchanged. - **Delete dead `mysql_types::CharacterSet` enum** (74c7b24) — the exhaustive 223-variant `#[repr(u8)]` enum in `MySQLTypes.rs` had zero callers; every consumer (`SSLRequest`, `HandshakeV10`, `HandshakeResponse41`) uses the live `protocol::character_set::CharacterSet` newtype instead. The enum's own header comment already noted nothing decodes into it. - **Collapse postgres `Tag` consts + `tag_name()` into one macro** (e3ed505) — 89 `pub const name: Tag = Tag(OID)` declarations plus a parallel 89-arm `tag_name()` match. One `pg_tags! { name = oid, ... }` macro now emits both. `is_binary_format_supported`/`format_code` moved to a separate `impl Tag` unchanged. All 89 `Tag` consts and all 223 `CharacterSet` consts verified preserved by name-set diff against `main`. ## Behavior notes (debug-log only, dead-stripped in release) - `Tag::tag_name()` arms now return `stringify!($name).trim_start_matches("r#")` instead of a direct literal (handles `r#box`). LLVM does not const-fold `trim_start_matches` at `-O`, so each call allocates a small stack `StrSearcher`. The sole caller is `bun_core::scoped_log!(Postgres, ...)` at `src/sql_jsc/postgres/PostgresRequest.rs:154`, which is dead-stripped in release builds. - `CharacterSet::label()` now covers ids 100–250 (previously returned `"(unknown)"` for those, since the old hand-written match only listed ids 1–99 while 223 consts existed). Sole caller is `bun_core::scoped_log!(MySQLConnection, ...)` at `src/sql_jsc/mysql/MySQLConnection.rs:657` — debug log output only. ## Verification - `cargo check -p bun_sql -p bun_sql_jsc`: clean, zero warnings. - `cargo clippy -p bun_sql -p bun_sql_jsc --no-deps`: clean. - `cargo fmt`: no diffs. - Const-table preservation: name-set diff of `Tag` (89), `CharacterSet` (223), and `Capabilities` bit positions (32) against `main` — all identical. - Per-change adversarial perf review: no regressions on any release-reachable path; the two debug-only items above are the only flagged differences. - `bun bd test` on the mock-server SQL tests that exercise the changed tables without DB containers — `postgres-binary-numeric`, `postgres-binary-array-bounds`, `postgres-multi-statement-fields`, `sql-mysql-auth-short-nonce`, `sql-connect-error-reporting`: 39 pass / 0 fail. ## Declined from review - Rewriting `Capabilities` on top of the workspace `bitflags!` crate — would change ~15 field-access sites (`self.CLIENT_SSL` → `self.contains(...)`) and the public bool-field API. Out of scope for a tidy PR. - Replacing `trim_start_matches("r#")` with an optional `as $label` macro arm — extra grammar for one entry on a debug-only path.
…gres and mysql (#32135) ## What this does Second slice carved out of #31994 (closed as too big), stacked on #32128 (merged). This one is the `src/sql_jsc/` Rust constructor-args dedup — no JS adapter changes. Net −58 lines across 20 files. Three commits: - **`src/sql/` prerequisites** (46d3fb8) — `QueryStatus` moves from `mysql/` to `shared/` (re-exported under the old `mysql::query_status` path so nothing breaks); new `shared/StatementStatus` with the same 4 variants both drivers already had; `FieldMessage` gains a `payload()` accessor so the only caller no longer needs an external exhaustive match; `NoticeResponse` becomes a type alias of `ErrorResponse` decoded via `decode_notice_internal` (same wire format, body identical to the old `NoticeResponse::decode_internal`). - **`src/sql_jsc/` dedup** (dc96ffd) — the headline change. Extracts the duplicated `createQuery(...)` and `createConnection(...)` argument parsing/validation prologue from `JSMySQLQuery` / `PostgresSQLQuery` and `JSMySQLConnection` / `PostgresSQLConnection` into `src/sql_jsc/shared/{query_ctor_args,connection_ctor_args}.rs`. Also moves the duplicated `dedupe_columns` from `MySQLStatement` / `PostgresSQLStatement` into `shared/SQLDataCell.rs`, switches both Statement types to the shared `StatementStatus`, and drops the now-unused `notice_response_jsc.rs` (replaced by `FieldMessage::payload()`). - **Test coverage for the rewired NoticeResponse path** (159cea4) — adds `NoticeResponse` and degenerate empty-notice cases to the existing async-message framing test in `test/js/sql/postgres-multi-statement-fields.test.ts`. This path had no prior coverage in `test/js/sql`. ## Behavioral equivalence Line-by-line diff of every removed per-driver block against the new shared helper: - All five `query_ctor_args` error messages verbatim (`"query must be a string"`, `"values must be an array"`, `"simple query cannot have parameters"`, `"query is too long"`, `throw_invalid_argument_type("query", "pendingValue", "Array")`); validation order unchanged. - `connection_ctor_args` validation order unchanged; `ssl_mode` decode equivalent for all i32 (negative → Disable, 0–4 → indexed, ≥5 → Disable); `tls` error message verbatim; same per-VM `SSL_CTX*` cache and `as_usockets_for_client_verification()` key; same `SSL_CTX_free` symbol. - `dedupe_columns` iteration order and reserve size identical. `.expect("OOM")` → `.unwrap_or_oom()` routes through `bun_core::handle_oom` per repo convention — same crash on OOM, no observable difference otherwise. - `bun_vm()` vs `sql_vm()`: the latter is `#[inline] fn sql_vm(&self) { self.bun_vm() }`. No user-reachable input produces a different output, error message, validation order, or SSL_CTX cache key. ## Verification - `cargo check -p bun_sql -p bun_sql_jsc` and `cargo clippy --no-deps`: clean, zero warnings. - `cargo fmt`: no diffs. - `bun bd test` mock-server SQL suite (`postgres-binary-numeric`, `postgres-binary-array-bounds`, `postgres-multi-statement-fields`, `sql-mysql-auth-short-nonce`, `sql-connect-error-reporting`): 41 pass / 0 fail (39 prior + 2 new NoticeResponse cases). - No commits on `main` have touched `src/sql_jsc/` since #31994's merge-base, so the per-driver files are a clean checkout with no clobbered work. ## Left for a follow-up The `src/js/internal/sql/shared.ts` JS adapter consolidation from #31994 (the `BasePooledConnection` extraction, +1308/−2273) is intentionally not in this PR — that's the higher-risk piece touching pool/connection lifecycle and the part that conflicted with #32028.
## What this does Third (and last Rust) slice from #31994, on top of #32128 and #32135. Consolidates the per-driver `StackReader` cursor type into `src/sql/shared/StackReader.rs`. Net −29 lines across 6 files. `StackReader` is the cursor-over-borrowed-buffer that both drivers' `NewReader` types wrap on the per-row decode path. The MySQL and Postgres copies were near-identical; the only per-driver bits were the error variant returned on a short read and the `NewReader<C>` wrapper type. Those become two small traits (`ShortRead`, `WrapReader`) that each driver implements in ~10 lines; the per-driver `StackReader.rs` files are now those impls plus a re-export. Also drops two dead items from `mysql/protocol/NewReader.rs`: the `NewReaderOf<C>` type alias (= `NewReader<C>`, zero callers) and `Decode::decode_allocator` (body identical to `decode`, zero callers). ## Behavioral equivalence - MySQL: byte-for-byte semantic match. Old already used `&Cell<usize>`; `skip` negative-count branch via `saturating_sub` is identical to the old explicit clamp. - Postgres: cursor was `&mut usize`; new shared uses `&Cell<usize>` for both. `IntoCursor for &mut usize` is `Cell::from_mut` (a `repr(transparent)` pointer cast), so the existing `&mut consumed`/`&mut offset` call site at `PostgresSQLConnection.rs:982` is unchanged and reads back the same locals after the reader is consumed. - `ShortRead::SHORT_READ` monomorphizes to the literal `AnyMySQLError::ShortRead` / `AnyPostgresError::ShortRead` — identical `Err` values to before. Two strictly-safer deltas on the postgres side, both unreachable on real wire data but the correct direction: - `ensure_length`/`ensure_capacity` use `checked_add` where the old `buffer.len() >= offset + length` could overflow-wrap in release on adversarial declared lengths. - `skip(count > isize::MAX)` clamps to buffer end instead of overflow-wrapping. ## Performance No new dynamic dispatch or boxing — `ShortRead`/`WrapReader`/`IntoCursor` are all generic-bound. `Cell::get/set` on `usize` compiles to plain load/store. The per-row hot path (`DataRow` → `NewReader::int` → `wrapped.read(N)`) is slightly leaner: shared `read` advances the cursor with `offset.set(offset + count)` instead of routing through `skip()` with its redundant bounds re-check. The one added `isize::try_from().unwrap_or()` in the postgres `skip` bridge is not on the row path (`skip` is called only from `FieldDescription` once per column definition and `Authentication` once per connection). ## Verification - `cargo check -p bun_sql -p bun_sql_jsc` and `cargo clippy --no-deps`: clean. - `cargo fmt`: no diffs. - Zero callers of `NewReaderOf` / `decode_allocator` via grep. ## Left for a follow-up The `src/js/internal/sql/shared.ts` JS adapter consolidation from #31994 (`BasePooledConnection`, +1308/−2273) — the higher-risk pool/lifecycle piece.
…n in shared.ts (#32145) ## What this does Final slice from #31994 (closed as too big), on top of #32128, #32135, #32141. Consolidates the JS adapters' duplicated pool/connection/query plumbing from `src/js/internal/sql/{postgres,mysql,sqlite}.ts` into `src/js/internal/sql/shared.ts` as a `BasePooledConnection` class hierarchy. Net −995 lines across 4 files. Two commits: - **Cherry-pick from #31994** (1aa3dbb) — checks out `src/js/internal/sql/` from `origin/claude/split/sql` as-is. This intentionally clobbers two pool-lifecycle fixes that landed on `main` since #31994's merge-base: #32041 (throwing onconnect/onclose corrupting the pool) and #32097 (forced `close()` resolving mid-handshake). - **Re-integrate #32041 + #32097 into the new structure** (6e6fd4f) — re-hosts both fixes in `BasePooledConnection`: - #32041: try/finally around the user `onconnect`/`onclose` calls in `handleConnected` and `#finishClose` so a throwing callback never skips pool bookkeeping; the `createPooledConnectionHandle` catch always defers via `process.nextTick` (drops the per-driver `deferSyncCloseError` flag — both drivers now match). - #32097: `startConnection()` is `abstract Promise<void>` and both subclasses await + assign `this.connection` at creation; `#beginConnecting` awaits it and closes the handle if `onFinish` was set (pool force-closed) in the microtask window before it materialized. ## Behavioral equivalence The original `BasePooledConnection` extraction in #31994 carried a per-file behavioral-equivalence audit against its merge-base (placeholders, escaping, helper commands, error message text, pool/transaction semantics all preserved) and was green on full CI build 61383. This PR re-applies that diff and adds the two missing fixes; the load-bearing verification is below. ## Verification - `bun bd`: builds clean. - The three lifecycle test files (`sql-onconnect-onclose-throw.test.ts`, `sql-close-pending-connection.test.ts`, `sql-connect-error-reporting.test.ts`): 26 pass / 0 fail. - **New tests in this PR**: `sql-onconnect-onclose-throw.test.ts` gains a forced-close variant per driver (a throwing `onclose` while the pool is force-closed mid-handshake, against a fake `net` server). The two re-hosted fixes meet in `BasePooledConnection`'s close handler, and that interaction had no coverage. Note these pass against `origin/main`'s `src/js/internal/sql` too (verified): both underlying fixes already live on main per-driver, so versus main this PR is equivalence-preserving by design and no test can fail on one side only. The HEAD~1 stash test below is the proof that the re-integration commit is load-bearing. - **Load-bearing stash test**: built and ran the same three files at HEAD~1 (the cherry-pick before re-integration) — 6 fail (`mysql: forced close() resolves when called before the native handle is stored` timeout; `postgres: pool calls from onclose are safe when connecting fails synchronously` → `reentry threw: TypeError`; both drivers' `throwing onclose still rejects pending queries on connect refused` timeouts). With the re-integration: 26 pass. The diff is required for both fixes. - Full `test/js/sql/` against live `postgres_plain`/`mysql_plain`/`*_tls` containers: 1526 pass / 7 fail / 6 errors locally. Every failure reproduces on `origin/main` in the same environment — see Notes below. ## Notes on local-only test failures (none introduced by this PR) - `sql-mysql-query-string-leak.test.ts` fails locally on a debug+ASAN build for both `origin/main` (314.6 MiB) and this branch (317.2 MiB), within noise of each other; both exceed the 256 MiB ASAN threshold. A `heapStats()` probe of the same workload shows `MySQLQuery 2→1` after GC — wrappers are finalized correctly. The test passes on `main`'s CI (release build); the threshold is tuned for release RSS overhead, not local debug. - `sql.test.ts > query string memory leak test` (postgres): same category. - `sqlite-sql.test.ts > Query Normalization Fuzzing Tests > handles exotic but valid SQL patterns`: 5.5s timeout on debug builds — pre-existing per #31994's original verification notes. - `describeWithContainer` cold-start races: `docker compose up -d --wait` returns non-zero on an already-healthy container (`test/docker/index.ts:157`), causing beforeEach hook timeouts in `sql-mysql.test.ts` (TLS), `sql-mysql-bind-oob`, `sql-mysql.helpers`, `sql-mysql.auth`. Pre-existing harness flake; clears on warm rerun. The Rust-side changes from #32097 (`src/sql_jsc/`) were already on `main` before this PR — only the JS hooks needed re-integrating.
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 gainssrc/sql/shared/(StackReader, QueryStatus, StatementStatus) andsrc/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:
NoticeResponseand degenerate empty-notice cases to the mock-server framing tests intest/js/sql/postgres-multi-statement-fields.test.ts. These drive the one protocol path this PR rewires (NoticeResponseis now a type alias ofErrorResponsedecoded viadecode_notice_internal) and had no prior coverage anywhere intest/js/sql. They pass on this branch and on the released bun.test/js/sqlon 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.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.tsandmysql.ts, inside the exact region this PR deduplicates. The merge commit 47c520c resolves the conflict by hosting that machinery once inBasePooledConnectioninshared.ts(connect-cycle budget fields, backoff scheduling inhandleClose,#finishClose,cancelRetry, plus the two adapter hunks: the queue-drain completion check inrelease()and retry cancellation in#close()). The driver-specific piece, which error code marks a retryable connect failure, becomes an abstractisConnectFailureErrorhook implemented per driver, mirroring the existingisNonRetryableErrorpattern.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, andconnectionTimeout: 0) pass; fulltest/js/sqlis 518 pass with the same 2 pre-existing sqlite debug-build timeouts as before the merge.