/** * 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; } /** Build a `Db` over a query runner (the driver at top level, or a transaction). */ function dbOver(runner: TxHandle, driver: Driver): Db { const db: Db = { driver, async all(sql, params = [], model) { const rows = await 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 runner.exec(sql, params); }, async tx(fn) { // Top level opens a real transaction; inside one, reuse the current tx. if (runner === driver) return driver.transaction((tx) => fn(dbOver(tx, driver))); return fn(db); }, async createTable(model) { await runner.exec(createTableSql(model, driver.dialect)); }, close() { return driver.close(); }, }; return db; } /** Build a `Db` client from a driver. */ export function createDb(driver: Driver): Db { return dbOver(driver, driver); }