/** * Boot-time convergence for columns Drizzle's schema declares but a * pre-existing table is missing. * * Lives in its own module (like `./widen-columns.js`) so stores/plugins can * import it without every `vi.mock("../db/client.js")` test needing to stub * it: the helper works directly against Postgres information_schema. * * ## Why * * `CREATE TABLE IF NOT EXISTS` only runs the CREATE the first time a table is * created — once it exists, adding a column to the Drizzle schema (e.g. * `schema.ts`) does nothing to already-existing rows unless a hand-written * `ALTER TABLE ... ADD COLUMN` migration ships alongside it. Fresh dev * databases mask this because the CREATE always includes every declared * column; only long-lived, pre-existing production tables are missing the * new column. Forgetting the migration turns every query that references the * column into a Postgres `42703 (undefined_column)` — e.g. * `session_recordings.network_error_count`, which 500'd every * `list-session-recordings` call. * * `ensureAdditiveColumns()` is belt-and-braces for that failure mode: after * the authoritative hand-written migrations run, diff each Drizzle table's * declared columns against the live table and additively patch any gap. It * is NOT a replacement for migrations — it never touches indexes, data * transforms, or existing columns, and a hand-written migration should still * ship for every schema change. This is a safety net for the case where one * doesn't. * * ## Safety rules (hard) * * - Additive only: never drops, renames, retypes, or otherwise touches a * column that already exists on the live table. * - A `NOT NULL` column is only ever added when it has a renderable default * (so existing rows get a valid value in the same statement). If it has no * renderable default, it is added as nullable when the Drizzle declaration * allows null, else the column is SKIPPED entirely with a loud log line * naming the column and the reason. * - Only simple literal defaults are rendered: numbers, strings, booleans, * and `sql` template defaults that stringify to one of a small allow-listed * set of safe constants (see `renderDefaultLiteral`). Any other `sql` * default is NOT rendered — the column is added without a `DEFAULT` (and * without `NOT NULL`, per the rule above) rather than risk interpolating * unsafe SQL. * - All identifiers (table/column names) are validated against a strict * `[A-Za-z_][A-Za-z0-9_]*` pattern and double-quoted; no value from a row or * from user data is ever interpolated into the generated SQL. * - If the table itself does not exist yet, this is a no-op — table creation * owns bringing every declared column into existence for a brand-new table. * - Idempotent and best-effort: a failure on one column is logged and does * not abort the rest, and a total failure (e.g. `information_schema` * unreadable) must never crash boot — the caller decides whether/how to * surface `errors` in the returned summary. */ import type { DbExec } from "./client.js"; export interface EnsureAdditiveColumnsLogger { info: (message: string) => void; warn: (message: string) => void; error: (message: string) => void; } export interface EnsureAdditiveColumnsOptions { db: DbExec; tables: unknown[]; logger?: EnsureAdditiveColumnsLogger; } export interface EnsureAdditiveColumnsResult { mode: "checked" | "skipped-serverless"; applied: string[]; skipped: Array<{ column: string; reason: string; }>; errors: Array<{ column: string; error: string; }>; } export declare function ensureAdditiveColumns(options: EnsureAdditiveColumnsOptions): Promise;