diff --git a/src/examples/IDBMirrorVFS.js b/src/examples/IDBMirrorVFS.js index 55a6dac2..09fcd0ed 100644 --- a/src/examples/IDBMirrorVFS.js +++ b/src/examples/IDBMirrorVFS.js @@ -354,7 +354,10 @@ export class IDBMirrorVFS extends FacadeVFS { file.blocks.set(0, newBlock); block = newBlock; } - block.set(pData, iOffset); + // pData is a Uint8ArrayProxy, which has no indexed access, so set() + // would read undefined at every index and store zeroes. subarray() + // returns a real Uint8Array over the same bytes. + block.set(pData.subarray(), iOffset); file.blockSize = Math.max(file.blockSize, iOffset + pData.byteLength); } return VFS.SQLITE_OK; diff --git a/test/IDBMirrorVFS.test.js b/test/IDBMirrorVFS.test.js index 7887653f..3d4b5c98 100644 --- a/test/IDBMirrorVFS.test.js +++ b/test/IDBMirrorVFS.test.js @@ -4,6 +4,7 @@ import { vfs_xAccess } from "./vfs_xAccess.js"; import { vfs_xClose } from "./vfs_xClose.js"; import { vfs_xRead } from "./vfs_xRead.js"; import { vfs_xWrite } from "./vfs_xWrite.js"; +import { vfs_rollback } from "./vfs_rollback.js"; const CONFIG = 'IDBMirrorVFS'; const BUILDS = ['asyncify', 'jspi']; @@ -22,6 +23,7 @@ describe(CONFIG, function() { vfs_xClose(context); vfs_xRead(context); vfs_xWrite(context); + vfs_rollback(context); }); } }); diff --git a/test/vfs_rollback.js b/test/vfs_rollback.js new file mode 100644 index 00000000..6515a1d2 --- /dev/null +++ b/test/vfs_rollback.js @@ -0,0 +1,47 @@ +import * as Comlink from 'comlink'; + +/** + * A rollback of a transaction large enough to spill the page cache. SQLite + * then journals the pages it writes, and stamps the journal header once they + * are on disk; the rollback reads that header back to undo them. + * @param {import('./TestContext.js').TestContext} context + */ +export function vfs_rollback(context) { + describe('vfs_rollback', function() { + let proxy, sqlite3, db; + beforeEach(async function() { + proxy = await context.create(); + sqlite3 = proxy.sqlite3; + db = await sqlite3.open_v2('rollback-test'); + }); + + afterEach(async function() { + await sqlite3.close(db); + await context.destroy(proxy); + }); + + it('should undo a transaction that spilled the page cache', async function() { + await sqlite3.exec(db, 'CREATE TABLE t(x)'); + await sqlite3.exec(db, "INSERT INTO t VALUES ('before')"); + + // Large enough that SQLite must write pages before the commit, which + // is what makes it journal them. + await sqlite3.exec(db, 'BEGIN'); + await sqlite3.exec(db, ` + INSERT INTO t WITH RECURSIVE c(x) AS + (SELECT 1 UNION ALL SELECT x + 1 FROM c WHERE x < 200000) + SELECT x FROM c`); + await sqlite3.exec(db, 'ROLLBACK'); + + const integrity = []; + await sqlite3.exec(db, 'PRAGMA integrity_check', + Comlink.proxy(row => { integrity.push(row[0]); })); + expect(integrity).toEqual(['ok']); + + const rows = []; + await sqlite3.exec(db, 'SELECT x FROM t', + Comlink.proxy(row => { rows.push(row[0]); })); + expect(rows).toEqual(['before']); + }); + }); +}