Files
OmniRoute/tests/unit/db-adapters/driverFactory.test.ts
epsilonode 296765fd6a fix(db): preserve readonly and existing-file semantics in node:sqlite fallback (#8724)
* fix(db): preserve node sqlite open semantics

* test(db): keep node sqlite coverage off Bun

* chore(changelog): number native sqlite fallback

* test(db): register node sqlite tests only under Node
2026-07-27 17:25:08 -03:00

309 lines
11 KiB
TypeScript

import { test, describe } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
const { createSyncDriverFactory, tryOpenSync, openDatabaseAsync, preInitSqlJs, getSqlJsAdapter } =
await import("../../../src/lib/db/adapters/driverFactory.ts");
const require = createRequire(import.meta.url);
const isBun = Boolean(process.versions.bun);
function forceNodeSqlite() {
return createSyncDriverFactory((moduleName: string) => {
if (moduleName === "better-sqlite3") {
throw new Error("forced better-sqlite3 load failure");
}
return require(moduleName);
});
}
function createTempDatabasePath(t: Parameters<typeof test>[1]) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-node-sqlite-"));
const databasePath = path.join(dir, "database.sqlite");
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
return databasePath;
}
describe("driverFactory", () => {
test("tryOpenSync retorna adapter síncrono ou null", () => {
const adapter = tryOpenSync(":memory:");
if (adapter) {
assert.ok(["better-sqlite3", "node:sqlite", "bun:sqlite"].includes(adapter.driver));
adapter.exec("CREATE TABLE t (v TEXT)");
adapter.prepare("INSERT INTO t VALUES (?)").run("ok");
const row = adapter.prepare("SELECT v FROM t").get() as { v: string };
assert.equal(row.v, "ok");
adapter.close();
} else {
assert.equal(adapter, null);
}
});
if (!isBun) {
test("prefers better-sqlite3 when it loads", (t) => {
const adapter = tryOpenSync(":memory:");
if (!adapter || adapter.driver !== "better-sqlite3") {
adapter?.close();
t.skip("better-sqlite3 is not available in this environment");
return;
}
assert.ok(adapter);
assert.equal(adapter.driver, "better-sqlite3");
adapter.close();
});
test("prefers better-sqlite3 before node:sqlite in the driver cascade", () => {
const fakeBetterSqlite = {
close() {},
name: ":memory:",
open: true,
};
let nodeSqliteRequested = false;
const openWithPreferredDriver = createSyncDriverFactory((moduleName: string) => {
if (moduleName === "better-sqlite3") {
return function FakeBetterSqlite() {
return fakeBetterSqlite;
};
}
if (moduleName === "node:sqlite") {
nodeSqliteRequested = true;
}
throw new Error(`unexpected driver load: ${moduleName}`);
});
const adapter = openWithPreferredDriver(":memory:");
assert.ok(adapter);
assert.equal(adapter.driver, "better-sqlite3");
assert.equal(nodeSqliteRequested, false);
adapter.close();
});
test("forced node:sqlite fallback creates, reopens, and queries a writable database", (t) => {
const databasePath = createTempDatabasePath(t);
const openNodeSqlite = forceNodeSqlite();
const writer = openNodeSqlite(databasePath);
assert.ok(writer);
assert.equal(writer.driver, "node:sqlite");
writer.exec("CREATE TABLE items (value TEXT)");
writer.prepare("INSERT INTO items VALUES (?)").run("native");
writer.close();
const reader = openNodeSqlite(databasePath);
assert.ok(reader);
assert.equal(reader.driver, "node:sqlite");
assert.equal(
(reader.prepare("SELECT value FROM items").get() as { value: string }).value,
"native"
);
reader.close();
});
test("forced node:sqlite fallback does not create missing existing-only paths", (t) => {
const databasePath = createTempDatabasePath(t);
const adapter = forceNodeSqlite()(databasePath, { readonly: true, fileMustExist: true });
assert.equal(adapter, null);
assert.equal(fs.existsSync(databasePath), false);
assert.equal(fs.existsSync(`${databasePath}-wal`), false);
assert.equal(fs.existsSync(`${databasePath}-shm`), false);
});
test("forced node:sqlite fallback preserves existing read-only behavior", (t) => {
const databasePath = createTempDatabasePath(t);
const { DatabaseSync } = require("node:sqlite") as {
DatabaseSync: new (filePath: string) => {
close(): void;
exec(sql: string): void;
};
};
const seed = new DatabaseSync(databasePath);
seed.exec("CREATE TABLE items (value TEXT); INSERT INTO items VALUES ('seed');");
seed.close();
const adapter = forceNodeSqlite()(databasePath, { readonly: true, fileMustExist: true });
assert.ok(adapter);
assert.equal(adapter.driver, "node:sqlite");
assert.equal(
(adapter.prepare("SELECT value FROM items").get() as { value: string }).value,
"seed"
);
assert.throws(() => adapter.exec("INSERT INTO items VALUES ('write')"));
adapter.close();
const check = new DatabaseSync(databasePath);
const row = check.prepare("SELECT value FROM items").get() as { value: string };
check.close();
assert.equal(row.value, "seed");
});
test("forced node:sqlite fallback keeps extension loading disabled", () => {
const adapter = forceNodeSqlite()(":memory:");
assert.ok(adapter);
assert.equal(adapter.driver, "node:sqlite");
const raw = adapter.raw as { loadExtension(path: string): void };
assert.throws(() => raw.loadExtension("not-a-trusted-extension"), {
code: "ERR_INVALID_STATE",
});
adapter.close();
});
}
test("retains the existing cascade when native drivers are unavailable", () => {
const openWithoutNativeDrivers = createSyncDriverFactory(() => {
throw new Error("forced driver load failure");
});
assert.equal(openWithoutNativeDrivers(":memory:"), null);
});
test("openDatabaseAsync sempre retorna um adapter válido", async () => {
const adapter = await openDatabaseAsync(":memory:");
assert.ok(["better-sqlite3", "node:sqlite", "bun:sqlite", "sql.js"].includes(adapter.driver));
adapter.exec("CREATE TABLE t (v TEXT)");
adapter.prepare("INSERT INTO t VALUES (?)").run("ok");
const row = adapter.prepare("SELECT v FROM t").get() as { v: string };
assert.equal(row.v, "ok");
adapter.close();
});
test("preInitSqlJs cacheia o adapter por filePath", async () => {
const path = `sqljs_cache_test_${Date.now()}`;
const adapter1 = await preInitSqlJs(path);
const adapter2 = await preInitSqlJs(path);
assert.equal(adapter1, adapter2, "Deve retornar o mesmo adapter cacheado");
adapter1.close();
});
test("getSqlJsAdapter retorna null para path não inicializado", () => {
const unique = `not_initialized_${Date.now()}`;
assert.equal(getSqlJsAdapter(unique), null);
});
test("getSqlJsAdapter retorna adapter após preInitSqlJs", async () => {
const path = `sqljs_get_test_${Date.now()}`;
await preInitSqlJs(path);
const adapter = getSqlJsAdapter(path);
assert.ok(adapter !== null);
assert.equal(adapter!.driver, "sql.js");
adapter!.close();
});
test("openDatabaseAsync suporta operações CRUD completas", async (t) => {
const os = await import("node:os");
const path = await import("node:path");
const fs = await import("node:fs");
const tmpFile = path.join(os.tmpdir(), `driver_crud_${Date.now()}.sqlite`);
t.after(() => {
try {
fs.unlinkSync(tmpFile);
} catch {}
});
const adapter = await openDatabaseAsync(tmpFile);
adapter.exec("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)");
const r1 = adapter.prepare("INSERT INTO items (name, qty) VALUES (?, ?)").run("apple", 5);
const r2 = adapter.prepare("INSERT INTO items (name, qty) VALUES (?, ?)").run("banana", 3);
assert.equal(r1.changes, 1);
assert.equal(r2.changes, 1);
const rows = adapter.prepare("SELECT * FROM items ORDER BY id").all() as Array<{
id: number;
name: string;
qty: number;
}>;
assert.equal(rows.length, 2);
assert.equal(rows[0].name, "apple");
assert.equal(rows[1].name, "banana");
adapter.close();
});
// #6628 (remaining gap): concurrent preInitSqlJs() calls for the same
// filePath must share ONE in-flight load instead of each caller
// independently fs.readFileSync + WASM-decoding the whole file — the
// thundering-herd amplifier of the OOM condition #6632 already partly
// fixed (restore-cycle-breaker + OOM early-abort), left un-implemented by
// the reporter's own proposed promise-sharing fix.
test("preInitSqlJs shares one in-flight load across concurrent callers", async (t) => {
const os = await import("node:os");
const path = await import("node:path");
// The dynamic-import namespace object is read-only; grab the mutable CJS
// `.default` (== module.exports) so readFileSync can be monkeypatched.
const fsNs = await import("node:fs");
const fs = fsNs.default;
const tmpFile = path.join(os.tmpdir(), `sqljs_race_${Date.now()}.sqlite`);
fs.writeFileSync(tmpFile, Buffer.alloc(1024 * 1024, 1));
t.after(() => {
try {
fs.unlinkSync(tmpFile);
} catch {}
});
let readCountForTarget = 0;
const originalReadFileSync = fs.readFileSync;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(fs as any).readFileSync = (...args: Parameters<typeof fs.readFileSync>) => {
if (args[0] === tmpFile) readCountForTarget += 1;
return originalReadFileSync(...args);
};
t.after(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(fs as any).readFileSync = originalReadFileSync;
});
const [a, b, c] = await Promise.all([
preInitSqlJs(tmpFile),
preInitSqlJs(tmpFile),
preInitSqlJs(tmpFile),
]);
assert.equal(
readCountForTarget,
1,
`expected exactly 1 shared full-file read for 3 concurrent preInitSqlJs() calls, got ${readCountForTarget}`
);
assert.equal(a, b, "concurrent callers must resolve to the SAME adapter instance");
assert.equal(b, c, "concurrent callers must resolve to the SAME adapter instance");
a.close();
});
test("cross-driver: escreve com adapter sync, relê com sql.js", async (t) => {
const os = await import("node:os");
const path = await import("node:path");
const fs = await import("node:fs");
const tmpFile = path.join(os.tmpdir(), `cross_driver_${Date.now()}.sqlite`);
t.after(() => {
try {
fs.unlinkSync(tmpFile);
} catch {}
});
const syncAdapter = tryOpenSync(tmpFile);
if (!syncAdapter) {
console.log("SKIP: nenhum driver síncrono disponível para cross-driver test");
return;
}
syncAdapter.exec("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)");
syncAdapter.prepare("INSERT INTO items (name) VALUES (?)").run("cross-test");
syncAdapter.close();
const { createSqlJsAdapter } = await import("../../../src/lib/db/adapters/sqljsAdapter.ts");
const reader = await createSqlJsAdapter(tmpFile);
const row = reader.prepare("SELECT name FROM items WHERE id = 1").get() as { name: string };
assert.equal(row.name, "cross-test");
reader.close();
});
});