diff --git a/server/db/sqlite.js b/server/db/sqlite.js new file mode 100644 index 0000000..10d5aff --- /dev/null +++ b/server/db/sqlite.js @@ -0,0 +1,50 @@ +// SQLite backend for the async DB adapter (dev + tests; also the current prod engine until pg cutover). +// +// Wraps the synchronous node:sqlite instance (schema applied at load in ../db.js) in the async interface +// the repos call. Results are returned via resolved Promises, so the SAME repo code runs unchanged on this +// synchronous engine and on asynchronous Postgres — the app never sees the difference. +const raw = require('../db'); // DatabaseSync instance with the full schema already applied + +// node:sqlite re-prepares cheaply, but caching by SQL text avoids re-parsing on hot paths. +const cache = new Map(); +function stmt(sql) { + let s = cache.get(sql); + if (!s) { s = raw.prepare(sql); cache.set(sql, s); } + return s; +} +const num = (v) => (typeof v === 'bigint' ? Number(v) : v); +const runResult = (r) => ({ changes: num(r.changes), lastInsertRowid: num(r.lastInsertRowid) }); + +function prepare(sql) { + return { + get: (...p) => Promise.resolve(stmt(sql).get(...p)), + all: (...p) => Promise.resolve(stmt(sql).all(...p)), + run: (...p) => Promise.resolve(runResult(stmt(sql).run(...p))), + }; +} + +function exec(sql) { raw.exec(sql); return Promise.resolve(); } + +// Transaction primitive. SQLite is single-connection, so BEGIN/COMMIT/ROLLBACK on `raw` is safe; the +// callback gets a runner with the same async run/get shape. (The pg backend implements this on ONE pooled +// client — the reason repos must use tx() rather than bare exec('BEGIN') for multi-statement atomicity.) +async function tx(fn) { + raw.exec('BEGIN'); + try { + const t = { + run: (sql, ...p) => Promise.resolve(runResult(stmt(sql).run(...p))), + get: (sql, ...p) => Promise.resolve(stmt(sql).get(...p)), + all: (sql, ...p) => Promise.resolve(stmt(sql).all(...p)), + }; + const out = await fn(t); + raw.exec('COMMIT'); + return out; + } catch (e) { + try { raw.exec('ROLLBACK'); } catch (_) {} + throw e; + } +} + +function init() { return Promise.resolve(); } // schema already applied synchronously in ../db.js + +module.exports = { prepare, exec, tx, init, name: 'sqlite', _raw: raw }; diff --git a/server/dbx.js b/server/dbx.js new file mode 100644 index 0000000..3e67c2f --- /dev/null +++ b/server/dbx.js @@ -0,0 +1,6 @@ +// Async DB adapter facade. The backend is chosen by DB_BACKEND (default 'sqlite'); 'pg' is added at +// cutover. Every backend implements the same async interface — prepare(sql).{get,all,run}, exec(sql), +// tx(fn), init() — so repos and app code are engine-agnostic. Swapping engines is one backend file, no +// repo changes. (This is the same "never hardwire the engine" principle we'll apply to the pub/sub layer.) +const name = process.env.DB_BACKEND || 'sqlite'; +module.exports = require('./db/' + name);