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
alii added a commit that referenced this pull request Jun 11, 2026
…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.
alii added a commit that referenced this pull request Jun 11, 2026
…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.
alii added a commit that referenced this pull request Jun 11, 2026
## 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.
alii added a commit that referenced this pull request Jun 12, 2026
…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.
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