Skip to content

sql(mysql): accept any spelling of ON DUPLICATE KEY UPDATE in the update helper#32040

Open
robobun wants to merge 2 commits into
mainfrom
farm/e95a5336/mysql-upsert-keyword-case
Open

sql(mysql): accept any spelling of ON DUPLICATE KEY UPDATE in the update helper#32040
robobun wants to merge 2 commits into
mainfrom
farm/e95a5336/mysql-upsert-keyword-case

Conversation

@robobun

@robobun robobun commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #32035

Problem

The MySQL adapter's update helper decides whether to append SET by checking the query suffix with a case-sensitive, exact single-space string match:

isUpsertUpdate(query: string): boolean {
  return query.trimEnd().endsWith("ON DUPLICATE KEY UPDATE");
}

SQL keywords are case-insensitive and separated by arbitrary whitespace, and the command detection already tolerates both, so spellings like lowercase or line-wrapped clauses reach the update-helper path but fail the upsert check and get a spurious SET:

await sql`INSERT INTO t ${sql({ id: 1, age: 30 })} on duplicate key update ${sql({ age: 31 })}`;
// generated: INSERT INTO t (`id`, `age`) VALUES(?, ?)  on duplicate key update  SET `age` = ?
// server:    You have an error in your SQL syntax ... near 'SET `age` = ?'

Fix

Match the clause with a case-insensitive regex that allows flexible whitespace in MySQLAdapter.isUpsertUpdate (src/js/internal/sql/mysql.ts):

const upsertSuffixRegExp = /\bON\s+DUPLICATE\s+KEY\s+UPDATE\s*$/i;

The base adapter and sqlite override return false (sqlite and postgres upserts are ON CONFLICT ... DO UPDATE SET where the user writes SET), so mysql was the only affected site.

Note: the fix originally patched the inline suffix check in mysql.ts's normalizeQuery; after #32145 consolidated normalizeQuery into shared.ts, it now lives in the isUpsertUpdate method that refactor introduced.

Tests

  • test/js/sql/sql-mysql-upsert-keyword-case.test.ts (new): mock MySQL server captures the COM_STMT_PREPARE text and asserts the exact generated SQL for uppercase, lowercase, mixed-case, and mixed-whitespace (newline/tab/multi-space) spellings. Runs without Docker. Fails on the unfixed build with the spurious SET visible in the diff.
  • test/js/sql/sql-mysql.helpers.test.ts: lowercase, mixed-case, and line-wrapped upsert variants alongside the existing uppercase one, against the real MySQL service.

Verified with main's src and these tests the mock test fails (all non-exact spellings get SET), and with the fix it passes. The full helpers file (15 tests including the new variants) passes end to end against a local MariaDB 11.8.

@robobun

robobun commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:37 PM PT - Jun 12th, 2026

@robobun, your commit 124d4cfbb51001ae713f5eb73061cd0bd2905ea0 passed in Build #62185! 🎉


🧪   To try this PR locally:

bunx bun-pr 32040

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

bun-32040 --bun

@coderabbitai

coderabbitai Bot commented Jun 10, 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: Pro

Run ID: 1235fd3e-ece0-42e7-9f84-9150f6e794d5

📥 Commits

Reviewing files that changed from the base of the PR and between 4655783 and 124d4cf.

📒 Files selected for processing (3)
  • src/js/internal/sql/mysql.ts
  • test/js/sql/sql-mysql-upsert-keyword-case.test.ts
  • test/js/sql/sql-mysql.helpers.test.ts

Walkthrough

Replace case-sensitive UPSERT suffix detection with an anchored, case-insensitive regex and add protocol-level and functional regression tests that validate normalized prepared SQL and upsert behavior across varied keyword casings and whitespace.

Changes

MySQL UPSERT case-insensitive detection

Layer / File(s) Summary
Case-insensitive UPSERT suffix detection
src/js/internal/sql/mysql.ts
Introduce upsertSuffixRegExp (anchored, case-insensitive) and update isUpsertUpdate(query) to use the regex instead of a case-sensitive endsWith() check.
Protocol-level prepared-statement capture and validation
test/js/sql/sql-mysql-upsert-keyword-case.test.ts
Add a Bun TCP mock MySQL server that performs handshake/auth, parses framed packets, captures SQL from COM_STMT_PREPARE, returns ERR 1064 for prepares, and asserts the client sends expected normalized prepared SQL for multiple casings/whitespace variants.
Functional upsert regression tests
test/js/sql/sql-mysql.helpers.test.ts
Add integration tests exercising INSERT ... ON DUPLICATE KEY UPDATE with lowercase and mixed-case clause spellings (and whitespace/line-break variants), asserting final table rows match expected values.

Suggested reviewers:

  • alii
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately describes the main change: making ON DUPLICATE KEY UPDATE detection case-insensitive in the MySQL update helper.
Description check ✅ Passed Description includes problem statement, fix details, and test coverage. Follows template with clear 'Problem' and 'Fix' sections explaining the case-sensitivity bug and regex solution.
Linked Issues check ✅ Passed All coding requirements from #32035 are met: case-insensitive regex matching implemented in isUpsertUpdate, regression tests added for lowercase/mixed-case upserts in both mock and real MySQL test files.
Out of Scope Changes check ✅ Passed All changes are narrowly scoped to fixing case-insensitive detection of ON DUPLICATE KEY UPDATE in the MySQL adapter and adding corresponding test coverage.

✏️ 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 10, 2026

Copy link
Copy Markdown
Collaborator Author

CI summary for maintainers: the sql changes in this PR have been green on every lane across all three builds. Each red lane had a different unrelated cause:

  • Build 61580: bunx.test.ts failed everywhere because @angular/cli@latest began requiring Node >= 24.15.0 (CI: bunx.test.ts fails on all platforms — @angular/cli@latest now requires Node >= 24.15.0, bun reports 24.3.0 #31797); since skipped on main via Skip bunx.test.ts in CI until the Node.js version bump #32042 and picked up here in the main merge.
  • Build 61614: single lane (Windows 11 aarch64), spawn-stdin-readable-stream.test.ts EPIPE race, also seen flaking on other branches (build 61510). CI was retriggered once for this.
  • Build 61688 (final): debian x64-asan, where fetch-http3-client.test.ts passed all 50 tests then LeakSanitizer reported a 1266-byte lsquic packet leak at exit (lsquic_mm_get_packet_out via bun_http::h3_client); debian x64, where third_party @duckdb/node-api/duckdb.test.ts segfaulted (also flakes on other branches, e.g. build 61545, matching the previously fixed exit-time crash class in napi_env use-after-free in ObjectWrap destructor causes segfault on exit (duckdb repro) #29692); and darwin 26 aarch64, where init.test.ts's "bun init utf-8" snapshot failed on an extra CLAUDE.md scaffold file before the job timed out (init.test.ts passed on every other lane of the same build). Two darwin 14 jobs expired waiting for agents. This branch adds no native changes over main 88d48c2 (the merge only brought the test/expectations.txt bunx skip), and all of these files passed on these same lanes in this branch's earlier builds.

sql-mysql.helpers.test.ts occasionally hits a docker service startup race (container name already in use) and has passed on retry every time. The one allowed retrigger is used; not pushing more. The diff is ready for review.

…update helper

SQL keywords are case-insensitive, but the upsert suffix check in
normalizeQuery was not, so a lowercase "on duplicate key update" got an
extra SET appended and produced invalid SQL.

Fixes #32035
@robobun robobun force-pushed the farm/e95a5336/mysql-upsert-keyword-case branch from c0efca0 to 4655783 Compare June 12, 2026 18:32

@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 `@src/js/internal/sql/mysql.ts`:
- Around line 282-287: The isUpsertUpdate method currently checks the literal
trailing substring which fails when there are multiple or non-space whitespace
characters; replace the exact-slice check with a case-insensitive regular
expression that matches the UPSERT suffix allowing flexible whitespace (e.g.
/\bON\s+DUPLICATE\s+KEY\s+UPDATE\s*$/i) against query.trimEnd(), so any
combination of spaces, tabs or newlines is recognized and prevents
normalizeQuery from appending a stray SET; update the function to use this regex
and keep the existing trimming behavior.
🪄 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: f2ab586f-2f8d-46f7-94ec-528517d8af83

📥 Commits

Reviewing files that changed from the base of the PR and between c0efca0 and 4655783.

📒 Files selected for processing (3)
  • src/js/internal/sql/mysql.ts
  • test/js/sql/sql-mysql-upsert-keyword-case.test.ts
  • test/js/sql/sql-mysql.helpers.test.ts

Comment thread src/js/internal/sql/mysql.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.

LGTM — one-line case-insensitivity fix with solid regression tests; the inline nit is pre-existing and non-blocking.

Extended reasoning...

Overview

The production change is a single method body in src/js/internal/sql/mysql.ts: isUpsertUpdate() now compares the uppercased 23-char tail of the trimmed query against "ON DUPLICATE KEY UPDATE" instead of using a case-sensitive endsWith(). This is a strict superset of the old matcher — uppercase queries match exactly as before, and lowercase/mixed-case spellings now also match. The rest of the diff is test coverage: a new Docker-free mock-server test that asserts the exact normalized SQL for three casings, plus a lowercase/mixed-case variant added next to the existing uppercase upsert test in sql-mysql.helpers.test.ts.

Security risks

None. The string being inspected is the static template-literal text accumulated by normalizeQuery in shared.ts, not user-supplied data, and the only effect of the predicate is whether the helper appends a literal " SET ". No new injection surface, no auth/crypto/permissions involvement.

Level of scrutiny

Low. This is a targeted bugfix for a reported issue (#32035) with an obvious root cause (case-sensitive comparison of case-insensitive SQL keywords). The fix is the minimal correct change, mirrors how detectCommand() already lowercases tokens, and cannot regress the previously-working uppercase path. Edge cases (query shorter than 23 chars after trim) degrade safely since .slice(-23) returns the whole string and the equality fails.

Other factors

  • The one inline finding is explicitly flagged as a pre-existing, non-blocking nit (single-space requirement between keywords) that the old endsWith() shared; it suggests a follow-up regex but doesn't affect correctness of this PR.
  • No CODEOWNERS match the touched paths.
  • CI: robobun reports the sql tests green on every lane across three builds; red lanes are documented unrelated flakes (angular CLI Node version, EPIPE race, lsquic ASAN leak, duckdb segfault, darwin agent expiry).
  • The base/sqlite isUpsertUpdate overrides return false and are untouched, consistent with the PR description that MySQL is the only adapter using this suffix form.

Comment thread src/js/internal/sql/mysql.ts Outdated
SQL separates keywords with arbitrary whitespace, so match the clause
with a case-insensitive regex instead of an exact single-space string.
@robobun robobun changed the title sql(mysql): match ON DUPLICATE KEY UPDATE case-insensitively in the update helper sql(mysql): accept any spelling of ON DUPLICATE KEY UPDATE in the update helper Jun 12, 2026

@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 — the whitespace handling from the earlier review is in, and the remaining tail-slice note is a non-blocking micro-optimization.

Extended reasoning...

Overview

This PR fixes #32035 by replacing a case-sensitive endsWith("ON DUPLICATE KEY UPDATE") check in MySQLAdapter.isUpsertUpdate with a case-insensitive, whitespace-tolerant regex (/\bON\s+DUPLICATE\s+KEY\s+UPDATE\s*$/i), hoisted to a module-level const. The rest of the diff is tests: a new mock-MySQL-server test that captures COM_STMT_PREPARE text and asserts the exact generated SQL across four spellings (no Docker required), plus three new live-DB cases in sql-mysql.helpers.test.ts.

Security risks

None. The change only affects whether the query builder appends a literal " SET " token between user-written keywords and the column-assignment list it already generates. Values remain parameterized as ? placeholders; identifiers are still escaped via escapeIdentifier. The regex is anchored, non-global (no lastIndex state), and runs on a string the builder already constructed — no user-controlled regex input.

Level of scrutiny

Low. The production change is effectively one line, in a leaf method whose only caller is normalizeQuery in shared.ts. The fix strictly widens the set of inputs that take the correct (no-SET) path; any input that matched the old endsWith also matches the new regex, so no regression surface. The mock-server test pins the exact generated SQL byte-for-byte, which is stronger than the prior coverage.

Other factors

  • My earlier inline suggestion (accept flexible whitespace, not just case) and the parallel CodeRabbit comment were both addressed in 124d4cf, with test coverage for the newline/tab/multi-space cases added to both test files.
  • The one new inline nit (regex now scans the full query instead of .slice(-64)) is explicitly non-blocking and self-describes the cost as µs-scale, dwarfed by the DB round-trip and by the O(N) string concatenation that built the query. It is fine to address by either restoring the slice or just updating the stale sentence in the PR description; neither blocks merge.
  • CI failures across the three builds are documented by robobun as unrelated flakes (angular/cli Node version bump, EPIPE race, lsquic ASAN leak, duckdb segfault, darwin init snapshot/timeout); the sql tests themselves were green on every lane.

Comment thread src/js/internal/sql/mysql.ts
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 update helper does not detect lowercase ON DUPLICATE KEY UPDATE

1 participant