/** * The database driver interface and the `Db` client built on top of it. * * Adapters (SQLite now; Postgres/MySQL/Mongo later) implement `Driver`. The * client adds ergonomics: `all`/`one` optionally map rows through a model's * `.parse` (so results match your schema), `tx` wraps work in a transaction, and * `createTable` runs a model's DDL. Every query is parameterized. */ import { createTableSql, type Dialect } from "./sql.ts"; import type { Model } from "./schema.ts"; export type Row = Record; export interface ExecResult { changes: number; lastInsertId?: number; } /** The minimal query surface — the driver itself and each transaction expose it. */ export interface TxHandle { /** Run a query returning rows (SELECT). Params are positional. */ query(sql: string, params?: unknown[]): Promise; /** Run a statement (INSERT/UPDATE/DELETE/DDL). */ exec(sql: string, params?: unknown[]): Promise; } export interface Driver extends TxHandle { dialect: Dialect; /** * Run `fn` inside a transaction on a single reserved connection, committing * on success and rolling back on throw. (Pooled drivers must reserve one * connection so BEGIN/…/COMMIT don't span connections.) */ transaction(fn: (tx: TxHandle) => Promise): Promise; close(): void | Promise; } export interface Db { driver: Driver; /** All matching rows, mapped through `model.parse` when a model is given. */ all(sql: string, params?: unknown[], model?: Model): Promise; /** The first matching row (or null), mapped through `model.parse`. */ one(sql: string, params?: unknown[], model?: Model): Promise; exec(sql: string, params?: unknown[]): Promise; /** Run `fn` inside a transaction; rolls back if it throws. */ tx(fn: (db: Db) => Promise): Promise; /** Create a table from its model (`CREATE TABLE IF NOT EXISTS`). */ createTable(model: Model): Promise; close(): void | Promise; } interface DbLifecycle { closing: boolean; active: Set>; closePromise?: Promise; } /** Build a `Db` over a query runner (the driver at top level, or a transaction). */ function dbOver( runner: TxHandle, driver: Driver, lifecycle: DbLifecycle, transactionScope = false, ): Db { function run(operation: () => Promise): Promise { if (lifecycle.closing && !transactionScope) { return Promise.reject(new Error("WRN-DB-CLOSED: database is closing or closed")); } const promise = Promise.resolve().then(operation); lifecycle.active.add(promise); void promise.finally(() => lifecycle.active.delete(promise)).catch(() => {}); return promise; } const db: Db = { driver, async all(sql, params = [], model) { const rows = await run(() => runner.query(sql, params)); return (model ? rows.map((r) => model.parse(r)) : rows) as never; }, async one(sql, params = [], model) { const rows = await db.all(sql, params, model as never); return (rows[0] ?? null) as never; }, exec(sql, params = []) { return run(() => runner.exec(sql, params)); }, async tx(fn) { // Top level opens a real transaction; inside one, reuse the current tx. if (runner === driver) return run(() => driver.transaction((tx) => fn(dbOver(tx, driver, lifecycle, true)))); return fn(db); }, async createTable(model) { await run(() => runner.exec(createTableSql(model, driver.dialect))); }, close() { if (lifecycle.closePromise) return lifecycle.closePromise; lifecycle.closing = true; lifecycle.closePromise = (async () => { await Promise.allSettled([...lifecycle.active]); await driver.close(); })(); return lifecycle.closePromise; }, }; return db; } /** Build a `Db` client from a driver. */ export function createDb(driver: Driver): Db { return dbOver(driver, driver, { closing: false, active: new Set() }); }