type NitroPluginDef = (nitroApp: any) => void | Promise; /** * True when an error from `ALTER TABLE ... ADD COLUMN` indicates the * column already existed. Recognizes both SQLite ("duplicate column * name") and Postgres ("column ... already exists" — exact text varies * by error code 42701, but the substring is stable). Exported so other * idempotent column-upgrade loops in the codebase don't reinvent this * regex with subtly different shapes. */ export declare function isDuplicateColumnError(err: unknown): boolean; /** * True when a migration statement failed because the connected DB ROLE lacks * privilege — e.g. a permission-limited dev/replica role that doesn't own the * table. Postgres raises SQLSTATE 42501 ("insufficient_privilege", routine * aclcheck_error, message "must be owner of table …"). We treat these as * NON-FATAL so a perms-limited database can't crash-loop the whole server: the * migration is skipped (left unrecorded) and a properly-privileged role applies * it later. Production, where the role owns its tables, never hits this path. */ export declare function isPermissionError(err: unknown): boolean; export interface RunMigrationsOptions { /** * Name of the migrations bookkeeping table. REQUIRED — there is intentionally * no default. Two templates that share a database (e.g. via the same Neon URL) * each have their own version space starting at v1, and a single shared * `_migrations` table will silently skip the second template's migrations if * the first has already advanced past those version numbers. This caused the * design template's migrations to be skipped entirely on a Neon DB that * slides had already populated up to v15 (PR #320 era). * * Use one bookkeeping table per template, e.g. `slides_migrations`. Core * feature plugins (e.g. the org module) follow the same convention with * their own prefix, e.g. `_org_migrations`. */ table: string; } /** * A single migration entry. * * `sql` can be a string (runs on every dialect) or an object with dialect * keys for dialect-gated SQL. Useful when Postgres needs an ALTER that * SQLite can't parse. * * { version: 14, sql: { postgres: "ALTER TABLE …" } } // no-op on sqlite * { version: 15, sql: { sqlite: "…", postgres: "…" } } // both dialects * * `name` is an optional stable, unique slug that opts a migration into * **name-based tracking** instead of the legacy version-number gate. See the * "Name-based tracking" section on `runMigrations` for why this exists and * exactly how the two gating strategies interact. */ export type MigrationSql = string | { postgres?: string; sqlite?: string; }; export interface MigrationEntry { version: number; sql: MigrationSql; /** * Stable, unique slug for this migration (e.g. `"analytics-alert-rules-table"`). * When present, this migration is tracked by NAME instead of by version * number — see the `runMigrations` doc comment for the full rationale and * gating rules. Must be unique across the migration list; a duplicate name * throws at startup (programmer error, not a runtime data problem). */ name?: string; } /** * Runs a list of migrations against the configured database, gated by a * per-table bookkeeping row (`options.table`) so repeated boots only apply * new migrations. * * ## Name-based tracking (why it exists) * * The legacy gate is purely numeric: `SELECT MAX(version)` from the * bookkeeping table, then apply every entry whose `version` is greater. That * scheme silently breaks down when **two independent branches each extend the * same migration list with different DDL under the same version numbers** — * exactly what happened to the analytics template's v75-v83 range. Branch A * shipped alert-rule tables as v75-v78; branch B shipped unrelated DDL as its * own v75-v83. Whichever branch deployed first recorded "v75..v83 applied" * in the bookkeeping table. When the other branch merged and deployed, its * migrations at those same version numbers were never applied — `MAX(version)` * was already ≥ their version, so the gate treated them as done even though * their actual DDL had never run. The result: `analytics_migrations` showed * rows for 1..83 with no gaps, yet `analytics_alert_rules`, * `analytics_alert_incidents`, and `session_recordings.network_error_count` * did not exist in production. Version numbers are not a stable identity — * they're just sequence position, and sequence position collides across * branches. * * Name-based tracking fixes this by keying application on a stable, unique * **string slug** instead of a position in a shared integer sequence: * * - Entries with a `name` are recorded in a companion table, * `${table}_named` (`name TEXT PRIMARY KEY`), and APPLY IFF their name is * absent from that table — completely independent of version numbers or * the legacy `MAX(version)` gate. Two branches can both ship a migration * named `"analytics-alert-rules-table"` at version 75, or one at version 75 * and the other at version 90 after a rebase — either way it applies * exactly once per database, keyed on the name. * - Entries WITHOUT a `name` keep the exact legacy behavior * (`version > MAX(recorded version)`), so nothing changes for existing * unnamed migrations. * - Named migrations still execute in list order, interleaved with unnamed * ones exactly as written. * - When a named migration's DDL runs, we record BOTH the named row (always) * and — if its `version` is greater than the current legacy max — the * legacy version row too, in the SAME atomic batch/transaction as the DDL. * This keeps the legacy `MAX(version)` gate monotonically advancing for * any unnamed migrations that come after it in the list, while ensuring a * named migration is never "double recorded" in a way that would let it * re-apply. * - A duplicate `name` across the migration list throws at startup — that's * a programmer error (copy-paste or merge mistake), not a runtime data * problem, and failing loud beats silently tracking the wrong row. * * New migrations should always set a `name`. Legacy unnamed migrations don't * need to be renamed retroactively — the two gating strategies coexist in the * same list — but if a table is EVER at risk of being shared across parallel * branches (any template's own migrations qualify, since branches routinely * extend the same list concurrently), giving new entries a name is what makes * them immune to the collision class described above. */ export declare function runMigrations(migrations: Array, options: RunMigrationsOptions): NitroPluginDef; export {}; //# sourceMappingURL=migrations.d.ts.map