From 19635f48ba4cd5b0e3db30d1109e2c0242c467e3 Mon Sep 17 00:00:00 2001 From: epsilonode Date: Sun, 26 Jul 2026 11:46:24 -0700 Subject: [PATCH 1/4] fix(db): preserve node sqlite open semantics --- src/lib/db/adapters/driverFactory.ts | 129 ++++++++++------- tests/unit/db-adapters/driverFactory.test.ts | 142 ++++++++++++++++++- 2 files changed, 217 insertions(+), 54 deletions(-) diff --git a/src/lib/db/adapters/driverFactory.ts b/src/lib/db/adapters/driverFactory.ts index 5025890962f6..008fd8225b2c 100644 --- a/src/lib/db/adapters/driverFactory.ts +++ b/src/lib/db/adapters/driverFactory.ts @@ -10,6 +10,17 @@ import type { SqliteAdapter } from "./types"; const _require = createRequire(import.meta.url); +type DriverLoader = (moduleName: string) => unknown; + +type NodeSqliteOptions = { + readOnly?: boolean; +}; + +function toNodeSqliteOptions(options?: Record): NodeSqliteOptions | undefined { + if (options?.readonly !== true) return undefined; + return { readOnly: true }; +} + /** * Logs the underlying cause of a swallowed sync-driver failure (#7288 * secondary finding). tryOpenSync() used to swallow both driver errors in @@ -67,70 +78,82 @@ function getSqlJsPendingCache(): Map> { return globalThis.__omnirouteSqlJsInitPromises; } -/** Tenta abrir com better-sqlite3 e node:sqlite sincronamente. Retorna null se ambos falharem. */ -export function tryOpenSync( - filePath: string, - options?: Record -): SqliteAdapter | null { - // Bun ships a supported SQLite implementation. Prefer it over the native - // Node addon, which Bun intentionally skips because its ABI is incompatible. - if (process.versions.bun) { - try { - const { Database } = _require("bun:sqlite") as { - Database: new (p: string, options?: Record) => BunSqliteDatabaseLike; - }; - if ( - options?.fileMustExist === true && - filePath !== ":memory:" && - !existsSync(filePath) - ) { - throw new Error(`SQLite file does not exist: ${filePath}`); +/** + * @internal + * + * Builds the synchronous driver cascade. Keeping the loader injectable makes + * the real node:sqlite branch testable without changing the public adapter API. + */ +export function createSyncDriverFactory(load: DriverLoader) { + return function tryOpenSync( + filePath: string, + options?: Record + ): SqliteAdapter | null { + // Bun ships a supported SQLite implementation. Prefer it over the native + // Node addon, which Bun intentionally skips because its ABI is incompatible. + if (process.versions.bun) { + try { + const { Database } = load("bun:sqlite") as { + Database: new (p: string, options?: Record) => BunSqliteDatabaseLike; + }; + if (options?.fileMustExist === true && filePath !== ":memory:" && !existsSync(filePath)) { + throw new Error(`SQLite file does not exist: ${filePath}`); + } + const db = new Database(filePath, { + ...(options?.readonly === true + ? { readonly: true } + : { readwrite: true, create: options?.fileMustExist !== true }), + }); + return createBunSqliteAdapter(db, filePath); + } catch (err) { + logSwallowedDriverError("bun:sqlite", err); } - const db = new Database(filePath, { - ...(options?.readonly === true - ? { readonly: true } - : { readwrite: true, create: options?.fileMustExist !== true }), - }); - return createBunSqliteAdapter(db, filePath); - } catch (err) { - logSwallowedDriverError("bun:sqlite", err); - } - } - - // better-sqlite3: rápido, nativo — skip em Bun - if (!process.versions.bun) { - try { - const BetterSqlite = _require("better-sqlite3") as { - new (p: string, o?: object): import("better-sqlite3").Database; - }; - const db = new BetterSqlite(filePath, options); - return createBetterSqliteAdapter(db); - } catch (err) { - // continua para próximo driver - logSwallowedDriverError("better-sqlite3", err); } - } - // node:sqlite: built-in desde Node 22.5 — skip em Bun - if (!process.versions.bun) { - const [maj, min] = (process.versions.node ?? "0.0").split(".").map(Number); - if (maj > 22 || (maj === 22 && min >= 5)) { + // better-sqlite3: rápido, nativo — skip em Bun + if (!process.versions.bun) { try { - const { DatabaseSync } = _require("node:sqlite") as { - DatabaseSync: new (p: string) => NodeSqliteDatabaseLike; + const BetterSqlite = load("better-sqlite3") as { + new (p: string, o?: object): import("better-sqlite3").Database; }; - const db = new DatabaseSync(filePath); - return createNodeSqliteAdapterFromDatabase(db, filePath); + const db = new BetterSqlite(filePath, options); + return createBetterSqliteAdapter(db); } catch (err) { - // continua - logSwallowedDriverError("node:sqlite", err); + // continua para próximo driver + logSwallowedDriverError("better-sqlite3", err); } } - } - return null; + // node:sqlite: built-in desde Node 22.5 — skip em Bun + if (!process.versions.bun) { + const [maj, min] = (process.versions.node ?? "0.0").split(".").map(Number); + if (maj > 22 || (maj === 22 && min >= 5)) { + try { + if (options?.fileMustExist === true && filePath !== ":memory:" && !existsSync(filePath)) { + throw new Error(`SQLite file does not exist: ${filePath}`); + } + const { DatabaseSync } = load("node:sqlite") as { + DatabaseSync: new (p: string, options?: NodeSqliteOptions) => NodeSqliteDatabaseLike; + }; + const nodeOptions = toNodeSqliteOptions(options); + const db = nodeOptions + ? new DatabaseSync(filePath, nodeOptions) + : new DatabaseSync(filePath); + return createNodeSqliteAdapterFromDatabase(db, filePath); + } catch (err) { + // continua + logSwallowedDriverError("node:sqlite", err); + } + } + } + + return null; + }; } +/** Tenta abrir com better-sqlite3 e node:sqlite sincronamente. Retorna null se ambos falharem. */ +export const tryOpenSync = createSyncDriverFactory(_require); + /** * Pré-inicializa sql.js para um filePath. * Armazena em globalThis para acesso posterior via getSqlJsAdapter(). diff --git a/tests/unit/db-adapters/driverFactory.test.ts b/tests/unit/db-adapters/driverFactory.test.ts index d17d67346b97..8116e51c7c99 100644 --- a/tests/unit/db-adapters/driverFactory.test.ts +++ b/tests/unit/db-adapters/driverFactory.test.ts @@ -1,9 +1,31 @@ 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 { tryOpenSync, openDatabaseAsync, preInitSqlJs, getSqlJsAdapter } = +const { createSyncDriverFactory, tryOpenSync, openDatabaseAsync, preInitSqlJs, getSqlJsAdapter } = await import("../../../src/lib/db/adapters/driverFactory.ts"); +const require = createRequire(import.meta.url); + +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[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:"); @@ -19,6 +41,124 @@ describe("driverFactory", () => { } }); + 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)); From 3166fec73570353aeba1e9970711ecd234e6d1e4 Mon Sep 17 00:00:00 2001 From: epsilonode Date: Sun, 26 Jul 2026 14:27:09 -0700 Subject: [PATCH 2/4] test(db): keep node sqlite coverage off Bun --- changelog.d/fixes/0000-native-sqlite-fallback.md | 1 + tests/unit/db-adapters/driverFactory.test.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 changelog.d/fixes/0000-native-sqlite-fallback.md diff --git a/changelog.d/fixes/0000-native-sqlite-fallback.md b/changelog.d/fixes/0000-native-sqlite-fallback.md new file mode 100644 index 000000000000..4c09beb7437c --- /dev/null +++ b/changelog.d/fixes/0000-native-sqlite-fallback.md @@ -0,0 +1 @@ +- **fix(db):** fall back to Node's built-in SQLite driver when better-sqlite3 is unavailable, while preserving read-only and existing-file safeguards diff --git a/tests/unit/db-adapters/driverFactory.test.ts b/tests/unit/db-adapters/driverFactory.test.ts index 8116e51c7c99..10a832434f1e 100644 --- a/tests/unit/db-adapters/driverFactory.test.ts +++ b/tests/unit/db-adapters/driverFactory.test.ts @@ -9,6 +9,7 @@ const { createSyncDriverFactory, tryOpenSync, openDatabaseAsync, preInitSqlJs, g 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) => { @@ -42,6 +43,8 @@ describe("driverFactory", () => { }); test("prefers better-sqlite3 when it loads", (t) => { + if (isBun) return; + const adapter = tryOpenSync(":memory:"); if (!adapter || adapter.driver !== "better-sqlite3") { adapter?.close(); @@ -55,6 +58,8 @@ describe("driverFactory", () => { }); test("prefers better-sqlite3 before node:sqlite in the driver cascade", () => { + if (isBun) return; + const fakeBetterSqlite = { close() {}, name: ":memory:", @@ -81,6 +86,8 @@ describe("driverFactory", () => { }); test("forced node:sqlite fallback creates, reopens, and queries a writable database", (t) => { + if (isBun) return; + const databasePath = createTempDatabasePath(t); const openNodeSqlite = forceNodeSqlite(); @@ -102,6 +109,8 @@ describe("driverFactory", () => { }); test("forced node:sqlite fallback does not create missing existing-only paths", (t) => { + if (isBun) return; + const databasePath = createTempDatabasePath(t); const adapter = forceNodeSqlite()(databasePath, { readonly: true, fileMustExist: true }); @@ -112,6 +121,8 @@ describe("driverFactory", () => { }); test("forced node:sqlite fallback preserves existing read-only behavior", (t) => { + if (isBun) return; + const databasePath = createTempDatabasePath(t); const { DatabaseSync } = require("node:sqlite") as { DatabaseSync: new (filePath: string) => { @@ -140,6 +151,8 @@ describe("driverFactory", () => { }); test("forced node:sqlite fallback keeps extension loading disabled", () => { + if (isBun) return; + const adapter = forceNodeSqlite()(":memory:"); assert.ok(adapter); assert.equal(adapter.driver, "node:sqlite"); From 6562ac7c50d7494286c76008a2b5d3057f8f39d1 Mon Sep 17 00:00:00 2001 From: epsilonode Date: Sun, 26 Jul 2026 14:37:05 -0700 Subject: [PATCH 3/4] chore(changelog): number native sqlite fallback --- ...0-native-sqlite-fallback.md => 8724-native-sqlite-fallback.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/fixes/{0000-native-sqlite-fallback.md => 8724-native-sqlite-fallback.md} (100%) diff --git a/changelog.d/fixes/0000-native-sqlite-fallback.md b/changelog.d/fixes/8724-native-sqlite-fallback.md similarity index 100% rename from changelog.d/fixes/0000-native-sqlite-fallback.md rename to changelog.d/fixes/8724-native-sqlite-fallback.md From 2bf66675a2eaca3e7d8ae95f8eb5d35018b8b414 Mon Sep 17 00:00:00 2001 From: epsilonode Date: Sun, 26 Jul 2026 15:05:44 -0700 Subject: [PATCH 4/4] test(db): register node sqlite tests only under Node --- tests/unit/db-adapters/driverFactory.test.ts | 208 +++++++++---------- 1 file changed, 99 insertions(+), 109 deletions(-) diff --git a/tests/unit/db-adapters/driverFactory.test.ts b/tests/unit/db-adapters/driverFactory.test.ts index 10a832434f1e..351da141dc21 100644 --- a/tests/unit/db-adapters/driverFactory.test.ts +++ b/tests/unit/db-adapters/driverFactory.test.ts @@ -42,127 +42,117 @@ describe("driverFactory", () => { } }); - test("prefers better-sqlite3 when it loads", (t) => { - if (isBun) return; - - 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", () => { - if (isBun) return; - - 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; + 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; } - 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) => { - if (isBun) return; - - 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(); - }); + assert.ok(adapter); + assert.equal(adapter.driver, "better-sqlite3"); + adapter.close(); + }); - test("forced node:sqlite fallback does not create missing existing-only paths", (t) => { - if (isBun) return; + 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(); + }); - const databasePath = createTempDatabasePath(t); - const adapter = forceNodeSqlite()(databasePath, { readonly: true, fileMustExist: true }); + 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(); + }); - 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 does not create missing existing-only paths", (t) => { + const databasePath = createTempDatabasePath(t); + const adapter = forceNodeSqlite()(databasePath, { readonly: true, fileMustExist: true }); - test("forced node:sqlite fallback preserves existing read-only behavior", (t) => { - if (isBun) return; + assert.equal(adapter, null); + assert.equal(fs.existsSync(databasePath), false); + assert.equal(fs.existsSync(`${databasePath}-wal`), false); + assert.equal(fs.existsSync(`${databasePath}-shm`), false); + }); - const databasePath = createTempDatabasePath(t); - const { DatabaseSync } = require("node:sqlite") as { - DatabaseSync: new (filePath: string) => { - close(): void; - exec(sql: string): void; + 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"); - }); + 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(); - test("forced node:sqlite fallback keeps extension loading disabled", () => { - if (isBun) return; + const check = new DatabaseSync(databasePath); + const row = check.prepare("SELECT value FROM items").get() as { value: string }; + check.close(); + assert.equal(row.value, "seed"); + }); - const adapter = forceNodeSqlite()(":memory:"); - assert.ok(adapter); - assert.equal(adapter.driver, "node:sqlite"); + 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", + const raw = adapter.raw as { loadExtension(path: string): void }; + assert.throws(() => raw.loadExtension("not-a-trusted-extension"), { + code: "ERR_INVALID_STATE", + }); + adapter.close(); }); - adapter.close(); - }); + } test("retains the existing cascade when native drivers are unavailable", () => { const openWithoutNativeDrivers = createSyncDriverFactory(() => {