mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
169 lines
6.8 KiB
TypeScript
169 lines
6.8 KiB
TypeScript
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<void>((resolve) => {
|
|
markFirstStarted = resolve;
|
|
});
|
|
let releaseFirst!: () => void;
|
|
const firstGate = new Promise<void>((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<void>((resolve) => {
|
|
markTransactionStarted = resolve;
|
|
});
|
|
let releaseTransaction!: () => void;
|
|
const transactionGate = new Promise<void>((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<void>;
|
|
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();
|
|
});
|
|
});
|