feat: support Bun bundled SQLite runtime (#7878)

* feat: support Bun bundled SQLite runtime

* fix: harden Bun SQLite backups and params

* docs: keep Node as the only supported runtime; document bun:sqlite as best-effort compatibility path

Co-authored-by: Arul Kumaran <arul@luracast.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
This commit is contained in:
Arul Kumaran
2026-07-22 01:00:26 +05:30
committed by GitHub
parent f6d6ad6047
commit edbce2e35b
15 changed files with 460 additions and 21 deletions

View File

@@ -741,6 +741,23 @@ jobs:
path: coverage-shard/*.json
if-no-files-found: error
test-bun-sqlite:
name: Bun SQLite Compatibility
runs-on: ubuntu-latest
timeout-minutes: 10
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run test:bun:db
test-vitest:
name: Vitest (MCP / autoCombo / UI components)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).

View File

@@ -482,8 +482,8 @@ list` shows worktrees you didn't create, leave them alone. End every session wit
## Environment
- **Runtime**: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules. This is the **only** runtime for the published `omniroute` CLI, the server, and the test suites (`node:test` + vitest) — `engines.node` is authoritative and end users never need Bun.
- **Bun (build/dev script runner only)**: Bun `1.3.10` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression`. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the published runtime, or the test runners — those stay on Node. Any new Bun-invoking script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those 5 scripts with `bun: not found`).
- **Runtime**: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules. This is the **only supported** runtime for the published `omniroute` CLI, the server, and the test suites (`node:test` + vitest) — `engines.node` is authoritative and end users never need Bun. A **best-effort `bun:sqlite` compatibility path** exists so a global Bun install (`bun install -g omniroute`) can start without `better-sqlite3` (driver adapter + Bun-aware process spawning); it is **not** a supported runtime — no support guarantees — and every Bun-specific runtime change MUST preserve the Node driver/fallback chain and ship a Bun test (`test:bun:db`) or an explicit reason why the path is Node-only.
- **Bun (build/dev script runner + compatibility smoke only)**: Bun `1.3.14` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression` — plus the focused `test:bun:db` compatibility smoke suite for the best-effort `bun:sqlite` path. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those scripts with `bun: not found`).
- **TypeScript**: 6.0+, target ES2022, module esnext, resolution bundler
- **Path aliases**: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`, `@omniroute/open-sse/*` → `open-sse/*`
- **Default port**: 20128 (API + dashboard on same port)

View File

@@ -134,7 +134,7 @@ export async function runServe(opts = {}) {
"Release",
"better_sqlite3.node"
);
if (existsSync(sqliteBinary) && !isNativeBinaryCompatible(sqliteBinary)) {
if (!process.versions.bun && existsSync(sqliteBinary) && !isNativeBinaryCompatible(sqliteBinary)) {
console.error(
"\x1b[31m✖ better-sqlite3 native module is incompatible with this platform.\x1b[0m"
);
@@ -230,7 +230,10 @@ export async function runServe(opts = {}) {
function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
const server = spawn("node", [...buildNodeHeapArgs(process.env, memoryLimit), serverJs], {
const server = spawn(process.versions.bun ? process.execPath : "node", [
...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)),
serverJs,
], {
cwd: APP_DIR,
env,
stdio: "ignore",
@@ -246,7 +249,10 @@ function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, noOpen, startedAt) {
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
const server = spawn("node", [...buildNodeHeapArgs(process.env, memoryLimit), serverJs], {
const server = spawn(process.versions.bun ? process.execPath : "node", [
...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)),
serverJs,
], {
cwd: APP_DIR,
env,
stdio: "pipe",

View File

@@ -40,7 +40,10 @@ export class ServerSupervisor {
// silently, so a boot that never becomes ready looked like a dead hang with zero
// output even at APP_LOG_LEVEL=debug. Pipe stdout too and buffer it alongside
// stderr so a readiness timeout can surface what the child actually printed.
this.child = spawn("node", [...heapArgs, this.serverPath], {
this.child = spawn(process.versions.bun ? process.execPath : "node", [
...(process.versions.bun ? [] : heapArgs),
this.serverPath,
], {
cwd: dirname(this.serverPath),
env: this.env,
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],

View File

@@ -3,7 +3,10 @@ import { resolveDataDir, resolveStoragePath } from "./data-dir.mjs";
import { ensureProviderSchema } from "./provider-store.mjs";
import { ensureSettingsSchema, hashManagementPassword, updateSettings } from "./settings-store.mjs";
async function loadBetterSqlite() {
async function loadSqlite() {
if (process.versions.bun) {
return (await import("bun:sqlite")).Database;
}
try {
return (await import("better-sqlite3")).default;
} catch {
@@ -11,6 +14,57 @@ async function loadBetterSqlite() {
}
}
function openBunSqlite(Database, dbPath, options) {
const raw = new Database(dbPath, options);
const prepare = (sql) => {
const statement = raw.query(sql);
return {
run: (...params) => statement.run(...normalizeBunSqliteParams(params)),
get: (...params) => statement.get(...normalizeBunSqliteParams(params)),
all: (...params) => statement.all(...normalizeBunSqliteParams(params)),
};
};
return {
prepare,
query: (sql) => raw.query(sql),
exec: (sql) => raw.exec(sql),
transaction: (fn) => raw.transaction(fn),
close: () => raw.close(),
serialize: () => raw.serialize(),
pragma: (pragmaStr, pragmaOptions) => {
const statement = raw.query(`PRAGMA ${pragmaStr}`);
if (pragmaOptions?.simple) {
const row = statement.get();
return row ? (Object.values(row)[0] ?? null) : null;
}
return statement.all();
},
};
}
export function normalizeBunSqliteParams(params) {
if (
params.length !== 1 ||
params[0] === null ||
typeof params[0] !== "object" ||
Array.isArray(params[0]) ||
params[0] instanceof Uint8Array ||
(typeof Buffer !== "undefined" && Buffer.isBuffer(params[0]))
) {
return params;
}
const expanded = {};
for (const [key, value] of Object.entries(params[0])) {
if (/^[:@$]/.test(key)) expanded[key] = value;
else {
expanded[`@${key}`] = value;
expanded[`:${key}`] = value;
expanded[`$${key}`] = value;
}
}
return [expanded];
}
export function createSqliteNativeError(error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("NODE_MODULE_VERSION") || message.includes("ERR_DLOPEN_FAILED")) {
@@ -37,9 +91,19 @@ export function createSqliteNativeError(error) {
}
async function openSqliteDatabase(dbPath, options = {}) {
const Database = await loadBetterSqlite();
const Database = await loadSqlite();
if (process.versions.bun) {
if (options.fileMustExist && !fs.existsSync(dbPath)) {
throw new Error(`SQLite file does not exist: ${dbPath}`);
}
options = options.readonly
? { readonly: true }
: { readwrite: true, create: options.fileMustExist !== true };
}
try {
return new Database(dbPath, options);
return process.versions.bun
? openBunSqlite(Database, dbPath, options)
: new Database(dbPath, options);
} catch (error) {
throw createSqliteNativeError(error);
}
@@ -71,7 +135,16 @@ export async function withReadonlySqlite(dbPath, callback) {
export async function backupSqliteFile(sourcePath, destPath) {
const db = await openSqliteDatabase(sourcePath, { readonly: true });
try {
await db.backup(destPath);
if (typeof db.backup === "function") {
await db.backup(destPath);
} else if (sourcePath === ":memory:" && typeof db.serialize === "function") {
fs.writeFileSync(destPath, Buffer.from(db.serialize()));
} else {
try {
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
} catch {}
fs.copyFileSync(sourcePath, destPath);
}
} finally {
db.close();
}

View File

@@ -112,6 +112,7 @@
"test:unit:shard": "concurrently --kill-others-on-fail -n s1,s2 \"npm:test:unit:shard:1\" \"npm:test:unit:shard:2\"",
"test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=1/2 \"tests/unit/serial/**/*.test.ts\"",
"test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts \"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts\" \"tests/unit/**/*.test.mjs\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 \"tests/unit/dashboard/**/*.test.ts\" && cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=2/2 \"tests/unit/serial/**/*.test.ts\"",
"test:bun:db": "bun test tests/unit/db-adapters/bunSqliteAdapter.test.ts tests/unit/db-adapters/driverFactory.test.ts tests/unit/db-adapters/cliSqlite.test.mjs",
"test:plan3": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/plan3-p0.test.ts",
"test:fixes": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/fixes-p1.test.ts",
"test:security": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/security-fase01.test.ts",

View File

@@ -0,0 +1,151 @@
import fs from "node:fs";
import type { PreparedStatement, RunResult, SqliteAdapter } from "./types";
/**
* The Bun runtime already ships a SQLite driver. Keep this adapter deliberately
* small so the rest of the application can use the same driver contract as
* better-sqlite3, node:sqlite, and sql.js.
*/
export interface BunSqliteDatabaseLike {
query(sql: string): {
run(...params: unknown[]): { changes: number | bigint; lastInsertRowid: number | bigint };
get(...params: unknown[]): unknown;
all(...params: unknown[]): unknown[];
};
exec(sql: string): void;
serialize?(): Uint8Array;
transaction<T>(fn: (...args: unknown[]) => T): {
(...args: unknown[]): T;
immediate?: (...args: unknown[]) => T;
};
close(): void;
}
function normalizeRunResult(result: {
changes?: number | bigint;
lastInsertRowid?: number | bigint;
}): RunResult {
return {
changes: Number(result.changes ?? 0),
lastInsertRowid: Number(result.lastInsertRowid ?? 0),
};
}
function normalizeParams(params: unknown[]): unknown[] {
if (params.length !== 1) return params;
const [first] = params;
if (
first === null ||
typeof first !== "object" ||
Array.isArray(first) ||
first instanceof Uint8Array ||
(typeof Buffer !== "undefined" && Buffer.isBuffer(first))
) {
return params;
}
// better-sqlite3 callers use bare object keys for @name, :name, and $name.
// Bun requires the sigil to match the SQL placeholder, so provide all three
// aliases just as the sql.js adapter does.
const expanded: Record<string, unknown> = {};
for (const [key, value] of Object.entries(first as Record<string, unknown>)) {
if (/^[:@$]/.test(key)) {
expanded[key] = value;
} else {
expanded[`@${key}`] = value;
expanded[`:${key}`] = value;
expanded[`$${key}`] = value;
}
}
return [expanded];
}
export function createBunSqliteAdapter(db: BunSqliteDatabaseLike, filePath: string): SqliteAdapter {
let isOpen = true;
return {
driver: "bun:sqlite",
get open() {
return isOpen;
},
get name() {
return filePath;
},
prepare(sql: string): PreparedStatement {
const statement = db.query(sql);
return {
run(...params: unknown[]): RunResult {
return normalizeRunResult(statement.run(...normalizeParams(params)));
},
get(...params: unknown[]): unknown {
return statement.get(...normalizeParams(params));
},
all(...params: unknown[]): unknown[] {
return statement.all(...normalizeParams(params));
},
};
},
exec(sql: string): void {
db.exec(sql);
},
pragma(pragmaStr: string, options?: { simple?: boolean }): unknown {
const statement = db.query(`PRAGMA ${pragmaStr}`);
if (options?.simple) {
const row = statement.get() as Record<string, unknown> | undefined;
return row ? (Object.values(row)[0] ?? null) : null;
}
return statement.all();
},
transaction<T>(fn: (...args: unknown[]) => T): (...args: unknown[]) => T {
return db.transaction(fn);
},
immediate(fn: () => void): void {
const transaction = db.transaction(fn);
if (typeof transaction.immediate === "function") {
transaction.immediate();
return;
}
db.exec("BEGIN IMMEDIATE");
try {
fn();
db.exec("COMMIT");
} catch (error) {
try {
db.exec("ROLLBACK");
} catch {}
throw error;
}
},
async backup(destination: string): Promise<void> {
if (filePath === ":memory:") return;
try {
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
} catch {}
fs.copyFileSync(filePath, destination);
},
checkpoint(mode = "TRUNCATE"): void {
try {
db.exec(`PRAGMA wal_checkpoint(${mode})`);
} catch {}
},
close(): void {
if (!isOpen) return;
db.close();
isOpen = false;
},
get raw() {
return db;
},
};
}

View File

@@ -1,5 +1,7 @@
import { createRequire } from "node:module";
import { existsSync } from "node:fs";
import { createBetterSqliteAdapter } from "./betterSqliteAdapter";
import { createBunSqliteAdapter, type BunSqliteDatabaseLike } from "./bunSqliteAdapter";
import {
createNodeSqliteAdapterFromDatabase,
type NodeSqliteDatabaseLike,
@@ -70,6 +72,31 @@ export function tryOpenSync(
filePath: string,
options?: Record<string, unknown>
): 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<string, unknown>) => 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);
}
}
// better-sqlite3: rápido, nativo — skip em Bun
if (!process.versions.bun) {
try {
@@ -157,7 +184,7 @@ export function getSqlJsAdapter(filePath: string): SqliteAdapter | null {
/**
* Factory assíncrona completa: tenta todos os drivers em cascata.
* Ordem: better-sqlite3 → node:sqlite → sql.js
* Ordem: bun:sqlite → better-sqlite3 → node:sqlite → sql.js
*/
export async function openDatabaseAsync(
filePath: string,

View File

@@ -1,10 +1,12 @@
// src/lib/db/adapters/sqljsAdapter.ts
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import type { SqliteAdapter, PreparedStatement, RunResult } from "./types";
const SAVE_DEBOUNCE_MS = 100;
const CHECKPOINT_INTERVAL_MS = 60_000;
const _require = createRequire(import.meta.url);
let _sqlJsLib: Awaited<ReturnType<(typeof import("sql.js"))["default"]>> | null = null;
@@ -22,13 +24,30 @@ function resolveSqlJsWasmPath(): string {
),
];
// Global Bun installs do not use the application's cwd as the package root.
// Resolve the actual JavaScript entrypoint so sql.js can find its sibling WASM
// asset when OmniRoute is launched from ~/.bun/install/global.
try {
const sqlJsEntry = _require.resolve("sql.js");
candidatePaths.push(path.join(path.dirname(sqlJsEntry), "sql-wasm.wasm"));
} catch {}
try {
const sqlJsPackage = _require.resolve("sql.js/package.json");
candidatePaths.push(
path.join(path.dirname(sqlJsPackage), "dist", "sql-wasm.wasm"),
path.join(path.dirname(sqlJsPackage), "sql-wasm.wasm")
);
} catch {}
for (const candidatePath of candidatePaths) {
if (fs.existsSync(candidatePath)) {
return candidatePath;
}
}
return candidatePaths[0];
throw new Error(
`[sqljsAdapter] Could not locate sql-wasm.wasm. Checked:\n${candidatePaths.join("\n")}`
);
}
/**

View File

@@ -10,7 +10,7 @@ export interface PreparedStatement {
}
export interface SqliteAdapter {
readonly driver: "better-sqlite3" | "node:sqlite" | "sql.js";
readonly driver: "better-sqlite3" | "node:sqlite" | "bun:sqlite" | "sql.js";
readonly open: boolean;
readonly name: string;

View File

@@ -169,10 +169,13 @@ function openSqliteDatabase(sqliteFile: string, options?: Record<string, unknown
// barrier below) and failed — surface the real cause instead of the
// generic/misleading "not pre-initialized yet" message (#7288).
const preInitError = getSqlJsPreInitError(sqliteFile);
const syncDrivers = process.versions.bun
? "bun:sqlite (failed)"
: "better-sqlite3 (failed), node:sqlite (unavailable)";
if (preInitError) {
throw new Error(
`[DB] Nenhum driver SQLite disponível para '${sqliteFile}'. ` +
"Drivers testados: better-sqlite3 (falhou), node:sqlite (indisponível), " +
`Drivers testados: ${syncDrivers}, ` +
`sql.js (falhou: ${preInitError}).`
);
}
@@ -180,7 +183,7 @@ function openSqliteDatabase(sqliteFile: string, options?: Record<string, unknown
throw new Error(
`[DB] Nenhum driver SQLite disponível para '${sqliteFile}'. ` +
"Chame ensureDbInitialized() no startup. " +
"Drivers testados: better-sqlite3 (falhou), node:sqlite (indisponível). " +
`Drivers testados: ${syncDrivers}. ` +
"sql.js WASM ainda não foi pré-inicializado."
);
}

View File

@@ -1,6 +1,15 @@
import os from "os";
import path from "path";
import Database from "better-sqlite3";
import { createRequire } from "node:module";
const _require = createRequire(import.meta.url);
const Database = process.versions.bun
? (_require("bun:sqlite").Database as typeof import("better-sqlite3"))
: (_require("better-sqlite3") as typeof import("better-sqlite3"));
function databaseOptions(readonly = false) {
return readonly ? { readonly: true } : { readwrite: true, create: true };
}
const getOmpDir = () => path.join(os.homedir(), ".omp", "agent");
const getOmpDbPath = () => path.join(getOmpDir(), "agent.db");
@@ -8,7 +17,7 @@ const getOmpDbPath = () => path.join(getOmpDir(), "agent.db");
export function getOmpCredentials(providerId: string) {
const dbPath = getOmpDbPath();
try {
const db = new Database(dbPath, { readonly: true });
const db = new Database(dbPath, databaseOptions(true));
const row = db
.prepare(
"SELECT data FROM auth_credentials WHERE provider = ? AND credential_type = 'api_key'"
@@ -28,7 +37,7 @@ export function getOmpCredentials(providerId: string) {
export function saveOmpCredentials(providerId: string, apiKey: string, baseUrl: string) {
const dbPath = getOmpDbPath();
const db = new Database(dbPath);
const db = new Database(dbPath, databaseOptions());
db.prepare("DELETE FROM auth_credentials WHERE provider = ?").run(providerId);
db.prepare(
@@ -46,7 +55,7 @@ export function saveOmpCredentials(providerId: string, apiKey: string, baseUrl:
export function deleteOmpCredentials(providerId: string) {
const dbPath = getOmpDbPath();
const db = new Database(dbPath);
const db = new Database(dbPath, databaseOptions());
db.prepare("DELETE FROM auth_credentials WHERE provider = ?").run(providerId);
db.close();
}

View File

@@ -0,0 +1,83 @@
import { test } 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 {
createBunSqliteAdapter,
type BunSqliteDatabaseLike,
} from "../../../src/lib/db/adapters/bunSqliteAdapter.ts";
test("bun:sqlite adapter supports CRUD, pragmas, transactions, and close", async (t) => {
if (!process.versions.bun) {
t.skip("bun:sqlite is only available under Bun");
return;
}
const { Database } = await import("bun:sqlite");
const adapter = createBunSqliteAdapter(new Database(":memory:"), ":memory:");
t.after(() => adapter.close());
adapter.exec("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)");
const result = adapter.prepare("INSERT INTO items (name) VALUES (?)").run("bun");
assert.equal(result.changes, 1);
assert.equal((adapter.prepare("SELECT name FROM items").get() as { name: string }).name, "bun");
assert.equal(adapter.driver, "bun:sqlite");
assert.equal(adapter.pragma("user_version", { simple: true }), 0);
adapter.prepare("INSERT INTO items (name) VALUES (@name)").run({ name: "named" });
assert.equal(
(
adapter.prepare("SELECT name FROM items WHERE name = :name").get({ name: "named" }) as {
name: string;
}
).name,
"named"
);
adapter.transaction(() => {
adapter.prepare("INSERT INTO items (name) VALUES (?)").run("transaction");
})();
assert.equal(adapter.prepare("SELECT COUNT(*) AS count FROM items").get().count, 3);
assert.equal(adapter.open, true);
adapter.close();
assert.equal(adapter.open, false);
});
test("bun:sqlite adapter backs up on-disk databases without serializing them", async (t) => {
const sourcePath = path.join(os.tmpdir(), `bun-sqlite-source-${Date.now()}.sqlite`);
const destinationPath = path.join(os.tmpdir(), `bun-sqlite-destination-${Date.now()}.sqlite`);
const sourceContents = "sqlite fixture";
const execCalls: string[] = [];
fs.writeFileSync(sourcePath, sourceContents);
t.after(() => {
for (const filePath of [sourcePath, destinationPath]) {
try {
fs.unlinkSync(filePath);
} catch {}
}
});
const db = {
query() {
throw new Error("query should not be called during backup");
},
exec(sql: string) {
execCalls.push(sql);
},
transaction() {
throw new Error("transaction should not be called during backup");
},
close() {},
serialize() {
throw new Error("serialize must not be called for an on-disk backup");
},
} as unknown as BunSqliteDatabaseLike;
const adapter = createBunSqliteAdapter(db, sourcePath);
await adapter.backup(destinationPath);
assert.deepEqual(execCalls, ["PRAGMA wal_checkpoint(TRUNCATE)"]);
assert.equal(fs.readFileSync(destinationPath, "utf8"), sourceContents);
});

View File

@@ -0,0 +1,47 @@
import { test } 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 { backupSqliteFile, normalizeBunSqliteParams } from "../../../bin/cli/sqlite.mjs";
test("CLI Bun SQLite parameter normalization preserves binary values", () => {
const typedArray = new Uint8Array([1, 2, 3]);
const buffer = Buffer.from([4, 5, 6]);
assert.deepEqual(normalizeBunSqliteParams([typedArray]), [typedArray]);
assert.deepEqual(normalizeBunSqliteParams([buffer]), [buffer]);
});
test("CLI Bun SQLite parameter normalization expands named object keys", () => {
assert.deepEqual(normalizeBunSqliteParams([{ id: 7 }]), [{ "@id": 7, ":id": 7, $id: 7 }]);
});
test("CLI Bun SQLite backs up physical files without serializing them", async (t) => {
if (!process.versions.bun) {
t.skip("bun:sqlite is only available under Bun");
return;
}
const sourcePath = path.join(os.tmpdir(), `cli-bun-sqlite-source-${Date.now()}.sqlite`);
const destinationPath = path.join(os.tmpdir(), `cli-bun-sqlite-destination-${Date.now()}.sqlite`);
t.after(() => {
for (const filePath of [sourcePath, destinationPath]) {
try {
fs.unlinkSync(filePath);
} catch {}
}
});
const { Database } = await import("bun:sqlite");
const source = new Database(sourcePath);
source.exec("CREATE TABLE items (value TEXT); INSERT INTO items VALUES ('copied')");
source.close();
await backupSqliteFile(sourcePath, destinationPath);
const destination = new Database(destinationPath, { readonly: true });
assert.equal(destination.query("SELECT value FROM items").get().value, "copied");
destination.close();
});

View File

@@ -8,7 +8,7 @@ describe("driverFactory", () => {
test("tryOpenSync retorna adapter síncrono ou null", () => {
const adapter = tryOpenSync(":memory:");
if (adapter) {
assert.ok(["better-sqlite3", "node:sqlite"].includes(adapter.driver));
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 };
@@ -21,7 +21,7 @@ describe("driverFactory", () => {
test("openDatabaseAsync sempre retorna um adapter válido", async () => {
const adapter = await openDatabaseAsync(":memory:");
assert.ok(["better-sqlite3", "node:sqlite", "sql.js"].includes(adapter.driver));
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");