Skip to content

sql(mysql): add foundRows option to opt into CLIENT_FOUND_ROWS - #30848

Open
robobun wants to merge 9 commits into
mainfrom
farm/a5a2fbce/mysql-found-rows
Open

robobun wants to merge 9 commits into
mainfrom
farm/a5a2fbce/mysql-found-rows

Conversation

@robobun

@robobun robobun commented May 15, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #30843.

Repro

import { SQL } from "bun";
const sql = new SQL({ adapter: "mysql", /* ... */ });
await sql`create table t (id int primary key, v int not null)`;
await sql`insert into t values (1, 10)`;

// UPDATE matches one row but changes no column value:
const { affectedRows } = await sql`update t set v = v where id = 1`;
// Bun 1.3.14:  affectedRows = 0  ← indistinguishable from "matched no rows"
// mysql2:      affectedRows = 1  (default)
// mariadb:     affectedRows = 1  (default)

Cause

Capabilities::get_default_capabilities in src/sql/mysql/Capabilities.rs
never set CLIENT_FOUND_ROWS, and there was no user-facing option to opt
in. The MySQL server's OK_Packet.affected_rows therefore carried
changed-rows counts, not matched-rows counts.

mysql2 enables CLIENT_FOUND_ROWS by default (opt-out via
flags: ["-FOUND_ROWS"]) and mariadb does the same (opt-out via
foundRows: false). Code migrated from either driver silently lost the
ability to tell "matched but unchanged" from "didn't match" apart.

Fix

Add a foundRows connection option (accepted on the options object and
as a ?foundRows=... URL query param). Default is true, matching the
mysql2 / mariadb defaults. When enabled, the client OR-s
CLIENT_FOUND_ROWS into the capability set it requests during the
HandshakeResponse41, so subsequent affected_rows counts come back with
matched-rows semantics.

new SQL({ adapter: "mysql", /* ... */ });                    // foundRows: true (default)
new SQL({ adapter: "mysql", foundRows: false, /* ... */ });  // server's changed-rows semantics
new SQL("mysql://root@127.0.0.1/test?foundRows=false");       // URL form (options object wins)

The option is threaded through:
parseOptions → createMySQLConnection call → JSMySQLConnection::create_instance → MySQLConnection::init → MySQLConnection.found_rows field → handle_handshake's requested.CLIENT_FOUND_ROWS.

parseOptions also writes foundRows into the DefinedOptions
structure for Postgres/SQLite adapters — they ignore it, so the option
is MySQL-only without any adapter switch in the parser.

Verification

New test: test/js/sql/sql-mysql-found-rows.test.ts uses a minimal mock
MySQL server (no Docker required) to inspect the client's
HandshakeResponse41 capability_flags bit 1 (CLIENT_FOUND_ROWS):

Case Bit 1
default (no foundRows) set ✓
foundRows: true set ✓
foundRows: false cleared ✓
URL ?foundRows=false cleared ✓
URL with duplicate foundRows keys no throw, default kept ✓
options foundRows: true + URL ?foundRows=false set (options win) ✓
server round-trip affectedRows: 1 reaches JS ✓

Fail-before:

$ USE_SYSTEM_BUN=1 bun test test/js/sql/sql-mysql-found-rows.test.ts
 3 pass
 4 fail     ← the four that exercise the new behavior

Pass-after:

$ bun bd test test/js/sql/sql-mysql-found-rows.test.ts
 7 pass
 0 fail

Existing MySQL tests (sql-mysql-auth-short-nonce, 28004,
adapter-env-var-precedence, adapter-override, sql-mysql-cached-error,
sql-mysql-bind-oob, sql-mysql-bind-blob-borrow,
sql-mysql-raw-length-prefix, sql-mysql-columns-realloc-oom,
sql-mysql-clean-reentry) still pass.

Extra commit: build fix

build: JSON-stringify OVERLAY_CSS define value in bake-codegen — wraps
the raw minified CSS in JSON.stringify so the pre-auto-quote bootstrap
bun (1.3.13, used in the CI Dockerfile) can parse define: values that
start with *{...}. Independent of the MySQL change; needed for cold
builds.

Rebase notes

Rebased twice as main moved:

  1. Onto 0b20408b65 (Hardening: input validation and bounds tightening across 26 subsystems #31129), which added allowPublicKeyRetrieval to the
    same MySQL connection plumbing. Resolved by keeping both parameters
    side-by-side: the native createConnection arg list ends with
    ...!prepare, !!allowPublicKeyRetrieval, !!foundRows, and
    JSMySQLConnection.rs reads argument(15) / argument(16).

  2. Onto ac312f0952, which refactored PooledMySQLConnection into a shared
    BasePooledConnection base class: the per-driver option destructuring and
    native createConnection call moved from mysql.ts into
    createPooledConnectionHandle in shared.ts. Resolved by taking main's
    structure and moving the foundRows threading there (destructure
    foundRows = true, pass !!foundRows after !!allowPublicKeyRetrieval),
    following the same trailing-arg pattern allowPublicKeyRetrieval already
    uses (extra trailing args are ignored by the Postgres driver).

  3. Onto 8f8695f4cd. Two conflicts:

    • shared.ts: main expanded the URL query parsing (renamed the loop var and
      added ssl-mode/ssl_mode/ssl/tls branches). Kept main's structure and
      moved the foundrows branch into it, using the same template-literal string
      coercion style as the neighboring branches (still covers the
      duplicate-key-array case).
    • MySQLConnection.rs: main added MariaDB capability negotiation
      (self.mariadb_capabilities = ...) in the same handshake block. Kept both:
      requested.CLIENT_FOUND_ROWS = self.found_rows before the intersect, then
      main's new mariadb_capabilities line.

All 7 sql-mysql-found-rows.test.ts cases pass after each rebase.


[review] gate passed · iteration 10 · 10 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/sql/sql-mysql-found-rows.test.ts
bun test v1.4.0 (717469d7e)

test/js/sql/sql-mysql-found-rows.test.ts:
81 | }
82 | 
83 | describe("Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS)", () => {
84 |   test("default: CLIENT_FOUND_ROWS is enabled (matches mysql2 / mariadb defaults)", async () => {
85 |     const caps = await runHandshakeCase({});
86 |     expect((caps & MYSQL_CLIENT_FOUND_ROWS) !== 0).toBe(true);
                                                        ^
error: expect(received).toBe(expected)

Expected: true
Received: false

      at <anonymous> (/workspace/bun/test/js/sql/sql-mysql-found-rows.test.ts:86:52)
(fail) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > default: CLIENT_FOUND_ROWS is enabled (matches mysql2 / mariadb defaults) [510.49ms]
86 |     expect((caps & MYSQL_CLIENT_FOUND_ROWS) !== 0).toBe(true);
87 |   });
88 | 
89 |   test("foundRows: true enables CLIENT_FOUND_ROWS", async () => {
90 |     const caps = await runHandshakeCase({ foundRows: true } as Bun.SQL.Options);
91 |     expect((caps & MYSQL_CLIENT_FOUN
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (9f51b7d9a)

test/js/sql/sql-mysql-found-rows.test.ts:
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > default: CLIENT_FOUND_ROWS is enabled (matches mysql2 / mariadb defaults) [9.71ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > foundRows: true enables CLIENT_FOUND_ROWS [2.42ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > foundRows: false disables CLIENT_FOUND_ROWS [1.81ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > URL ?foundRows=false disables CLIENT_FOUND_ROWS [2.06ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > URL with duplicate foundRows keys doesn't throw [1.84ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > options object wins over URL query string [1.78ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > affectedRows reflects the server's OK_Packet.affected_rows value [2.00ms]

 7 pass
 0 fail
 7 expect() calls
Ran 7 tests across 1 file. [218.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/sql/sql-mysql-found-rows.test.ts
bun test v1.4.0 (717469d7e)

test/js/sql/sql-mysql-found-rows.test.ts:
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > default: CLIENT_FOUND_ROWS is enabled (matches mysql2 / mariadb defaults) [528.86ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > foundRows: true enables CLIENT_FOUND_ROWS [149.16ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > foundRows: false disables CLIENT_FOUND_ROWS [39.19ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > URL ?foundRows=false disables CLIENT_FOUND_ROWS [56.96ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > URL with duplicate foundRows keys doesn't throw [43.88ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > options object wins over URL query string [41.33ms]
(pass) Bun.SQL MySQL foundRows (CLIENT_FOUND_ROWS) > affectedRows reflects the server's OK_Packet.affected_rows value [42.23ms]

 7 pass
 0 fail
 7 expect() calls
Ran 7 tests across 1 file. [3.85s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 717ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/24] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[2/24] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 240 extern-C blocks audited
[3/24] gen cpp.rs (cppbind)
[4/24] gen JS modules (bundle-modules)
Preprocess modules (8522ms)
Bundle modules (41ms)
Postprocesss modules (220ms)
Bundle Functions (786ms)
Generate Code (31ms)

[9.62s] Bundled "src/js" for production
  2633 kb
  197 internal modules
  13 native modules
  91 internal functions across 17 files
[4/8] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_ast v0.0.0 (/workspace/bun/src/ast)
�[1m�[92m   Compiling�[0m bun_install_types v0.0.0 (/workspace/bun/src/install_types)
�[1m�[92m   Compiling�[0m bun_parsers v0.0.0 (/workspace/bun/src/parsers)
�[1m�[92m   Compiling�[0m bun_react_compiler v0.0.0 (/works
... (truncated)
diff hotspot
docs/runtime/sql.mdx                     |  22 ++++
 packages/bun-types/sql.d.ts              |  17 ++++
 src/codegen/bake-codegen.ts              |   3 +-
 src/js/internal/sql/mysql.ts             |   1 +
 src/js/internal/sql/shared.ts            |  18 +++-
 src/js/private.d.ts                      |   2 +-
 src/sql_jsc/mysql/JSMySQLConnection.rs   |   2 +
 src/sql_jsc/mysql/MySQLConnection.rs     |  12 ++-
 test/js/sql/sql-mysql-found-rows.test.ts | 169 +++++++++++++++++++++++++++++++
 test/js/sql/wire-frames.ts               |  13 ++-
 10 files changed, 251 insertions(+), 8 deletions(-)

gate history · 1 passed · 0 rejected · iteration 10

evidence per changed file
file                                      reads  edits  tests
docs/runtime/sql.mdx                          2      1     25
packages/bun-types/sql.d.ts                   6      4     25
src/codegen/bake-codegen.ts                   5      9     25
src/js/internal/sql/mysql.ts                  8      6     25
src/js/internal/sql/shared.ts                14     17     25
src/js/private.d.ts                           5      5     25
src/sql_jsc/mysql/JSMySQLConnection.rs        8     12     25
src/sql_jsc/mysql/MySQLConnection.rs          9     10     25
test/js/sql/sql-mysql-found-rows.test.ts      2      5     25
test/js/sql/wire-frames.ts                    3      3     25

root cause · written by the author bot

Bun's MySQL client never requested the CLIENT_FOUND_ROWS capability during the protocol handshake, so UPDATE statements reported rows actually changed rather than rows matched, diverging from the default behavior of the mysql2 and mariadb drivers. The fix adds a foundRows connection option, settable via the options object or URL query string and defaulting to true, which is threaded from option parsing through the native binding into the connection's capability negotiation. When enabled, the CLIENT_FOUND_ROWS flag is set in the requested capabilities before intersecting with the server's, s…

@robobun

robobun commented May 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:57 PM PT - Aug 15th, 2026

✅ @robobun, your commit 9f51b7d9ab438c4ff7926e197c502f6db23d8c15 passed in Build #98706! 🎉


🧪   To try this PR locally:

bunx bun-pr 30848

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

bun-30848 --bun

@coderabbitai

coderabbitai Bot commented May 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds a foundRows boolean option to Bun.SQL's MySQL adapter, enabling CLIENT_FOUND_ROWS capability negotiation during handshake. When enabled, affected_rows values match mysql2/mariadb semantics (matched rows). The option defaults to true and can be set via connection options or URL query string.

Changes

MySQL foundRows feature

Layer / File(s) Summary
Public type contract
packages/bun-types/sql.d.ts
Adds foundRows?: boolean | undefined to the public PostgresOrMySQLOptions interface with documentation describing MySQL CLIENT_FOUND_ROWS semantics and default behavior.
Option parsing and normalization
src/js/internal/sql/shared.ts
Parses foundRows from URL query strings (case-insensitive, treating "false"/"0" as disabled) and from the options object, with options-object values overriding URL query strings. Stores normalized boolean in returned options.
Internal type definitions
src/js/private.d.ts
Updates DefinedPostgresOrMySQLOptions to include "foundRows" in the set of keys allowed to be "defined" during type configuration.
MySQL binding (TypeScript/Zig interface)
src/js/internal/sql/mysql.ts
Updates MySQLDotZig.createConnection signature to accept foundRows: boolean, destructures it from pooled connection options with default true, and forwards it to underlying createMySQLConnection call.
MySQL protocol layer (Rust/FFI)
src/sql_jsc/mysql/JSMySQLConnection.rs, src/sql_jsc/mysql/MySQLConnection.rs
Extracts found_rows from JS FFI argument, passes it through MySQLConnection::init, stores it as a connection field, and uses it to control CLIENT_FOUND_ROWS capability negotiation during handshake. Capability negotiation now computes requested flags, conditionally sets CLIENT_FOUND_ROWS based on found_rows, then intersects with server capabilities.
Test coverage for foundRows behavior
test/js/sql/sql-mysql-found-rows.test.ts
Implements a mock MySQL server with wire-protocol helpers and test harness that captures negotiated capability flags. Tests verify: default configuration enables CLIENT_FOUND_ROWS, explicit true/false options, URL query string ?foundRows=false, options-over-URL precedence, and that result.affectedRows forwards the server's OK_Packet.affected_rows value.
Codegen define quoting
src/codegen/bake-codegen.ts
Wraps OVERLAY_CSS define value with JSON.stringify(...) when calling Bun.build, adding comments about Bun's JSON parsing of define values and compatibility with older Bun versions.
Formatting and cfg-attribute rewrites
src/crash_handler/lib.rs, src/errno/lib.rs, src/perf/tracy.rs, src/runtime/..., src/spawn/..., src/spawn_sys/..., src/runtime/jsc_hooks.rs, src/runtime/webview/ChromeProcess.rs
Non-functional reformatting of #[cfg]/#[cfg_attr] attributes and minor expression rewrapping across multiple Rust files; no behavioral changes.

Suggested reviewers:

  • alii
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implementation fully satisfies issue #30843 requirements: adds foundRows option defaulting to true, supports URL query parameters with options precedence, threads capability through handshake, provides comprehensive test coverage, and maintains backward compatibility.
Out of Scope Changes check ✅ Passed All changes are in-scope: MySQL foundRows feature changes, associated TypeScript type updates, and formatting improvements for cfg attributes. The unrelated build fix (bake-codegen OVERLAY_CSS) is explicitly noted as independent.
Title check ✅ Passed The title clearly and concisely describes the main change: adding the MySQL foundRows option for CLIENT_FOUND_ROWS.
Description check ✅ Passed The description explains the problem, implementation, supported configuration, verification steps, test results, and build fix in sufficient detail.

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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't find any bugs, but this changes the default affectedRows semantics for all existing Bun MySQL users (now matched-rows instead of changed-rows) — that's a product decision worth a human sign-off, and there's also a CI build failure on x64-baseline.

Extended reasoning...

Overview

This PR adds a foundRows connection option to Bun.SQL's MySQL adapter that controls the CLIENT_FOUND_ROWS capability flag during handshake, threaded from the public TypeScript API (sql.d.ts, shared.ts, mysql.ts, private.d.ts) through the Rust FFI layer (JSMySQLConnection.rs) into the protocol handshake (MySQLConnection.rs). It ships with a self-contained mock-server test (sql-mysql-found-rows.test.ts). It also bundles two unrelated changes: a JSON.stringify wrap for the OVERLAY_CSS define in bake-codegen.ts (cold-build fix), and ~10 Rust files of pure cargo fmt whitespace from the autofix.ci commit.

Security risks

None identified. The new option is a boolean that toggles a single capability bit; URL query parsing is case-insensitive string comparison with no injection surface; the null-byte / protocol-injection guards on connection strings are unchanged.

Level of scrutiny

Medium-high. The mechanical plumbing is straightforward and the capability-bit change in handle_handshake is minimal and correctly placed before intersect(). However, the key judgment call is that foundRows defaults to true, which silently flips affectedRows semantics for every existing Bun MySQL user from changed-rows to matched-rows. The PR argues this matches mysql2/mariadb defaults (true), but it's still a behavioral break for anyone already on Bun's MySQL adapter — that's a product/compat decision a maintainer should explicitly endorse rather than have auto-approved.

Other factors

  • robobun reports a build failure on x64-baseline - build-rust for the latest commit, which should be resolved or explained before merge.
  • The PR mixes three concerns (feature + unrelated build fix + autofix formatting); fine if that's the project norm, but worth a human glance.
  • Test coverage for the new option is solid (6 cases including URL vs. options precedence and OK_Packet round-trip).

@robobun
robobun force-pushed the farm/a5a2fbce/mysql-found-rows branch from fe16787 to 69a7af4 Compare May 15, 2026 17:54
Comment thread src/js/private.d.ts Outdated
Comment thread src/js/internal/sql/shared.ts Outdated
@robobun
robobun force-pushed the farm/a5a2fbce/mysql-found-rows branch from 6a6c689 to 345720f Compare May 15, 2026 19:01

@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

🤖 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 `@packages/bun-types/sql.d.ts`:
- Around line 383-387: Update the JSDoc for the foundRows option (the comment
block around foundRows in packages/bun-types/sql.d.ts) to remove "MySQL only"
and explicitly state it applies to both MySQL and MariaDB protocols/adapters;
mention that when enabled the driver reports rows matched by the WHERE clause in
affectedRows and that this matches the default behavior of the mysql2 and
mariadb drivers so adapter: "mariadb" is supported.
🪄 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: ce0252cd-d4b6-4f23-a5b5-282ac2d665c5

📥 Commits

Reviewing files that changed from the base of the PR and between 14aa0cf and 345720f.

📒 Files selected for processing (18)
  • packages/bun-types/sql.d.ts
  • src/codegen/bake-codegen.ts
  • src/crash_handler/lib.rs
  • src/errno/lib.rs
  • src/js/internal/sql/mysql.ts
  • src/js/internal/sql/shared.ts
  • src/js/private.d.ts
  • src/perf/tracy.rs
  • src/runtime/cli/Arguments.rs
  • src/runtime/cli/run_command.rs
  • src/runtime/cli/upgrade_command.rs
  • src/runtime/jsc_hooks.rs
  • src/runtime/webview/ChromeProcess.rs
  • src/spawn/process.rs
  • src/spawn_sys/spawn_process.rs
  • src/sql_jsc/mysql/JSMySQLConnection.rs
  • src/sql_jsc/mysql/MySQLConnection.rs
  • test/js/sql/sql-mysql-found-rows.test.ts

Comment thread packages/bun-types/sql.d.ts Outdated
Comment thread packages/bun-types/sql.d.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for addressing all the nits — the code looks good to me. I'm not auto-approving only because this flips the default affectedRows semantics for existing MySQL users (changed-rows → matched-rows), which is a product call worth a maintainer's explicit sign-off; the musl build failures in CI are also worth a glance before merge.

Extended reasoning...

Overview

This PR adds a foundRows connection option to Bun.SQL's MySQL/MariaDB adapter, threading it from parseOptions() (TS) → createMySQLConnection (TS binding) → JSMySQLConnection::create_instance (Rust FFI) → MySQLConnection.found_rows field → handle_handshake's CLIENT_FOUND_ROWS capability bit. It defaults to true, matching the mysql2/mariadb driver defaults. The PR also bundles an unrelated cold-build fix in bake-codegen.ts (wrap OVERLAY_CSS in JSON.stringify) and ~10 Rust files of purely cosmetic #[cfg(...)] attribute reformatting from autofix.ci. A new mock-MySQL-server test file (7 cases) and a docs section in runtime/sql.mdx round it out.

All three of my earlier inline nits (duplicate "max" in the type union, String() coercion for duplicate URL query keys, and the missing sql.mdx docs section) have been addressed and resolved.

Security risks

None identified. The option is a boolean that toggles a single well-defined MySQL capability flag during the handshake; no user-controlled data flows into protocol bytes beyond that. URL query parsing coerces through String() before .toLowerCase(), avoiding the array-method TypeError on duplicate keys.

Level of scrutiny

Moderate-to-high. The implementation is small, well-isolated, and well-tested, but two things push this above the auto-approve bar:

  1. Default behavior change. Setting foundRows: true by default flips affectedRows semantics for every existing Bun MySQL user from changed-rows to matched-rows. Code that relied on affectedRows === 0 to mean "no column values changed" will silently behave differently. The PR description and docs are clear about this and the rationale (ecosystem alignment) is sound, but choosing to ship a default flip vs. an opt-in is a product decision a maintainer should explicitly sign off on.
  2. Wire-protocol path. The Rust change in MySQLConnection::handle_handshake restructures capability negotiation slightly (compute → set bit → intersect). It's correct on inspection, but handshake code is load-bearing.

Other factors

  • CI (Build #54959) shows build failures on three musl targets (x64-musl, x64-musl-baseline, aarch64-musl). These may be infrastructure-related and unrelated to this change, but should be confirmed green before merge.
  • The bundled bake-codegen.ts fix is independent of the MySQL change (acknowledged in the PR description).
  • Test coverage for the new option is thorough (default, explicit true/false, URL form, options-over-URL precedence, duplicate-key URL, and affectedRows round-trip via mock server).

@robobun
robobun force-pushed the farm/a5a2fbce/mysql-found-rows branch 2 times, most recently from 2655518 to 5e65cda Compare May 21, 2026 09:47

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All prior nits are addressed and I didn't find any bugs, but this changes the default affectedRows semantics for existing MySQL users and threads a new arg through the Rust handshake path — worth a human sign-off on the default flip.

Extended reasoning...

Overview

Adds a foundRows connection option (default true) for the MySQL/MariaDB adapter that toggles CLIENT_FOUND_ROWS in the HandshakeResponse41 capability flags. Threaded through parseOptions (shared.ts) → createMySQLConnection (mysql.ts, positional arg 16) → JSMySQLConnection::create_instance (Rust FFI) → MySQLConnection::init → handle_handshake. Also adds public types, docs in runtime/sql.mdx, a 300-line mock-server test, and an unrelated build fix in bake-codegen.ts.

Security risks

None identified. The option only flips a single capability bit during handshake; no auth, crypto, or input-validation surface changes. The URL query-string parser coerces via String() before .toLowerCase() (fixed after my earlier nit), so no new throw paths on adversarial URLs.

Level of scrutiny

Medium-high. While the implementation is clean, well-tested, and all prior review feedback (mine and CodeRabbit's) has been addressed, this is not a purely additive change — it flips the default affectedRows semantics for every existing Bun MySQL user from changed-rows to matched-rows. That's the right call for mysql2/mariadb compatibility, but it's a behavioral default change to a public API that a maintainer should explicitly approve. It also touches the Rust protocol handshake (MySQLConnection.rs:642-655) and adds a positional FFI argument (callframe.argument(16)), which sits adjacent to the recently-rebased allowPublicKeyRetrieval plumbing.

Other factors

  • All four prior inline comments (3 from me, 1 from CodeRabbit) are resolved with follow-up commits.
  • Bundles an unrelated bake-codegen.ts build fix (JSON.stringify wrap for OVERLAY_CSS) — reasonable but worth a glance.
  • Test coverage is solid (mock server inspects the actual capability bit on the wire, plus precedence and duplicate-key cases).

@robobun
robobun force-pushed the farm/a5a2fbce/mysql-found-rows branch from 5e65cda to e97f7e9 Compare June 12, 2026 18:17

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No bugs found and all prior nits are addressed — deferring because this changes the default affectedRows semantics for all existing MySQL users and threads a new param through the JS→Rust FFI handshake path, which is worth a human sign-off.

Extended reasoning...

Overview

This PR adds a foundRows connection option to Bun.SQL's MySQL adapter, mapping to the CLIENT_FOUND_ROWS capability flag. It touches 9 files across the stack: public types (sql.d.ts), prose docs (sql.mdx), JS option parsing (shared.ts, mysql.ts, private.d.ts), Rust FFI/protocol (JSMySQLConnection.rs, MySQLConnection.rs), a 300-line mock-MySQL-server test, and an unrelated build fix in bake-codegen.ts.

Security risks

None identified. The change adds a boolean that toggles a single capability bit during the MySQL handshake; no auth, crypto, or input-validation surface is affected. The URL-query-param parsing is defensive (coerces through String() after my earlier nit was addressed).

Level of scrutiny

Moderate-to-high. Two factors push this above auto-approve threshold:

  1. Default behavior change: foundRows defaults to true, which flips affectedRows semantics for every existing Bun MySQL user from changed-rows to matched-rows. The rationale (matching mysql2/mariadb defaults) is sound and well-documented, but it's still a product decision that could silently affect existing code that branches on affectedRows === 0.
  2. Native FFI threading: A new positional argument is added at index 16 of createMySQLConnection and plumbed through JSMySQLConnection::create_instance → MySQLConnection::init → handle_handshake. The rebase notes describe resolving a conflict with allowPublicKeyRetrieval at index 15; positional FFI arg ordering is exactly the kind of thing a human should eyeball.

Other factors

  • All three of my earlier inline nits (duplicate "max" in the type union, String() coercion for duplicate URL keys, missing sql.mdx docs) and CodeRabbit's MariaDB-wording nit have been addressed and resolved.
  • Test coverage is solid: a Docker-free mock MySQL server asserts the capability bit in all six option/URL/precedence permutations plus a regression test for the duplicate-key case.
  • An unrelated bake-codegen.ts build fix is bundled in; it's small and well-commented but adds to the review surface.
  • No human reviewer has weighed in yet; CodeRabbit suggested alii.

robobun and others added 7 commits August 15, 2026 20:40
mysql2 and mariadb both enable CLIENT_FOUND_ROWS by default, so an
UPDATE that matches a row but does not change any column value returns
affectedRows: 1. Bun.SQL's MySQL adapter had the flag defined in
Capabilities but never set it and had no way to opt in, so the same
query returned affectedRows: 0 — indistinguishable from an UPDATE that
matched nothing. Code migrated from mysql2 / mariadb hit this.

Add a foundRows connection option (default true, matching mysql2 and
mariadb) that OR-s CLIENT_FOUND_ROWS into the client-side capability
set built in handshake negotiation. Accepted as options.foundRows and
as the URL query param ?foundRows=false; options object wins over URL.
For non-MySQL adapters the option is parsed and ignored.

Fixes #30843
Bun.build parses define: values as JSON. The other defines
(side, IS_ERROR_RUNTIME, IS_BUN_DEVELOPMENT) already go through
JSON.stringify / String, but OVERLAY_CSS was passing the raw minified
CSS as-is. That CSS starts with '*{...}'; bun versions that lack the
auto-quote fallback from #30679 reject it with 'Operators are not
allowed in JSON' and abort bake-codegen. The CI Dockerfile bootstraps
from bun 1.3.13, which predates the fix, so cold builds fail. Wrap
the value in JSON.stringify to match the other defines.
node-http-backpressure.test.ts allocates a 2GB buffer and was SIGKILL'd
on one aarch64 shard. Unrelated to the MySQL foundRows diff; every
other test-bun shard on every other platform passed.
…asing

Bun's URLSearchParams.toJSON() returns an Array when a key appears
more than once in the query string, so
`?foundRows=true&foundRows=false` gave us an array and
`.toLowerCase()` threw `TypeError`. Coerce via String() first to
match the sslmode branch's handling — duplicate keys now fall through
to the default instead of crashing the connection path.

Covered by a new test case in sql-mysql-found-rows.test.ts. Spotted
by @claude on review.
…ction

JSDoc had "MySQL only" which could imply `adapter: "mariadb"` is unsupported.
Both adapters route through MySQLAdapter → createMySQLConnection → the same
MySQL wire protocol, so the option works for either.

Also add a short block to docs/runtime/sql.mdx explaining the new default
(matched-rows) and how to opt out via `foundRows: false` or
`?foundRows=false` on the URL.
@robobun
robobun force-pushed the farm/a5a2fbce/mysql-found-rows branch from e97f7e9 to 717469d Compare August 15, 2026 20:45
Comment thread src/codegen/bake-codegen.ts Outdated
Comment thread src/js/internal/sql/shared.ts
Comment thread src/js/internal/sql/shared.ts Outdated
Comment thread src/js/internal/sql/shared.ts Outdated
Comment thread src/js/internal/sql/shared.ts Outdated
Comment thread src/sql_jsc/mysql/JSMySQLConnection.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLConnection.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLConnection.rs Outdated
Comment thread src/codegen/bake-codegen.ts Outdated
Comment thread src/js/internal/sql/shared.ts Outdated
Comment thread src/sql_jsc/mysql/JSMySQLConnection.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLConnection.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/sql_jsc/mysql/JSMySQLConnection.rs:503-512 — 🟡 Nit: the PR adds multi-line narrative comments at 8 sites that the repo's comment-cop bot is already flagging (unresolved inline comments dated 2026-08-15 against HEAD). REVIEW.md's enforced-style section: "Only comment what the code cannot say. One line. Never restate what the code does. Never narrate the change." The 'matches mysql2/mariadb defaults' rationale is repeated 4+ times across the diff — it's already stated once in the JSDoc (sql.d.ts:413-428) and once in docs/runtime/sql.mdx, so each in-code site can drop to one line or nothing.

    Extended reasoning...

    What this is

    The PR adds paragraph-length comments at 8 locations that either restate what the code does, narrate the change, or repeat the same rationale verbatim across multiple sites. The repo's REVIEW.md explicitly enforces a comment style under "Code style & idioms reviewers enforce":

    Only comment what the code cannot say. One line. Never restate what the code does. Never narrate the change. Prefer links to GitHub issues.

    CLAUDE.md #13 reinforces: "If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code." And #14: "After every code comment you write, ask yourself, 'Is this information the next Claude would spend multiple tool calls trying to understand?'. If the answer isn't clearly yes, the code comment is noise — delete it."

    The repo automates this check: the comment-cop github-actions bot has left 8 unresolved inline comments on this PR (dated 2026-08-15, i.e. against current HEAD 717469d), each quoting the CLAUDE.md #13 rule verbatim, at exactly the locations below.

    Affected sites

    Location Lines What it comments
    src/codegen/bake-codegen.ts 59–66 7-line comment justifying JSON.stringify on the OVERLAY_CSS define
    src/js/internal/sql/shared.ts 851–852 2-line doc comment (extended to name foundRows)
    src/js/internal/sql/shared.ts 1802–1805 3-line comment on let foundRows: boolean = true
    src/js/internal/sql/shared.ts 1839–1843 5-line comment inside the foundrows URL branch
    src/js/internal/sql/shared.ts 2019–2021 3-line comment before the options-override block
    src/sql_jsc/mysql/JSMySQLConnection.rs 503–511 9-line comment for a 1-line let found_rows = callframe.argument(16).to_boolean()
    src/sql_jsc/mysql/MySQLConnection.rs 94–96 3-line doc on the found_rows field
    src/sql_jsc/mysql/MySQLConnection.rs 678–682 5-line comment before requested.CLIENT_FOUND_ROWS = self.found_rows

    Why these violate the rule

    Repetition: The phrase "matches the mysql2 / mariadb defaults" (or a close variant) appears at shared.ts:1802, JSMySQLConnection.rs:505, MySQLConnection.rs:96, and MySQLConnection.rs:680 — four in-code sites — plus the JSDoc at packages/bun-types/sql.d.ts:413-428 and docs/runtime/sql.mdx. The JSDoc and docs page are the canonical, discoverable places for this rationale; repeating it at every plumbing hop is exactly the "narrate the change" pattern the rule forbids.

    Restating the code: The 5-line comment at MySQLConnection.rs:678–682 explains that requested.CLIENT_FOUND_ROWS = self.found_rows sets CLIENT_FOUND_ROWS from the foundRows option — the code says that. The 9-line comment at JSMySQLConnection.rs:503–511 spends three lines explaining that callframe.argument(16) returns UNDEFINED when omitted and to_boolean coerces UNDEFINED to false — that's how to_boolean works everywhere in the file (see argument(15) two lines up, which has no such comment).

    Paragraph-length workaround justification: The bake-codegen.ts comment (7 lines) is precisely the shape CLAUDE.md #13 targets: a paragraph explaining why a workaround is OK. The information that matters ("pre-#30679 bootstrap bun rejects unquoted CSS as a define value") fits in one line with an issue link.

    Step-by-step: JSMySQLConnection.rs:503-511

    1. The line being commented is let found_rows = callframe.argument(16).to_boolean(); — a single-token assignment identical in shape to let allow_public_key_retrieval = callframe.argument(15).to_boolean(); two lines above (which has no comment).
    2. The 9-line comment explains: (a) what foundRows: true does semantically, (b) that it matches mysql2/mariadb, (c) how it's applied at handshake, (d) what argument(16) returns for a missing arg, (e) what to_boolean does with UNDEFINED, (f) that mysql.ts always passes an explicit boolean.
    3. Points (a)–(c) are already in the JSDoc, docs page, and MySQLConnection.rs field/handshake comments. Points (d)–(e) describe standard CallFrame/JSValue semantics used throughout the file. Point (f) is change narration.
    4. Per REVIEW.md, none of this is "what the code cannot say" — the code plus the JSDoc is self-explanatory.

    Impact

    None at runtime — this is purely comment hygiene. Filed as nit: it doesn't block on correctness, but (1) REVIEW.md explicitly lists this in its enforced-style section, and (2) the repo's own comment-cop bot has 8 unresolved flags on the PR, so a human reviewer will almost certainly request it.

    Fix

    Trim each site to at most one line (or delete entirely — the JSDoc + docs page carry the rationale). Suggested replacements:

    • bake-codegen.ts:59-66 → // Quote so pre-#30679 bootstrap bun (1.3.13, CI Dockerfile) accepts the raw CSS as a define value.
    • shared.ts:1802-1805 → delete (the JSDoc covers it), or // mysql2/mariadb default; ignored by non-MySQL adapters.
    • shared.ts:1839-1843 → // "false"/"0" disables; coerce first — toJSON() returns an array for duplicate keys.
    • shared.ts:2019-2021 → // options object wins over URL query string (or delete — this is the file's convention for every option)
    • JSMySQLConnection.rs:503-511 → delete (matches the uncommented argument(15) line above)
    • MySQLConnection.rs:94-96 → /// Request CLIENT_FOUND_ROWS during handshake (matched-rows semantics).
    • MySQLConnection.rs:678-682 → delete (the field doc + capability-negotiation comment 10 lines up cover it)

    🔬 also observed by github-actions

Comment thread test/js/sql/sql-mysql-found-rows.test.ts Outdated
Comment thread test/js/sql/sql-mysql-found-rows.test.ts Outdated
- extend wire-frames.ts with MYSQL_CLIENT_FOUND_ROWS, an affectedRows param
  on mysqlOkPacket, and a HandshakeResponse41 client_flag parser; import
  them instead of hand-rolling the byte layout
- read options.foundRows into a local once
  (bun/no-duplicate-conditional-property-access)
- drop restating comments

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed this PR and didn't find any bugs — all earlier nits have been addressed. Because it changes the default affectedRows semantics for existing MySQL users (changed-rows → matched-rows), adds new public API surface (foundRows option + URL param), and bundles an unrelated bake-codegen.ts build fix, a human sign-off on the default/API decision would still be worthwhile.

What was reviewed:

  • Option threading through parseOptions → createPooledConnectionHandle → JSMySQLConnection (arg 16) → MySQLConnection.found_rows → handle_handshake's requested.CLIENT_FOUND_ROWS before the capability intersect.
  • mysqlOkPacket's new affectedRows param — default (0) is byte-identical to the prior hardcoded payload, so existing wire-frames callers are unaffected.
  • URL ?foundRows= parsing coerces via template literal (handles the duplicate-key array case) and options-object precedence over URL — both covered by tests.
  • Postgres path ignores the extra trailing !!foundRows arg to nativeCreateConnection, consistent with how allowPublicKeyRetrieval already works.
Extended reasoning...

Overview

This PR adds a foundRows boolean connection option to Bun.SQL for MySQL/MariaDB that toggles the CLIENT_FOUND_ROWS capability flag during the protocol handshake. When enabled (the new default), affectedRows reports rows matched by the WHERE clause rather than rows changed — matching the mysql2 and mariadb npm driver defaults. It touches 10 files: option parsing in shared.ts, the MySQL native-call type in mysql.ts, the Rust JSC binding (JSMySQLConnection.rs reads argument(16)), the connection struct and handshake in MySQLConnection.rs, public types in sql.d.ts, internal types in private.d.ts, prose docs in sql.mdx, a new 7-case test file using the shared wire-frames.ts mock helpers, and small additions to wire-frames.ts itself. It also carries an unrelated one-line build fix in bake-codegen.ts (wrapping OVERLAY_CSS in JSON.stringify for bootstrap-bun compatibility).

Security risks

None identified. The option is a boolean that controls one bit in the client's capability-negotiation word; it's coerced with !! before crossing to native, and to_boolean() on the Rust side. The URL query-param path coerces via template-literal string interpolation before comparison (so no method calls on attacker-controlled non-string values). No new parsing of untrusted network bytes; the only wire-protocol change is which capability bits the client requests, which the server intersects anyway.

Level of scrutiny

Medium-high. The implementation itself is mechanically simple — one bool threaded through five layers, one assignment before a capability intersect — and is well-covered by a mock-server test that inspects the actual HandshakeResponse41 bytes. However, two aspects push this above the auto-approve bar:

  1. Default behavior change. Existing Bun MySQL users will see affectedRows flip from changed-rows to matched-rows semantics on upgrade. This aligns with the ecosystem (mysql2/mariadb) and is well-documented in this PR, but it's a product decision that a maintainer should consciously accept, not one an automated review should ratify.
  2. New public API surface. The foundRows option name, its true default, and the ?foundRows= URL query-param spelling are all user-facing API commitments. Per the repo's own .claude/docs/landing-prs.md API-design guidance, new API surface warrants maintainer agreement.

Other factors

  • All five prior review rounds (duplicate "max" in the type union, .toLowerCase() on duplicate-key arrays, MariaDB doc wording, missing sql.mdx docs, wire-frames helper reuse, node:net prefix, comment-cop paragraph trims) have been addressed and every thread is resolved.
  • Test coverage is solid: 7 cases including default, explicit true/false, URL param, duplicate URL keys, options-over-URL precedence, and an end-to-end affectedRows round-trip. The PR description shows fail-before with USE_SYSTEM_BUN=1 (4 fail) and pass-after (7 pass).
  • The wire-frames.ts change to mysqlOkPacket keeps the default payload byte-identical (mysqlLenencInt(0) → [0x00]), so 30+ existing callers are unaffected.
  • The bundled bake-codegen.ts build fix is orthogonal to the MySQL change; a maintainer may prefer it split out, but it's a one-line low-risk codegen-script tweak.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun.SQL MySQL does not expose CLIENT_FOUND_ROWS / foundRows, causing affectedRows compatibility issues with mysql2 and mariadb

1 participant