diff --git a/integration-tests/tests/async.test.js b/integration-tests/tests/async.test.js index f6536a7..aabf4d5 100644 --- a/integration-tests/tests/async.test.js +++ b/integration-tests/tests/async.test.js @@ -987,6 +987,27 @@ test.serial("Database.batch() rejects non-array argument", async (t) => { await t.throwsAsync(() => db.batch("SELECT 1"), { instanceOf: TypeError }); }); +test.serial("A statement that failed with SQLITE_BUSY does not block the next COMMIT", async (t) => { + const path = genDatabaseFilename(); + const [holder] = await connect(path); + await holder.exec("PRAGMA journal_mode=WAL"); + await holder.exec("CREATE TABLE t(x)"); + const [db] = await connect(path, { timeout: 50 }); + await holder.exec("BEGIN IMMEDIATE"); + // Keep the failed statement referenced so that garbage collection cannot finalize it. + const begin = await db.prepare("BEGIN IMMEDIATE"); + await t.throwsAsync(() => begin.run(), { code: "SQLITE_BUSY" }); + await holder.exec("ROLLBACK"); + await (await db.prepare("BEGIN IMMEDIATE")).run(); + await (await db.prepare("INSERT INTO t VALUES (1)")).run(); + await (await db.prepare("COMMIT")).run(); + const row = await (await db.prepare("SELECT count(*) AS n FROM t")).get(); + t.is(row.n, 1); + db.close(); + holder.close(); + for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(path + suffix, { force: true }); +}); + const connect = async (path_opt, options = {}) => { const path = path_opt ?? "hello.db"; const provider = process.env.PROVIDER; diff --git a/integration-tests/tests/sync.test.js b/integration-tests/tests/sync.test.js index 7bcb05c..1a5540b 100644 --- a/integration-tests/tests/sync.test.js +++ b/integration-tests/tests/sync.test.js @@ -801,6 +801,26 @@ test.serial("Database.batch() rejects non-array argument", async (t) => { t.throws(() => db.batch("SELECT 1"), { instanceOf: TypeError }); }); +test.serial("A statement that failed with SQLITE_BUSY does not block the next COMMIT", async (t) => { + const path = genDatabaseFilename(); + const [holder] = await connect(path); + holder.exec("PRAGMA journal_mode=WAL"); + holder.exec("CREATE TABLE t(x)"); + const [db, errorType] = await connect(path, { timeout: 50 }); + holder.exec("BEGIN IMMEDIATE"); + // Keep the failed statement referenced so that garbage collection cannot finalize it. + const begin = db.prepare("BEGIN IMMEDIATE"); + t.throws(() => begin.run(), { instanceOf: errorType, code: "SQLITE_BUSY" }); + holder.exec("ROLLBACK"); + db.prepare("BEGIN IMMEDIATE").run(); + db.prepare("INSERT INTO t VALUES (1)").run(); + db.prepare("COMMIT").run(); + t.is(db.prepare("SELECT count(*) AS n FROM t").get().n, 1); + db.close(); + holder.close(); + for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(path + suffix, { force: true }); +}); + const connect = async (path_opt, options = {}) => { const path = path_opt ?? "hello.db"; const provider = process.env.PROVIDER; diff --git a/src/lib.rs b/src/lib.rs index d4c0270..7a321f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1066,7 +1066,11 @@ impl Statement { let future = async move { let _timeout_guard = register_timeout(&stmt, query_timeout); - stmt.run(params).await.map_err(Error::from)?; + let result = stmt.run(params).await; + if result.is_err() { + stmt.reset(); + } + result.map_err(Error::from)?; let changes = if conn.total_changes() == total_changes_before { 0 } else { @@ -1386,7 +1390,11 @@ pub fn statement_run_sync( let total_changes_before = conn.total_changes(); let start = std::time::Instant::now(); - inner_stmt.run(params).await.map_err(Error::from)?; + let result = inner_stmt.run(params).await; + if result.is_err() { + inner_stmt.reset(); + } + result.map_err(Error::from)?; let changes = if conn.total_changes() == total_changes_before { 0 } else {