sql: support named parameters in SQLite unsafe() queries - #37109
Conversation
WalkthroughSQLite ChangesSQLite named parameters
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
test/js/sql/sqlite-sql.test.ts:2122-2123— This raises the per-test timeout to 60s on a pre-existing test unrelated to the named-parameter feature, which conflicts withtest/CLAUDE.md("CRITICAL: Do not set a timeout on tests") and REVIEW.md ("Don't raise per-test timeouts to make a slow test pass; shrink the workload"). The idiomatic fix is to loweriterations(e.g. 10000 → ~2000) so the test finishes under the default in debug+ASAN — a 60s ceiling multiplies across retries on a genuine hang and hides future perf regressions in this codepath.Extended reasoning...
What changed
Commit
53173688("test: allow 60s for the 10k-insert statement finalization test") adds a60_000ms per-test timeout to the pre-existing "properly finalizes prepared statements" test intest/js/sql/sqlite-sql.test.ts, with the trailing comment// 10k awaited inserts need more than the 5s default under debug + ASAN. This test predates the PR and is unrelated to the named-parameter feature being implemented — it's a drive-by adjustment to make the file pass under debug+ASAN.Why this conflicts with repo rules
Two repo guidelines cover this exact pattern:
test/CLAUDE.md:120: "CRITICAL: Do not set a timeout on tests. Bun already has timeouts."REVIEW.md(Tests reviewers reject): "Don't raise per-test timeouts to make a slow test pass; shrink the workload."
The comment justifying the timeout ("10k awaited inserts need more than the 5s default under debug + ASAN") is precisely the rationale the REVIEW.md rule says should trigger a workload reduction rather than a ceiling raise. There is minor tension with the root
CLAUDE.mdnote that a per-test timeout is acceptable "only for the rare outlier", but the more-specifictest/CLAUDE.mdand the explicit REVIEW.md rule both point the other way here — this is not an outlier with an irreducible workload, it's a loop counter that can simply be smaller.Step-by-step
- The test at
sqlite-sql.test.ts:2096setsconst iterations = 10000and awaits anINSERTper iteration in a serialforloop. - Under a debug+ASAN build (which
CLAUDE.mdnotes runs 10-100× slower than release), 10k awaited round-trips throughSQLiteQueryHandle.run→db.prepare/db.runexceed the 5s default. - Rather than reducing
iterations, the PR adds}, 60_000);at line 2123 to raise the ceiling. - If this test ever genuinely hangs (e.g. a future regression in statement finalization), each attempt now burns 60s; multiplied across the retry policy this adds minutes to a red CI run.
- It also masks perf regressions: a change that makes each insert 5× slower would still pass under a 60s budget where it would have failed under the default.
Impact
Test-hygiene only — nothing in shipped runtime code is affected, so this doesn't block correctness. But it violates a rule the repo marks CRITICAL, and it's an unrelated drive-by change in a feature PR, which makes it easy to revert without touching the actual fix.
Suggested fix
Drop the
60_000argument and the trailing comment, and reduce the workload instead — e.g.const iterations = 2000;(or whatever value comfortably clears the default timeout under debug+ASAN while still exercising "many" prepared statements). The test's purpose is to verify statements are finalized across many iterations; 2k iterations demonstrates that as well as 10k does.
|
Addressed the timeout finding in 206263b: dropped the 60s per-test timeout and shrank the insert loop to 1000 iterations, which fits the default timeout on a debug ASAN build while still exercising statement finalization. |
There was a problem hiding this comment.
LGTM — all prior feedback addressed (docs now note the strict: true prefix requirement, sql.file() has a named-params test, the long comment is one line, and the finalization test's iteration count was cut to fit the default timeout).
What was reviewed:
- The
$isArraybranch inSQLiteQueryHandle.runfor all four call sites (all/values/raw/db.run) — arrays still spread, objects pass as a single arg. unsafeQuery'svalues?.length ?? 0check sets thesimpleflag for objects, but the SQLite handle ignores flags so it's harmless.- New
sql.filetest uses the sameawait using tempDir+path.join(dir, …)pattern already established in this file.
Extended reasoning...
Overview
Small, focused bugfix: Bun.SQL's SQLite adapter was spreading unsafe()/file() bindings via $apply, which treats a plain object as a zero-length array-like and silently binds nothing. The fix adds an $isArray(values) branch — arrays spread as before, objects are passed as a single argument to stmt.all/stmt.values/stmt.raw/db.run, which bun:sqlite already supports natively for :name/$name/@name placeholders. Type declarations for unsafe() and file() are widened to any[] | Record<string, any>, and docs/JSDoc note the SQLite-only behavior and the strict: true prefix caveat.
Security risks
None. Values are still bound as parameters by the underlying bun:sqlite layer, not concatenated into SQL. unsafe() remains the explicit "you own the query string" escape hatch it already was; this change only affects how the bindings argument is forwarded.
Level of scrutiny
Low-to-medium. The runtime change is ~10 lines in one file, mechanically branching on $isArray. The rest is docs, types, and 7 new tests. This is a redo of a previously accepted community PR (#23375). The evidence block shows all new tests fail on main and pass with the fix, and the full sqlite-sql.test.ts suite passes on both debug-ASAN and release.
Other factors
All four review threads from the prior pass are resolved and verified against the current diff:
- The docs example at
sql.mdx:440-445now constructs the connection withstrict: trueand the prose explains prefixed vs. bare keys. - The JSDoc
@exampleinsql.d.tsuses{ ":id": 1 }and states keys keep the prefix unlessstrict: true. - A
sql.file()named-params test was added alongside the existing file tests, using the sameawait using tempDir+path.join(dir, …)idiom already used earlier in the file (tempDirreturns aDisposableStringextendingString, sopath.joinaccepts it). - The 10k-insert finalization test was reduced to 1000 iterations with a comment, replacing the earlier 60s per-test timeout.
I also checked that unsafeQuery in src/js/bun/sql.ts:134 reading values?.length (which is undefined on a plain object, so the simple flag gets set) does not affect the SQLite path — SQLiteQueryHandle.run never consults query flags. The values ?? [] default in createQueryHandle keeps the null/undefined case on the array branch.
There was a problem hiding this comment.
LGTM — the two nits from the earlier pass (sql.file() coverage, strict-mode caveat in docs/JSDoc) are addressed, and the loop shrink replaces the timeout bump per convention.
What was reviewed:
$isArraybranch inSQLiteQueryHandle.run: array path unchanged ($apply), object path uses$callto hand the bindings object tobun:sqlite's existing named-parameter support; same shape applied todb.run.- Test matrix covers strict/non-strict, all three prefixes,
.values()/.raw(), UPDATE/DELETE counts, transactionunsafe, andsql.file();tempDir/path.joinusage matches the file's existing pattern. - The 10000→1000 iteration shrink keeps the
iterations - 300assertion arithmetically consistent and still cycles enough statements to exercise finalization.
Extended reasoning...
Overview
Four files touched: src/js/internal/sql/sqlite.ts (~18 lines — widen the values type and branch on $isArray to pass named-parameter objects via $call instead of spreading via $apply), packages/bun-types/sql.d.ts (widen unsafe/file signatures + JSDoc), docs/runtime/sql.mdx (SQLite named-parameter example with the strict-mode prefix caveat), and test/js/sql/sqlite-sql.test.ts (7 new tests + one unrelated iteration shrink).
Security risks
None introduced. sql.unsafe() is already the documented escape hatch for raw SQL; this only changes how the bindings argument is forwarded to bun:sqlite, which already supports object bindings. The $isArray/$call/$apply intrinsics follow the tamper-resistance convention for src/js/ builtins. No native code touched.
Level of scrutiny
Low-to-moderate. The runtime change is a small conditional whose array branch is byte-identical to the previous behavior, and the object branch delegates to an existing, well-tested bun:sqlite feature. The type/docs changes are declarative. The one adjacent change — shrinking the finalization loop from 10000 to 1000 — follows the REVIEW.md rule ("shrink the workload" over raising a timeout) and the finalCount === iterations - 300 assertion stays valid.
Other factors
Both prior inline nits from the earlier bug-hunt pass are addressed and resolved: the docs/JSDoc now state that keys keep the prefix unless strict: true (with the example showing strict: true), and a sql.file() named-parameter test was added. The comment-cop feedback on the inline comment was also addressed (trimmed to one line). Test coverage is thorough across the variant matrix, and the PR evidence shows the new tests fail on main and pass with the fix. New test code matches existing file conventions (await using dir = tempDir(...), path.join(dir, ...)).
There was a problem hiding this comment.
LGTM — thanks for addressing the earlier feedback (docs/JSDoc now clarify the strict: true requirement, sql.file() is covered, the comment is one line, and the finalization test shrinks the loop instead of raising the timeout).
What was reviewed:
SQLiteQueryHandle.run—$isArraybranch correctly routes objects through$calland arrays through$applyfor all four sinks (all/values/raw/db.run); tamper-proof intrinsics used throughout.- Traced
sql.unsafe/sql.fileand their transaction/reserved twins throughnormalizeQuery→createQueryHandle: the string-input path returnsvaluesunchanged, so objects reach the handle intact; thesimpleflag set whenvalues.lengthis undefined is harmless for SQLite (adapter ignores flags). - Seven new tests cover strict/non-strict, all three prefixes,
values()/raw(), UPDATE/DELETE counts, transactions, andsql.file().
Extended reasoning...
Overview
This PR fixes #22799: Bun.SQL's unsafe()/file() silently dropped named-parameter objects for SQLite because $apply treated a plain object as a zero-length array-like. The fix is an $isArray(values) branch in SQLiteQueryHandle.run that passes objects via $call (single argument) instead of $apply (spread), delegating to bun:sqlite's existing named-parameter support. Type signatures for unsafe()/file() are widened to any[] | Record<string, any>, and docs/JSDoc explain the SQLite-only behavior and the strict: true requirement for bare keys.
Files touched: ~18 LOC in src/js/internal/sql/sqlite.ts, type widening in packages/bun-types/sql.d.ts, one docs paragraph in docs/runtime/sql.mdx, and 7 new tests plus a shrunk iteration count in test/js/sql/sqlite-sql.test.ts.
Prior review resolution
I left two nits on the previous revision, both now addressed:
- The docs example now sets
strict: trueand the prose explains that keys keep the prefix ({ ":id": 1 }) without it; the JSDoc@exampleuses the prefixed form so it works under the default. - A
sql.file()test with a named-parameter object was added.
The comment-cop bot's note was addressed (comment reduced to one line stating the $apply hazard), and the statement-finalization test now shrinks its loop from 10000 to 1000 rather than raising the timeout — matching REVIEW.md's "shrink the workload" guidance.
Security risks
None introduced. unsafe() is already the escape hatch for raw SQL; this change only fixes how bound parameter values reach bun:sqlite, which handles the actual binding. No new user-controlled data reaches an interpolation site. Postgres/MySQL adapters are untouched.
Level of scrutiny
Low-to-moderate. The runtime change is a four-line conditional in built-in JS that delegates to already-tested bun:sqlite machinery. I traced the value flow end-to-end: sql.unsafe/sql.file (and transaction/reserved variants) → Query → adapter.normalizeQuery (returns object unchanged for string input) → createQueryHandle → SQLiteQueryHandle.run. The SQLQueryFlags.simple flag that gets set when values.length is undefined is a no-op for SQLite since createQueryHandle ignores the flags argument. Tamper-proof intrinsics ($isArray, $call, $apply) are used correctly per the built-ins conventions.
Other factors
- Test coverage is thorough for the variant matrix: strict and non-strict modes, all three placeholder prefixes,
.values()/.raw()result modes, INSERT/UPDATE/DELETE change counts, inside a transaction, and viasql.file(). Verified in the PR evidence to fail on main and pass with the fix under both debug+ASAN and release. - The change is a redo of #23375 against current main as requested there.
- No open reviewer comments remain; all inline threads are resolved.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@docs/runtime/sql.mdx`:
- Around line 440-445: Update the “Queries in files” documentation to include
SQLite named-object bindings supported by sql.file(), including the placeholder
formats and strict-mode bare-key behavior. Keep the existing positional $1/$2
parameter documentation intact.
In `@test/js/sql/sqlite-sql.test.ts`:
- Around line 1389-1399: Add `$name` and `@name` binding cases to the non-strict
`unsafe` named-parameter test alongside the existing `:id` case, using matching
`$name` and `@name` keys in the parameter objects and verifying the inserted or
queried values. Keep coverage focused on prefix retention for all three
supported binding prefixes.
🪄 Autofix
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: d355a600-5ab2-42fa-b876-91ac7fcc348e
📒 Files selected for processing (4)
docs/runtime/sql.mdxpackages/bun-types/sql.d.tssrc/js/internal/sql/sqlite.tstest/js/sql/sqlite-sql.test.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/js/sql/sqlite-sql.test.ts (1)
1606-1618: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd non-strict
sql.file()coverage.This test uses only
strict: trueand bare object keys. It does not prove thatsql.file()works in default mode with prefixed keys. Add a default-mode case such as{ ":id": 1 }and assert the filtered row.As per coding guidelines, “Tests must cover the complete relevant variant matrix, including sibling APIs and flag states.”
🤖 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 `@test/js/sql/sqlite-sql.test.ts` around lines 1606 - 1618, Add non-strict coverage alongside the existing “file with named parameters” test by creating SQL without strict mode, passing prefixed parameter keys such as { ":name": "Alice" } to sql.file(), and asserting the filtered row result. Keep the existing strict-mode test unchanged.Source: Coding guidelines
🤖 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.
Outside diff comments:
In `@test/js/sql/sqlite-sql.test.ts`:
- Around line 1606-1618: Add non-strict coverage alongside the existing “file
with named parameters” test by creating SQL without strict mode, passing
prefixed parameter keys such as { ":name": "Alice" } to sql.file(), and
asserting the filtered row result. Keep the existing strict-mode test unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 63066fff-2bc1-4fc8-8f6e-3e12e31cba0e
📒 Files selected for processing (2)
docs/runtime/sql.mdxtest/js/sql/sqlite-sql.test.ts
|
Added non-strict sql.file() coverage in ad89b39: the same query file now runs against a default-mode connection with prefixed keys. |
ad89b39 to
4fa58ea
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
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/sqlite.ts`:
- Around line 248-254: Wrap all prepared-statement execution after
db.prepare(sql), including the values, raw, and all $call/$apply branches, in a
try/finally block and move stmt.finalize() into finally. Preserve the existing
result handling while ensuring named-binding exceptions and other failures
finalize the statement before propagating.
🪄 Autofix
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: b1d59e35-2637-42a4-a7b3-fa53fe882d60
📒 Files selected for processing (4)
docs/runtime/sql.mdxpackages/bun-types/sql.d.tssrc/js/internal/sql/sqlite.tstest/js/sql/sqlite-sql.test.ts
|
looks like this is a behavior only in sqlite should we add it even if postgres and mysql dont have? |
|
@cirospaciari Right, this is SQLite only. Some context for the call:
What makes the current state worth changing either way is that on main, passing an object to the SQLite adapter silently binds nothing and a SELECT returns If you would rather keep the adapters uniform, the alternative is to make SQLite throw the same |
@cirospaciari #22799 explains the rationale: ability to use the same import/API for multiple databases. I would love to see this implemented as I have a project with integration tests on PostgreSQL, SQLite, and several other platforms. More consistency would be nice. |
|
@robobun check code reviews here plz |
|
@cirospaciari Reviews are addressed, all threads resolved. The one code change that came out of them is in 1c8f66e: statement execution is now wrapped in try/finally so a failed bind (for example a missing named parameter in strict mode) still finalizes the prepared statement, with a test for that path. The rest were test and doc tightening. Full sqlite-sql.test.ts passes locally (239 tests), CI is running on the latest push. |
|
The red on the latest build is three build steps failing to download vendored artifacts from GitHub after 5 attempts (lolhtml tarball on freebsd x64 and linux x64-musl, the WebKit prebuilt on windows aarch64), so those lanes never got to run tests. Nothing in this change is involved; every lane that built is passing, and I already used my one retrigger on an earlier build. A job retry on those three steps should clear it. |
Bun.SQL's unsafe() only bound array values. Passing an object of named parameters to a SELECT silently returned no rows: $apply treats the object as an empty array-like, so the named placeholders stayed unbound and SQLite compared against NULL. Detect object bindings in SQLiteQueryHandle and pass them as a single argument to the underlying bun:sqlite statement methods (stmt.all, stmt.values, stmt.raw, db.run), which already support :name, $name, and @name placeholders. Array values spread into positional arguments as before. Fixes #22799 Co-authored-by: Savan Nahar <savannahar68@gmail.com>
10,000 awaited inserts take 13-17s under a debug ASAN build, past the 5s default per-test timeout. The test is unchanged otherwise.
…ameters The docs and JSDoc examples used bare object keys, which only bind under strict: true; the default connection needs prefixed keys like ":id". Also adds a test for sql.file() with a named-parameters object since its signature was widened too.
Per-test timeouts are disallowed in test/CLAUDE.md; 1000 serial awaited inserts still exercise statement finalization and fit the default timeout on a debug ASAN build.
Shrink to 1000 iterations only when isDebug, per review.
…eters in the sql.file docs
A missing named binding in strict mode throws from stmt.all/values/raw, which previously skipped stmt.finalize() and left the native statement alive until GC. Move finalize into a finally block and cover the error path. Also assert the raw() result bytes instead of only the lengths.
1c8f66e to
fa34508
Compare
…sql-close-drains-same-tick-queries main landed the same isDebug iterations tweak in sqlite-sql.test.ts (#37109), so that hunk is now upstream.
… spread (#35950) ### Problem - With the SQLite adapter of `Bun.SQL`, an object or array used as a query *value* is taken as the binding set for the whole statement instead of being bound as one value. - An indexed-key object overrides every other parameter, including trusted ones that come later in the query: ```ts const sql = new SQL("sqlite://:memory:"); const attackerId = JSON.parse('{"0": 2, "1": "bob"}'); // e.g. a request body await sql`SELECT * FROM docs WHERE id = ${attackerId} AND tenant = ${"alice"}`; // before: bob's row (the tenant filter was rebound to "bob") // after: TypeError: Binding expected string, TypedArray, boolean, number, bigint or null ``` - Any other object (`{ x: 1 }`, a `Date`) in that position silently binds `NULL`: ``sql`insert into t values (${ { x: 1 } })` `` stores a NULL row with no error. - Same for `sql.unsafe("select ? as a", [{ x: 1 }])` and for a `sql({...})` insert helper whose first column holds an object. - Cause: `SQLiteQueryHandle.run` in `src/js/internal/sql/sqlite.ts` spread the positional array into `stmt.all/values/raw(...values)` and `db.run(sql, ...values)`. `bun:sqlite` reads the whole binding set from its first argument (array = positional list, other object = named/indexed map; see `rebindStatement` in `src/jsc/bindings/sqlite/JSSQLStatement.cpp`), so whatever sat in slot 0 decided how the statement was bound and the remaining arguments were ignored. ### Fix - Pass the positional array itself as the single argument (`stmt.all.$call(stmt, values)`, `db.run.$call(db, sql, values)`), which is `bun:sqlite`'s documented positional form. Named-parameter objects (#37109) already took this path, so the `$isArray` branches collapse into one call. - Every element now goes through `rebindValue`, so a non-bindable value in any slot fails with `bun:sqlite`'s own `TypeError` ("Binding expected string, TypedArray, boolean, number, bigint or null"), the same error such a value already produced in every slot other than the first. An empty array binds nothing, as before. - Verified with `test/js/sql/sqlite-sql.test.ts` ("an object or array interpolated as a value cannot stand in for the parameter list"): 8 of the 9 new tests fail on the unfixed build and all pass with the fix; the 9th checks that positional values, `unsafe()` arrays and the `sql()` helpers still bind in order. - The whole file (248 tests, including the named-parameter tests from #37109) and the other SQLite adapter tests in `test/js/sql` pass on a debug build. ### Background - `Bun.SQL` tagged templates are turned into `?` placeholders plus a positional array of values (`normalizeQuery` in `src/js/internal/sql/shared.ts`); the SQLite adapter hands that array to `bun:sqlite`. - `bun:sqlite`'s `Statement.all/values/raw` and `Database.run` accept either scalars spread as arguments, one array of positional values, or one object of named parameters. The JS wrapper (`src/js/bun/sqlite.ts`) only looks at the first argument to pick the form, and the native side binds from that argument alone. - `strict: true` connections were not affected in the same way: missing parameters throw there, so these queries failed with `Missing parameter "1"` rather than silently rebinding. <details> <summary>Earlier revision of this PR</summary> The first revision also moved `stmt.finalize()` into a `finally` block and scaled the 10k-insert finalization test down on debug builds. Both of those landed separately in #37109, which also added named-parameter support for `unsafe()` and conflicted with this branch; the branch was rebased on top of it and now only contains the positional-array change plus the tests. The tests were extended with the `unsafe()` array and `sql()` insert-helper cases. </details> <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 7 · 2 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 8 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/sql/sqlite-sql.test.ts bun test v1.4.0 (aec9cf5) test/js/sql/sqlite-sql.test.ts: (pass) Connection & Initialization > common default connection strings > should parse common connection strings [213.48ms] (pass) Connection & Initialization > should connect to in-memory SQLite database [21.58ms] (pass) Connection & Initialization > should connect to file-based SQLite database [146.94ms] (pass) Connection & Initialization > should handle connection with options object [160.92ms] (pass) Connection & Initialization > onconnect and onclose callbacks are invoked for SQLite [29.00ms] (pass) Connection & Initialization > onconnect receives Error when open fails (readonly non-existent) [93.33ms] (pass) Connection & Initialization > should create database file if it doesn't exist [291.85ms] (pass) Connection & Initialization > should work with relative paths [199.90ms] (pass) Connection & Initialization > Environment Variable Handling > should use DATABASE_URL for SQLite when it's a SQLite URL [127.10ms] (pass) Connection & Initializat ... (truncated) release without fix: 16 FAILED bun test v1.4.0-canary.1 (da3851e) test/js/sql/sqlite-sql.test.ts: (pass) Connection & Initialization > common default connection strings > should parse common connection strings [2.83ms] (pass) Connection & Initialization > should connect to in-memory SQLite database [0.29ms] (pass) Connection & Initialization > should connect to file-based SQLite database [64.10ms] (pass) Connection & Initialization > should handle connection with options object [3.53ms] (pass) Connection & Initialization > onconnect and onclose callbacks are invoked for SQLite [0.81ms] (pass) Connection & Initialization > onconnect receives Error when open fails (readonly non-existent) [6.61ms] (pass) Connection & Initialization > should create database file if it doesn't exist [110.90ms] (pass) Connection & Initialization > should work with relative paths [57.83ms] (pass) Connection & Initialization > Environment Variable Handling > should use DATABASE_URL for SQLite when it's a SQLite URL [97.61ms] (pass) Connection & Initialization > Environment Variable Handling > should handle DATABASE_URL with :memory: [0.99ms] (pass) Connection & Initialization > Environment Variable Handling > should h ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console 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/sqlite-sql.test.ts bun test v1.4.0 (aec9cf5) test/js/sql/sqlite-sql.test.ts: (pass) Connection & Initialization > common default connection strings > should parse common connection strings [198.53ms] (pass) Connection & Initialization > should connect to in-memory SQLite database [22.40ms] (pass) Connection & Initialization > should connect to file-based SQLite database [85.15ms] (pass) Connection & Initialization > should handle connection with options object [166.10ms] (pass) Connection & Initialization > onconnect and onclose callbacks are invoked for SQLite [31.86ms] (pass) Connection & Initialization > onconnect receives Error when open fails (readonly non-existent) [62.42ms] (pass) Connection & Initialization > should create database file if it doesn't exist [69.58ms] (pass) Connection & Initialization > should work with relative paths [207.83ms] (pass) Connection & Initialization > Environment Variable Handling > should use DATABASE_URL for SQLite when it's a SQLite URL [230.15ms] (pass) Connection & Initializatio ... (truncated) release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 1226ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/22] gen cpp.rs (cppbind) [2/22] gen JS modules (bundle-modules) Preprocess modules (16279ms) Bundle modules (267ms) Postprocesss modules (612ms) Bundle Functions (1617ms) Generate Code (47ms) [18.85s] Bundled "src/js" for production 2626 kb 197 internal modules 13 native modules 91 internal functions across 17 files [2/9] 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_exe_format v0.0.0 (/workspace/bun/src/exe_format) �[1m�[92m Compiling�[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph) �[1m�[92m Compiling�[0m bun_bunfig v0.0.0 (/workspace/bun/src/bunfig) �[1m�[92m Compiling�[0m bun_install v0.0.0 (/workspace/bun/src/install) �[1m�[92m Compiling�[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc) �[1m�[92m Compiling�[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc) �[1m�[92m Compiling�[0m bun_ast_jsc v0.0 ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/js/internal/sql/sqlite.ts | 10 ++--- test/js/sql/sqlite-sql.test.ts | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 5 deletions(-) ``` </details> **gate history** · 3 passed · 1 rejected · iteration 7 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/js/internal/sql/sqlite.ts 5 5 0 test/js/sql/sqlite-sql.test.ts 3 5 0 ``` </details> <!-- robobun:evidence:end --> Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com>
Redo of #23375 by @savannahar68 against current main, as requested there. Fixes #22799.
Problem
Bun.SQL'sunsafe()only bound array values. Passing an object of named parameters to a SELECT silently returned no rows:Cause
SQLiteQueryHandle.runspread values with$apply. A plain object is an array-like of length 0 toapply, sostmt.all()ran with nothing bound and SQLite treated the named placeholders as NULL, returning empty results instead of erroring.Fix
In
src/js/internal/sql/sqlite.ts, detect object bindings and pass them as a single argument to the underlyingbun:sqlitemethods (stmt.all/stmt.values/stmt.raw/db.run), which already support:name,$name, and@nameplaceholders. Arrays keep spreading into positional arguments. Statement execution is now wrapped in try/finally so a failed bind (for example a missing named parameter in strict mode) still finalizes the prepared statement. Theunsafe/filetype signatures are widened and the docs note that this is SQLite only and that object keys keep their prefix unless the connection setsstrict: true.Named parameters stay SQLite only: Postgres and MySQL already reject non-array values natively (
src/sql_jsc/shared/QueryCtorArgs.rs), and those adapters are unchanged.Verification
Eight new tests in
test/js/sql/sqlite-sql.test.ts: strict mode, all three placeholder prefixes, non-strict mode with prefixed keys for all three prefixes,values()/raw()result modes (raw bytes checked), the missing-binding error path, UPDATE/DELETE change counts,unsafe()inside a transaction, andsql.file()on both strict and default connections. All eight fail on main (empty results) and pass with this change; the rest of the file and the bun-types integration test pass. The pre-existing 10k-insert finalization test uses 1000 iterations on debug builds only, so the file fits the default timeout under ASAN.[review] gate passed · iteration 5 · 4 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 4 passed · 3 rejected · iteration 5
evidence per changed file