// src/lib/db/adapters/driverFactory.ts import fs from "node:fs"; import { createRequire } from "node:module"; import { createBetterSqliteAdapter } from "./betterSqliteAdapter"; import { createLibsqlAdapter } from "./libsqlAdapter"; import type { SqliteAdapter, PreparedStatement, RunResult } from "./types"; const _require = createRequire(import.meta.url); declare global { var __omnirouteSqlJsAdapters: Map | undefined; } function getSqlJsCache(): Map { if (!globalThis.__omnirouteSqlJsAdapters) { globalThis.__omnirouteSqlJsAdapters = new Map(); } return globalThis.__omnirouteSqlJsAdapters; } function buildNodeAdapterSync( db: { prepare(sql: string): { run(...p: unknown[]): { changes: number | bigint; lastInsertRowid: number | bigint }; get(...p: unknown[]): unknown; all(...p: unknown[]): unknown[]; }; exec(sql: string): void; close(): void; }, filePath: string ): SqliteAdapter { let _isOpen = true; const MAX_STMT_CACHE_SIZE = 200; interface CachedStatement { stmt: ReturnType; sql: string; } const stmtCache = new Map(); function getCached(sql: string) { let entry = stmtCache.get(sql); if (entry) { stmtCache.delete(sql); stmtCache.set(sql, entry); } else { const stmt = db.prepare(sql); if (stmtCache.size >= MAX_STMT_CACHE_SIZE) { const oldestKey = stmtCache.keys().next().value; if (oldestKey !== undefined) { const oldest = stmtCache.get(oldestKey); if (oldest?.stmt && "finalize" in oldest.stmt) { try { (oldest.stmt as any).finalize(); } catch {} } stmtCache.delete(oldestKey); } } entry = { stmt, sql }; stmtCache.set(sql, entry); } return entry.stmt; } function runSp(fn: (...args: unknown[]) => T, ...args: unknown[]): T { const sp = `sp_${Math.random().toString(36).slice(2)}`; db.exec(`SAVEPOINT "${sp}"`); try { const r = fn(...args); db.exec(`RELEASE "${sp}"`); return r; } catch (e) { try { db.exec(`ROLLBACK TO "${sp}"`); db.exec(`RELEASE "${sp}"`); } catch {} throw e; } } return { driver: "node:sqlite", get open() { return _isOpen; }, get name() { return filePath; }, prepare(sql: string): PreparedStatement { const stmt = getCached(sql); return { run(...params: unknown[]): RunResult { const r = stmt.run(...params); return { changes: Number(r.changes ?? 0), lastInsertRowid: Number(r.lastInsertRowid ?? 0), }; }, get(...params: unknown[]): unknown { return stmt.get(...params); }, all(...params: unknown[]): unknown[] { return stmt.all(...params); }, }; }, exec(sql: string): void { db.exec(sql); }, pragma(pragmaStr: string, options?: { simple?: boolean }): unknown { if (options?.simple) { const row = db.prepare(`PRAGMA ${pragmaStr}`).get() as Record | undefined; if (!row) return null; return Object.values(row)[0] ?? null; } return db.prepare(`PRAGMA ${pragmaStr}`).all(); }, transaction(fn: (...args: unknown[]) => T): (...args: unknown[]) => T { return (...args: unknown[]) => runSp(fn, ...args); }, immediate(fn: () => void): void { runSp(() => fn()); }, async backup(destination: string): Promise { 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 { try { for (const entry of stmtCache.values()) { if (entry.stmt && "finalize" in entry.stmt) { try { (entry.stmt as any).finalize(); } catch {} } } stmtCache.clear(); db.close(); } finally { _isOpen = false; } }, get raw() { return db; }, }; } /** Tenta abrir com better-sqlite3 e node:sqlite sincronamente. Retorna null se ambos falharem. */ export function tryOpenSync( filePath: string, options?: Record ): SqliteAdapter | null { // Try LibSQL (Turso) first if TURSO_DATABASE_URL is set if (process.env.TURSO_DATABASE_URL) { try { const isDirect = process.env.TURSO_DIRECT_CONNECTION === "true"; const syncUrl = isDirect ? undefined : process.env.TURSO_DATABASE_URL; const dbUrl = isDirect ? process.env.TURSO_DATABASE_URL : `file:${filePath}`; const adapter = createLibsqlAdapter(filePath, { url: dbUrl, syncUrl: syncUrl, authToken: process.env.TURSO_AUTH_TOKEN, syncInterval: process.env.TURSO_SYNC_INTERVAL ? Number(process.env.TURSO_SYNC_INTERVAL) : 30000, }); console.log(`[DB] Successfully connected to Turso/libSQL using ${isDirect ? "Direct" : "Embedded Replica"} connection.`); return adapter; } catch (err: any) { console.warn("[DB] Failed to initialize Turso/libSQL adapter. Falling back to local SQLite.", err.message); } } // 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 { // continua para próximo driver } } // 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 { const { DatabaseSync } = _require("node:sqlite") as { DatabaseSync: new (p: string) => Parameters[0]; }; const db = new DatabaseSync(filePath); return buildNodeAdapterSync(db, filePath); } catch { // continua } } } return null; } /** * Pré-inicializa sql.js para um filePath. * Armazena em globalThis para acesso posterior via getSqlJsAdapter(). * Idempotente — seguro chamar múltiplas vezes. */ export async function preInitSqlJs(filePath: string): Promise { const cache = getSqlJsCache(); const existing = cache.get(filePath); if (existing) return existing; const { createSqlJsAdapter } = await import("./sqljsAdapter"); const adapter = await createSqlJsAdapter(filePath); cache.set(filePath, adapter); return adapter; } /** Retorna adapter sql.js pré-inicializado ou null se ainda não inicializado. */ export function getSqlJsAdapter(filePath: string): SqliteAdapter | null { return getSqlJsCache().get(filePath) ?? null; } /** * Factory assíncrona completa: tenta todos os drivers em cascata. * Ordem: better-sqlite3 → node:sqlite → sql.js */ export async function openDatabaseAsync( filePath: string, options?: Record ): Promise { const sync = tryOpenSync(filePath, options); if (sync) { console.log(`[DB] Driver: ${sync.driver} | file: ${filePath}`); return sync; } console.warn("[DB] Drivers síncronos indisponíveis — usando sql.js (WASM)"); const adapter = await preInitSqlJs(filePath); console.log(`[DB] Driver: sql.js | file: ${filePath}`); return adapter; }