/** * Database models — the single source of truth for a table's shape. * * A model defined with `table()` + the `v` column builder drives (1) DDL for * migrations, (2) coercion/validation of DB rows into typed objects * (`model.parse`), and later (3) the types the sqlc-style query generator emits. * Column types are dialect-neutral; each adapter maps them to its own SQL types. */ export type BaseType = "id" | "text" | "int" | "real" | "bool" | "timestamp" | "json"; export interface ColumnDef { type: BaseType; /** NOT NULL unless `.optional()` was called. Ids are implicitly not-null. */ notNull: boolean; primaryKey: boolean; autoIncrement: boolean; unique: boolean; /** Literal default, or the sentinel "now" for a current-timestamp default. */ default?: unknown; references?: { table: string; column: string }; } /** A fluent column definition. Chain modifiers, then hand it to `table()`. */ export class Column { readonly def: ColumnDef; constructor(type: BaseType, overrides: Partial = {}) { this.def = { type, notNull: true, primaryKey: false, autoIncrement: false, unique: false, ...overrides, }; } optional(): this { this.def.notNull = false; return this; } unique(): this { this.def.unique = true; return this; } default(value: unknown): this { this.def.default = value; return this; } primaryKey(): this { this.def.primaryKey = true; return this; } references(table: string, column = "id"): this { this.def.references = { table, column }; return this; } /** Coerce a raw DB value into its JS type (used by `model.parse`). */ coerce(raw: unknown): unknown { if (raw === null || raw === undefined) return this.def.notNull ? raw : null; switch (this.def.type) { case "id": case "int": return typeof raw === "bigint" ? Number(raw) : Number(raw); case "real": return Number(raw); case "bool": return raw === true || raw === 1 || raw === "1" || raw === "true"; case "timestamp": return raw instanceof Date ? raw : new Date(raw as string | number); case "json": return typeof raw === "string" ? safeJson(raw) : raw; default: return String(raw); } } } function safeJson(value: string): unknown { try { return JSON.parse(value); } catch { return value; } } /** Column builders. `v.id()` is an auto-increment primary key. */ export const v = { id: () => new Column("id", { primaryKey: true, autoIncrement: true }), text: () => new Column("text"), string: () => new Column("text"), int: () => new Column("int"), number: () => new Column("real"), real: () => new Column("real"), bool: () => new Column("bool"), boolean: () => new Column("bool"), timestamp: () => new Column("timestamp"), json: () => new Column("json"), }; export type Columns = Record; export interface Model> { name: string; columns: Columns; /** Coerce a raw DB row into a typed object (unknown columns pass through). */ parse(row: Record): T; /** Column definitions, for migrations and the query generator. */ describe(): Record; } /** Define a table model from a name and a map of columns. */ export function table>(name: string, columns: Columns): Model { return { name, columns, parse(row) { const out: Record = { ...row }; for (const [key, column] of Object.entries(columns)) { if (key in row) out[key] = column.coerce(row[key]); } return out as T; }, describe() { const out: Record = {}; for (const [key, column] of Object.entries(columns)) out[key] = column.def; return out; }, }; }