Skip to content

sql(mysql): set result.count and result.command for statements without a result set - #41886

Open
robobun wants to merge 7 commits into
mainfrom
robobun/d2d6aabf/mysql-count-command
Open

robobun wants to merge 7 commits into
mainfrom
robobun/d2d6aabf/mysql-count-command

Conversation

@robobun

@robobun robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • On the MySQL adapter, the result of an INSERT, UPDATE, DELETE or DDL statement had count = 0 and command = null. The PostgreSQL and SQLite adapters fill both (count = rows affected, command = "INSERT"/"UPDATE"/...), so if (result.count === 0) meant something else on MySQL. Only the MySQL-specific affectedRows carried the number.
  • The cause: JSMySQLQuery::resolve (src/sql_jsc/mysql/JSMySQLQuery.rs) always built CommandTag::Select(result_count), where result_count is the number of rows read (0 for a reply with no result set), and onResolveMySQLQuery (src/js/internal/sql/mysql.ts) ignored the tag and never set result.command.

Fix

  • handle_result_set_ok passes count = affected_rows when the OK packet answers a statement with no result set (no column-count header was received, HEADER_RECEIVED unset), and the row count when it terminates a result set. affectedRows and lastInsertRowid are unchanged.
  • command is the leading keyword of the statement, upper-cased. The MySQL protocol sends no command tag, so it comes from the query text, as on the SQLite adapter: a small scanner (src/sql/mysql/StatementKeyword.rs, generic over Latin-1/UTF-16 so the query string is not transcoded) skips whitespace, comments and ( (MySQL rules: -- needs a following space, /*! ... */ is code), and for a multi-statement query steps to the next ;-separated statement per result, without splitting on ; inside quotes or comments (NO_BACKSLASH_ESCAPES is read from the OK packet's status flags). Sixteen common keywords reach JS as an index into a table the binding exposes, the rest as a string.
  • Refs Bun.SQL: no documented or typed way to read the affected-row count portably across adapters #40432 (the same UPDATE gives count: 2 on SQLite/PostgreSQL and count: 0 on MySQL). Out of scope: whether UPDATE counts matched or changed rows (CLIENT_FOUND_ROWS, sql(mysql): add foundRows option to opt into CLIENT_FOUND_ROWS #30848). sql: report affectedRows on PostgreSQL and SQLite query results #40434 adds affectedRows to the other adapters plus docs and types for the metadata fields; its wording for the MySQL count/command cells needs to follow whichever of the two lands first.
  • The count change (about 20 lines in MySQLConnection.rs, JSMySQLQuery.rs, mysql.ts) and the command scanner are separable. If only the first is wanted now, say so and I will split the scanner into a follow-up.
  • Verified: test/js/sql/sql-mysql.test.ts "result count and command are set for statements without a result set" (prepared and text protocol, multi-statement with ; in quotes and comments, REPLACE, TRUNCATE, CREATE); fails on bun 1.4.3-canary. Also ran the rest of sql-mysql.test.ts, sql-mysql.transactions.test.ts and sql-mysql.helpers.test.ts against MariaDB 11.8.

Background

  • A MySQL server answers a statement either with a result set (column definitions, rows, then an EOF/OK terminator) or with a bare OK packet that carries affected_rows and last_insert_id. PostgreSQL instead ends every statement with CommandComplete "INSERT 0 3", which is where its command and count come from.
  • Bun.SQL resolves every query to an SQLResultArray whose non-enumerable count, command, lastInsertRowid and affectedRows mirror postgres.js. The MySQL native side calls onResolveMySQLQuery once per result; a multi-statement or CALL query produces several results, accumulated until SERVER_MORE_RESULTS_EXISTS clears.
Notes
  • Before (bun 1.4.3-canary, MariaDB 11.8): INSERT 3 -> count=0 command=null affectedRows=3; UPDATE 3 -> count=0 command=null affectedRows=3; DELETE 1 -> count=0 command=null affectedRows=1. After: count=3 command=INSERT, count=3 command=UPDATE, count=1 command=DELETE; SELECT keeps count = rows.length and gets command=SELECT.
  • CALL p() yields one result per result set plus a final OK; all of them report command: "CALL". When a CALL is followed by more statements in the same text, the results after the procedure's first one are attributed one statement too early. The protocol gives no way to tell a procedure's extra result sets from the next statement's, and command is informational, so this is accepted.
  • A statement whose text has no leading keyword (empty, comment only) reports command: null, as PostgreSQL does for an empty tag.
  • Self-reviewed, and the first bot review's four findings (-- rule, /*! executable comments, NO_BACKSLASH_ESCAPES, one keyword table) are all addressed in cc3ef7f.
  • The scanner only looks at the first word of a single-statement query; the rest of a statement is scanned only when a further result arrives, so large single INSERT texts cost nothing extra.

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 87e54dda-4cab-4852-b7cc-4d575658a5f7

📥 Commits

Reviewing files that changed from the base of the PR and between b52d3e5 and 1e0ca91.

📒 Files selected for processing (10)
  • src/js/internal/sql/mysql.ts
  • src/sql/lib.rs
  • src/sql/mysql/MySQLQueryResult.rs
  • src/sql/mysql/StatementKeyword.rs
  • src/sql/mysql/StatusFlags.rs
  • src/sql_jsc/mysql.rs
  • src/sql_jsc/mysql/JSMySQLQuery.rs
  • src/sql_jsc/mysql/MySQLConnection.rs
  • src/sql_jsc/mysql/MySQLQuery.rs
  • test/js/sql/sql-mysql.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

Changes

The MySQL binding now reports statement commands and accurate result counts. It scans multi-statement SQL while handling comments, quotes, escapes, and executable comments. It also exposes the NO_BACKSLASH_ESCAPES state and updates JavaScript callback types.

MySQL result metadata

Layer / File(s) Summary
Keyword scanning and result contracts
src/sql/mysql/StatementKeyword.rs, src/sql/mysql/MySQLQueryResult.rs, src/sql/mysql/StatusFlags.rs, src/sql/lib.rs
Adds KeywordCursor, the SERVER_STATUS_NO_BACKSLASH_ESCAPES flag, and the count and backslash_escapes result fields.
Result metadata collection and conversion
src/sql_jsc/mysql/MySQLQuery.rs, src/sql_jsc/mysql/MySQLConnection.rs, src/sql_jsc/mysql.rs
Tracks statement keywords, derives counts from result-set headers or OK packets, and exposes common command strings to JavaScript.
JavaScript result API and validation
src/sql_jsc/mysql/JSMySQLQuery.rs, src/js/internal/sql/mysql.ts, test/js/sql/sql-mysql.test.ts
Passes command values and counts through the callback, maps command indexes to strings, updates TypeScript declarations, and tests prepared and text protocols.

Suggested reviewers: alii, jarred-sumner

Merge Risk: ⚪ Minimal · up to 1e0ca

MySQL statements now report affected-row counts and uppercase commands through the public result API, with the changed query forms covered by passing SQL tests. No actionable merge risk remains.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the main change: populating MySQL result.count and result.command for statements without result sets.
Description check ✅ Passed The description provides the problem, implementation details, scope, background, and verification results. It covers the required content, although it uses custom headings instead of the template head…

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

@github-actions github-actions Bot added the claude label Sep 8, 2026
@robobun

robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:10 AM PT - Sep 8th, 2026

❌ @robobun, your commit 1945966 has 1 failures in Build #112954 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 41886

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

bun-41886 --bun

@robobun

robobun commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced on bun 1.4.3-canary against MariaDB 11.8 and PostgreSQL 16: insert ... values (1),(2),(3) gave count=0 command=null affectedRows=3 on MySQL and count=3 command=INSERT on the other two adapters. With this branch MySQL reports count=3 command=INSERT (prepared and text protocol), and the new test in test/js/sql/sql-mysql.test.ts fails before and passes after.

CI (build 112954, head 1945966): the SQL suites pass on every lane. The remaining red is test-crypto-dh-leak.js on x64-asan (also failing on main) plus retried flakes in unrelated files.

@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.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/sql/mysql/StatementKeyword.rs Outdated
Comment thread src/sql/mysql/StatementKeyword.rs Outdated
Comment thread src/sql/mysql/StatementKeyword.rs
Comment thread src/sql_jsc/mysql/MySQLQuery.rs Outdated
…s code, honor NO_BACKSLASH_ESCAPES; expose the keyword table to JS from one place
Comment thread src/js/internal/sql/mysql.ts Outdated
Comment thread src/sql/mysql/MySQLQueryResult.rs Outdated
Comment thread src/sql/mysql/StatementKeyword.rs Outdated
Comment thread src/sql/mysql/StatementKeyword.rs Outdated
Comment thread src/sql/mysql/StatementKeyword.rs Outdated
Comment thread src/sql/mysql/StatementKeyword.rs Outdated
Comment thread src/sql/mysql/StatementKeyword.rs Outdated
Comment thread src/sql/mysql/StatementKeyword.rs Outdated
Comment thread src/sql_jsc/mysql.rs Outdated
Comment thread src/sql_jsc/mysql/JSMySQLQuery.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLConnection.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLQuery.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLQuery.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLQuery.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLQuery.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLQuery.rs Outdated
Comment thread src/sql/mysql/StatementKeyword.rs
Comment thread src/sql/mysql/StatementKeyword.rs
Comment thread src/sql/mysql/StatementKeyword.rs
Comment thread src/sql/mysql/StatementKeyword.rs
Comment thread src/sql_jsc/mysql/MySQLConnection.rs

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/sql_jsc/mysql/JSMySQLQuery.rs
Comment thread src/sql/mysql/MySQLQueryResult.rs
Comment thread src/sql/mysql/StatementKeyword.rs
Comment thread src/sql_jsc/mysql/MySQLConnection.rs

@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.

LGTM — f463c92 addresses the last point: the cursor now caches backslash_escapes per statement and handle_result_set_ok reads the flag from the pre-result status_flags, so statement K-1's body is scanned with the mode it was actually lexed under.

Re-checked the earlier four (-- needs a following space/control/EOF via at(pos+2) <= b' ', /*!//*M! treated as code, NO_BACKSLASH_ESCAPES gating the \ branch, commands derived from COMMON_KEYWORDS at binding init) — all still in place. The scanner's at() bounds-guards every lookahead, command_to_js propagates JsResult and a throw routes to reject before result() runs, and the change is confined to result metadata so a lexer miss on an unmodelled shape (ANSI_QUOTES, extra CALL result sets — both noted in the PR) mislabels command rather than affecting execution.

Extended reasoning...

Overview

The PR makes MySQL query results report command (the statement's leading keyword) and a correct count (affected rows for OK-only replies, row count for result sets), matching the PostgreSQL/SQLite adapters. It adds a small generic lexer (KeywordCursor) over the query text, threads NO_BACKSLASH_ESCAPES through the result path, exposes a shared keyword table from Rust to JS, and drops the borrowed CommandTag::Select hack in JSMySQLQuery. Ten files touched, ~250 net lines, plus a comprehensive test in the existing sql-mysql.test.ts.

Security risks

None material. The lexer reads user SQL text but only to derive an informational string; every index goes through the bounds-checked at() helper and the range returned into keyword_to_js is produced by the same function that walked the slice, so it cannot exceed sql.len(). Heap allocation in keyword_to_js is bounded by the run of consecutive ASCII letters. No new data flows to the server; query execution and row decoding are untouched.

Level of scrutiny

Moderate. The lexer is the only non-mechanical piece and it has been through three review rounds — each finding (-- rule, /*! executable comments, NO_BACKSLASH_ESCAPES, single-source keyword table, escape-mode lag) is now addressed with a targeted fix I re-verified in the current diff. The blast radius of any residual lexer edge case is a wrong result.command string on a multi-statement .simple() query, which is informational; count/affectedRows/rows are unaffected.

Other factors

No CODEOWNERS entry covers src/sql/ or src/sql_jsc/. The new test exercises both prepared and text protocols, ; inside quotes/comments, -- without a trailing space, # comments, /*!40101 ... */, REPLACE, and TRUNCATE, and the PR states it fails on the previous release. Exception propagation follows the repo convention (JsResult returned, error routed to reject_with_js_value with ensure_still_alive on the produced value). Bug hunt exited on dry_streak with no findings.

@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.

Code review found no issues

No high-confidence issues detected in this change.

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.

2 participants