Conversation
The wasm driver tracked the transaction itself in `_inTx`. SQLite ends a transaction on its own after SQLITE_FULL, SQLITE_IOERR or SQLITE_BUSY, and the flag then latched: the next savepoint skipped its `BEGIN`, and releasing it threw "cannot commit - no transaction is active". `inTransaction` now reads `sqlite3_get_autocommit`, which the wasm build binds but does not type — hence the cast, and the check at init that it is there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without the `pointer` guard, `sqlite3_get_autocommit(undefined)` returns 0 and a closed connection reports a transaction open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…egins Review follow-up. Reading `inTransaction` from SQLite drops the invariant the cached flag carried, that no transaction implies an empty savepoint stack. When SQLite ends a transaction itself, the stale stack makes `commitIfNeeded` defer to a savepoint owner that no longer exists, and the next write sits in a transaction nobody closes. The Node driver has read better-sqlite3's live `inTransaction` all along and had the same hole; both are fixed, and both mocks now move the flag the way SQLite does. The capi check moves above the open so a build without `sqlite3_get_autocommit` does not leak the handle, and `inTransaction` is no longer `configurable`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sirtimid
added this pull request to stack #1109
September 15, 2026 22:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The wasm driver kept its own
_inTxflag and set it whenever it issued aBEGIN,COMMITorROLLBACK. SQLite does not only end transactions when asked: afterSQLITE_FULL,SQLITE_IOERRorSQLITE_BUSYit ends the current one itself, along with every savepoint in it. The flag then latched true. The nextcreateSavepointsaw a transaction it thought was already open and skipped itsBEGIN; the matchingreleaseSavepointthrewcannot commit - no transaction is active; and the browser kernel store went on refusing every later write to a database that was perfectly healthy.inTransactionis now a getter oversqlite3_get_autocommit. The wasm build binds that function but does not declare it, hence the narrowAutocommitCapicast — and the check at init, before the database is opened, that it really is there.Trusting SQLite costs an invariant the cached flag used to carry for free:
_inTx === falseimplied the savepoint stack was empty, because the same code cleared both. It does not any more. When SQLite ends a transaction under an open savepoint,beginIfNeededopens a fresh one while_spStackstill names a savepoint from the old one, andcommitIfNeeded— which declines when the stack is non-empty, on the grounds that a savepoint owner will commit — defers to an owner that no longer exists. The write sits in a transaction nobody closes, and every later write joins it. SobeginIfNeedednow clears the stack when it opens a transaction. The Node driver needed the same fix: it has read better-sqlite3's livedb.inTransactionall along, so it has had this hole since the savepoint stack was introduced.Changes
Database.inTransactionreplaces_inTx: a getter installed ininitDBoversqlite3_get_autocommit, guarded for a closed connection, with a hard failure at init if the wasm build does not bind the function.beginIfNeededclears the savepoint stack when it opens a transaction, in both the wasm and the Node driver.Testing
wasm.transactions.test.tsis new and deliberately carries novi.mock, so it runs against the real wasm SQLite build. Its two regression guards both fail onmain:takes the next crank in a transaction of its own— SQLite ends the transaction, the next savepoint has to open its own.commits a write made after SQLite ends the transaction itself— asserts no transaction is left open afterwards, which is what fails when the savepoint stack is stale.reports no transaction once the database is closedpins thepointerguard: without itsqlite3_get_autocommit(undefined)returns 0 and a closed database reports a transaction open forever.The mocked suites needed their databases to stop lying. In both
wasm.test.tsandnodejs.test.ts,preparenow returns distinct statement mocks forBEGIN,COMMITandABORT, and stepping one of those is what moves the transaction flag — as it does in SQLite. Assertions that read the driver's own bookkeeping now read which statement it ran. That conversion is most of this diff, and it repaired three assertions that could not fail: twonot.toHaveBeenCalledWith(objectContaining({ sql }))against a zero-argumentstep(), and one test that ended in a bare assignment where anexpectwas meant.nodejs.test.tsgainsdrops savepoints SQLite discarded with the transactionfor the Node half of the fix, andwasm.test.tsgainsreports the write failure, not a rollback with nothing to undo, which pins theif (db.inTransaction)guard inrollbackIfNeeded— without it, a rollback attempted after SQLite already ended the transaction throws and replaces the real "disk is full" with "cannot rollback".Every fix above was mutation-checked by reverting it alone and confirming the named test fails.
kernel-storeandocap-kernelsuites are green locally, as are the builds;@ocap/kernel-test'scrank-rollback,vatstoreandgarbage-collectionrun green against a rebuiltdist. Itsremote-commstest crashes the Node worker on this machine identically onorigin/main, so it is not from this change.Resolves #1074
🤖 Generated with Claude Code
Note
Medium Risk
Changes core persistence transaction/savepoint logic in both wasm and Node drivers; incorrect behavior could lose or strand writes, though the fix targets a known data-loss bug in the browser store.
Overview
Fixes kernel-store SQLite transaction bookkeeping when SQLite ends a transaction on its own (disk full, I/O error, busy) instead of only on explicit
BEGIN/COMMIT/ROLLBACK.The wasm driver drops the cached
_inTxflag for a liveinTransactiongetter backed bysqlite3_get_autocommit, with init failing if that C API is missing.beginIfNeedednow clears the savepoint stack whenever it starts a new transaction so stale savepoint names cannot block commits; the Node driver gets the same stack clear while it already relied on better-sqlite3’sinTransaction.Tests add unmocked wasm integration coverage for savepoint/transaction recovery, tighten mocks so
BEGIN/COMMIT/ABORTdrive transaction state, and add regressions for post-recovery writes and error reporting. CHANGELOG records the persistence fixes.Reviewed by Cursor Bugbot for commit 9658e48. Bugbot is set up for automated code reviews on this repo. Configure here.