Repository navigation
Conversation
The SQLite adapter did not implement attachConnectionCloseHandler, so close() never told a transaction whose callback was still running that the database was gone. The begin() promise stayed pending forever. close() now calls the attached handlers with ERR_SQLITE_CONNECTION_CLOSED, as the pooled adapters do when a connection closes. Queries that the callback sends afterwards reject with the same error.
|
Updated 10:42 AM PT - Sep 17th, 2026
✅ @robobun, your commit 1891e4de70f34e90fa5d2262992b697609abef7b passed in 🧪 To try this PR locally: bunx bun-pr 43116That installs a local version of the PR into your bun-43116 --bun |
|
Status: reproduced and fixed here. Ready for review. How I reproduced it: the script in the Notes block of the description. The two new tests in |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review. Walkthrough
ChangesSQLite connection closure
Suggested reviewers: Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to The SQLite close-handling change has no identified unresolved behavior that should block merging. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I also checked the close-handler re-entrancy and throw paths: the only handler registered is onTransactionDisconnected, which just flips connectionState and calls promise/Query reject (no detach during iteration, and the set is swapped before the loop anyway), so a throwing handler leaving begin() pending is not reachable from in-tree code. sql.reserve() throws for SQLite, so no reserved-connection handler path needs the same hook.
Extended reasoning...
Findings were reported inline, so this body only records what else was examined. The candidate "handler throws during close() leaves other begin() promises pending" was traced through src/js/bun/sql.ts onTransactionDisconnected: it sets the closed bit, rejects each tracked Query (Query.reject in src/js/internal/sql/query.ts only updates status flags and calls the stored reject), and rejects the transaction promise — none of which throws or mutates the adapter's handler set. The post-close COMMIT/ROLLBACK path was also checked: run_internal_transaction_sql short-circuits on the closed bit, so nothing is sent to the closed bun:sqlite Database and the duplicate reject is a no-op. Not approving because a verified finding is being posted inline and another verified finding was dropped from the posted set.
Problem
Bun.SQLSQLite adapter, thesql.begin(cb)promise stays pending forever whensql.close()runs duringcbandcbnever finishes. After a forced close, the PostgreSQL and MySQL adapters rejectbegin()withERR_POSTGRES_CONNECTION_CLOSED/ERR_MYSQL_CONNECTION_CLOSED.cbcontinues after the close, its queries reject with the rawRangeError: Cannot use a closed database, andbegin()rejects withError: Database has closed.begin()registers its close handler through the optional adapter hookattachConnectionCloseHandler(src/js/bun/sql.ts:650).SQLiteAdapterdoes not implement the hook, soclose()(src/js/internal/sql/sqlite.ts:457) notifies nothing.Fix
SQLiteAdapterimplements the hook anddetachConnectionCloseHandlerwith a set of handlers.close()calls each handler withERR_SQLITE_CONNECTION_CLOSEDafter the useronclosecallback, the order the pooled adapters use.onTransactionDisconnected. It marks the transaction closed and rejectsbegin(). Later tagged-template queries fromcbreject with the same error, and no COMMIT or ROLLBACK goes to the closed database.close()on SQLite stays immediate and still ignorestimeout.test/js/sql/sqlite-sql.test.ts. Two new tests fail on 1.4.3-canary.1 and pass with this change. Notes lists the other suites.Background
sql.begin(cb)returns a promise fromPromise.withResolvers(). It settles whencbsettles, or when the close handler of the connection fires.bun:sqliteDatabase, andclose()closes it at once.Notes
Repro (from the report):
{"begin":"PENDING","close({ timeout: 1 })":"resolved","query after close":"rejected: ERR_SQLITE_CONNECTION_CLOSED"}{"begin":"rejected: ERR_SQLITE_CONNECTION_CLOSED","close({ timeout: 1 })":"resolved","query after close":"rejected: ERR_SQLITE_CONNECTION_CLOSED"}What
begin()does whenclose()runs during the callback, SQLite adapter:ERR_SQLITE_CONNECTION_CLOSEDRangeError: Cannot use a closed database,begin():Error: Database has closedERR_SQLITE_CONNECTION_CLOSEDError: Database has closed(from COMMIT)ERR_SQLITE_CONNECTION_CLOSEDError: Database has closed(from ROLLBACK)ERR_SQLITE_CONNECTION_CLOSEDThe close rolls the open transaction back, before and after this change. Checked with a file database: the row that the callback inserted is gone when the file is opened again.
The same script against a local PostgreSQL with
close({ timeout: 1 })and a callback that never settles givesbegin()rejected withERR_POSTGRES_CONNECTION_CLOSED. That is the behavior this change matches.begin()is rejected beforeclose()resolves on both adapters, and the first new test asserts that withBun.peek.status, so a missing rejection fails at once and does not wait for the test timeout.Not changed here:
close()on PostgreSQL and MySQL waits for an open transaction, up totimeoutseconds when one is given. The SQLite adapter never waited, and this change keeps that. To make it wait would turn aclose()that resolves today into one that can stay pending.tx.savepoint(cb)promise whosecbnever settles stays pending after a forced close on every adapter (checked on PostgreSQL too). That promise belongs to an async function that awaitscb, so nothing outside can reject it.begin()still rejects.tx.unsafe()andtx.file()do not read the transaction state on any adapter. The shared code insrc/js/bun/sql.ts:681sends them to the captured connection. In a callback that outlives the close they still reject with the raw error:RangeError: Cannot use a closed databaseon SQLite,Error: connection must be a PostgresSQLConnectionon PostgreSQL. The same gap lets atxhandle kept afterbegin()settled rununsafe()on the released connection. sql: reject tx.unsafe()/tx.file() on a settled transaction or released reserved handle #33751 had a fix for that and was closed as stale, not on merit. It is a separate change to shared code and it is tracked on its own.Suites run with the debug build on Linux:
test/js/sql/sqlite-sql.test.ts: 252 pass, 0 fail.test/js/sql/sql-onconnect-onclose-throw.test.ts,sqlite-url-parsing.test.ts,adapter-override.test.ts,adapter-env-var-precedence.test.ts,sql-close-pending-connection.test.ts: 289 pass, 0 fail.[human-review] gate passed · iteration 1 · 2 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 0 rejected · iteration 1
evidence per changed file