import { describe, expect, it } from "vitest"; import type { SqliteExecutor } from "../../src/sqlite/index.ts"; import { openNodeSqliteDatabase } from "../../src/sqlite/node.ts"; describe("Node SQLite driver", () => { it("configures connection defaults and accepts explicit settings", async () => { for (const options of [{}, { busyTimeoutMs: 123, walAutoCheckpointPages: 0 }]) { const database = await openNodeSqliteDatabase(":memory:", options); try { expect(await database.get("PRAGMA busy_timeout")).toEqual({ timeout: options.busyTimeoutMs ?? 5_000 }); expect(await database.get("PRAGMA wal_autocheckpoint")).toEqual({ wal_autocheckpoint: options.walAutoCheckpointPages ?? 1_000, }); expect(await database.get("PRAGMA synchronous")).toEqual({ synchronous: 1 }); } finally { await database.close(); } } }); it("commits work done through the transaction handle and closes idempotently", async () => { const database = await openNodeSqliteDatabase(":memory:"); await database.transaction(async (transaction) => { await transaction.exec("CREATE TABLE async_probe (value INTEGER)"); await transaction.run("INSERT INTO async_probe (value) VALUES (?)", 1); }); expect(await database.get("SELECT value FROM async_probe")).toEqual({ value: 1 }); await database.close(); await database.close(); }); it("serializes concurrent transactions", async () => { const database = await openNodeSqliteDatabase(":memory:"); await database.exec("CREATE TABLE transaction_queue (value INTEGER)"); let markFirstStarted!: () => void; const firstStarted = new Promise((resolve) => { markFirstStarted = resolve; }); let releaseFirst!: () => void; const firstGate = new Promise((resolve) => { releaseFirst = resolve; }); const first = database.transaction(async (transaction) => { await transaction.exec("INSERT INTO transaction_queue (value) VALUES (1)"); markFirstStarted(); await firstGate; }); await firstStarted; let secondStarted = false; const second = database.transaction(async (transaction) => { secondStarted = true; await transaction.exec("INSERT INTO transaction_queue (value) VALUES (2)"); }); await Promise.resolve(); expect(secondStarted).toBe(false); releaseFirst(); await Promise.all([first, second]); expect(await database.all("SELECT value FROM transaction_queue ORDER BY value")).toEqual([ { value: 1 }, { value: 2 }, ]); await database.close(); }); it("runs operations in call order whether they start immediately or wait", async () => { const database = await openNodeSqliteDatabase(":memory:"); await database.exec("CREATE TABLE call_order (value INTEGER)"); // Operations called during a transaction must neither see its uncommitted rows nor join its rollback. const transaction = database.transaction(async (handle) => { await handle.run("INSERT INTO call_order (value) VALUES (?)", 1); await Promise.resolve(); throw new Error("roll back"); }); const beforeWrite = database.all("SELECT value FROM call_order ORDER BY value"); const write = database.run("INSERT INTO call_order (value) VALUES (?)", 2); const afterWrite = database.all("SELECT value FROM call_order ORDER BY value"); await expect(transaction).rejects.toThrow("roll back"); await write; expect(await beforeWrite).toEqual([]); expect(await afterWrite).toEqual([{ value: 2 }]); await database.close(); }); it("queues ordinary operations behind an active transaction", async () => { const database = await openNodeSqliteDatabase(":memory:"); await database.exec("CREATE TABLE operation_queue (value INTEGER)"); let markTransactionStarted!: () => void; const transactionStarted = new Promise((resolve) => { markTransactionStarted = resolve; }); let releaseTransaction!: () => void; const transactionGate = new Promise((resolve) => { releaseTransaction = resolve; }); const pending = database.transaction(async (transaction) => { await transaction.exec("INSERT INTO operation_queue (value) VALUES (1)"); markTransactionStarted(); await transactionGate; }); await transactionStarted; let writeSettled = false; const write = database.exec("INSERT INTO operation_queue (value) VALUES (2)").finally(() => { writeSettled = true; }); let readSettled = false; const read = database.all("SELECT value FROM operation_queue ORDER BY value").finally(() => { readSettled = true; }); await Promise.resolve(); expect(writeSettled).toBe(false); expect(readSettled).toBe(false); releaseTransaction(); await pending; await write; await expect(read).resolves.toEqual([{ value: 1 }, { value: 2 }]); await database.close(); }); it("queues database calls made synchronously by a transaction that started immediately", async () => { const database = await openNodeSqliteDatabase(":memory:"); await database.exec("CREATE TABLE barrier_probe (value INTEGER)"); let outside!: Promise; const transaction = database.transaction(async (handle) => { // Misuse: this call must wait for the transaction instead of joining it. outside = database.run("INSERT INTO barrier_probe (value) VALUES (?)", 2); await handle.run("INSERT INTO barrier_probe (value) VALUES (?)", 1); await Promise.resolve(); throw new Error("roll back"); }); await expect(transaction).rejects.toThrow("roll back"); await outside; expect(await database.all("SELECT value FROM barrier_probe")).toEqual([{ value: 2 }]); await database.close(); }); it("rejects a transaction handle used after its transaction settles", async () => { const database = await openNodeSqliteDatabase(":memory:"); await database.exec("CREATE TABLE stale_probe (value INTEGER)"); let handle!: SqliteExecutor; await database.transaction(async (transaction) => { handle = transaction; await transaction.run("INSERT INTO stale_probe (value) VALUES (?)", 1); }); const stale = "SQLite transaction handle is no longer active"; await expect(handle.exec("INSERT INTO stale_probe (value) VALUES (2)")).rejects.toThrow(stale); await expect(handle.run("INSERT INTO stale_probe (value) VALUES (?)", 3)).rejects.toThrow(stale); expect(await database.all("SELECT value FROM stale_probe")).toEqual([{ value: 1 }]); await database.close(); }); it("does not preserve the callback error when rollback itself fails", async () => { const database = await openNodeSqliteDatabase(":memory:"); await database.exec("CREATE TABLE rollback_probe (value INTEGER)"); await expect( database.transaction(async (transaction) => { await transaction.exec("INSERT INTO rollback_probe (value) VALUES (1)"); await transaction.exec("COMMIT"); throw new Error("rejected after an escaped commit"); }), ).rejects.toThrow(AggregateError); expect(await database.get("SELECT value FROM rollback_probe")).toEqual({ value: 1 }); await database.close(); }); });