sql(mysql): accept any spelling of ON DUPLICATE KEY UPDATE in the update helper#32040
sql(mysql): accept any spelling of ON DUPLICATE KEY UPDATE in the update helper#32040robobun wants to merge 2 commits into
Conversation
|
Updated 8:37 PM PT - Jun 12th, 2026
✅ @robobun, your commit 124d4cfbb51001ae713f5eb73061cd0bd2905ea0 passed in 🧪 To try this PR locally: bunx bun-pr 32040That installs a local version of the PR into your bun-32040 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
WalkthroughReplace 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. ChangesMySQL UPSERT case-insensitive detection
Suggested reviewers:
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
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:
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
c0efca0 to
4655783
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/js/internal/sql/mysql.tstest/js/sql/sql-mysql-upsert-keyword-case.test.tstest/js/sql/sql-mysql.helpers.test.ts
There was a problem hiding this comment.
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
isUpsertUpdateoverrides returnfalseand are untouched, consistent with the PR description that MySQL is the only adapter using this suffix form.
SQL separates keywords with arbitrary whitespace, so match the clause with a case-insensitive regex instead of an exact single-space string.
There was a problem hiding this comment.
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.
Fixes #32035
Problem
The MySQL adapter's update helper decides whether to append
SETby checking the query suffix with a case-sensitive, exact single-space string match: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:Fix
Match the clause with a case-insensitive regex that allows flexible whitespace in
MySQLAdapter.isUpsertUpdate(src/js/internal/sql/mysql.ts):The base adapter and sqlite override return false (sqlite and postgres upserts are
ON CONFLICT ... DO UPDATE SETwhere the user writesSET), 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
isUpsertUpdatemethod that refactor introduced.Tests
test/js/sql/sql-mysql-upsert-keyword-case.test.ts(new): mock MySQL server captures theCOM_STMT_PREPAREtext 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 spuriousSETvisible 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.