/** * The bracketed migration runner (journal spec §2.3a rules 1–3; T12774, * T12781, T12785, T12786, T12795, T12796, T12797, T12809). * * Drizzle's `migrateSync` runs every pending migration in ONE transaction of * its own, with `PRAGMA foreign_keys` left ON (the PRAGMA is a no-op inside a * transaction). A table rebuild's `DROP TABLE` therefore cascade-deletes * child rows, and nothing can run inside its transaction. This runner keeps * drizzle's journal byte-for-byte and replaces the transaction discipline: * * - **Journal-identical (R5-3).** Pending migrations are selected by NAME * (drizzle's `getMigrationsToRun`), each row is written with the RAW file's * sha256, `created_at = folderMillis`, `name` and `applied_at`, exactly as * `migrateSync` writes it. `reconcileJournal` and drizzle's own * `upgradeSyncIfNeeded` run inside the leading bracket, in today's order. * Statements run one per `prepare()`, as drizzle's session does. * - **One bracket per migration FILE (R6-6).** Each file gets its own * `BEGIN IMMEDIATE … COMMIT` and its own journal row. * - **FK discipline (A, NEW-1, R5-2, R5-4, NEW-6).** `foreign_keys` is turned * OFF before `BEGIN` only for a file that rebuilds a table. For such a file * the runner snapshots the FK violations of its scope (rebuilt and * DML-touched tables plus their FK children) as a rowid-free multiset, * re-checks after the statements, and rolls back on any NEW violation. * Pre-existing orphans never block a migration. `finally` restores * `foreign_keys`, reads it back and asserts no transaction is open. * - **Recovery kept.** A duplicate-column or table-exists error re-runs * `reconcileJournal` once and retries, as `migrateWithRetry` does; a BUSY * error retries the whole bracket with backoff, never mid-bracket. * - **Hooks (D4, NEW-8).** `beforeMigrations` runs before the first bracket * (the sealer's pending work and the repair diff for suspect tables, S3); * `suspendCapture` / `reinstallCapture` run inside each bracket around the * statements (the capture triggers, S2); `afterMigration` runs after each * commit (the `chash` re-baseline, S3). All are optional. * * Drizzle is pinned to {@link PINNED_DRIZZLE_VERSION}; a test fails when the * installed version differs, so an upgrade must re-run the parity suite. * * @task T12796 * @task T12809 * @module store/migration-runner */ import type { DatabaseSync } from 'node:sqlite'; import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite'; /** The drizzle-orm version the runner's journal parity was proven against. */ export declare const PINNED_DRIZZLE_VERSION = "1.0.0-rc.4"; /** The journal table drizzle uses by default. */ export declare const MIGRATIONS_TABLE = "__drizzle_migrations"; /** Optional work around the brackets (D4 ordering). */ export interface MigrationBracketHooks { /** Before the first bracket: seal pending captures, repair suspect tables (S3). */ beforeMigrations?(db: DatabaseSync): void; /** Inside each bracket, before the statements: drop the capture triggers (S2). */ suspendCapture?(db: DatabaseSync): void; /** Inside each bracket, after the statements: reinstall the capture triggers (S2). */ reinstallCapture?(db: DatabaseSync): void; /** After each committed bracket: re-baseline `chash` for the file's tables (S3). */ afterMigration?(db: DatabaseSync, migration: { name: string; hash: string; }): void; } /** One migration lineage sharing the store's journal. */ export interface MigrationLineage { readonly folder: string; /** Run `reconcileJournal` for this lineage first (as the callers do today). */ readonly reconcile?: { readonly existenceTable: string; readonly logSubsystem: string; readonly siblings?: readonly string[]; }; } /** * Legacy lineages the runner refuses by folder name (cleo-dev ruling on the * S2 runner scope, 2026-09-30). They keep `migrateWithRetry` in their own * binders (`sqlite.ts`, `memory-sqlite.ts`, `nexus-sqlite.ts`, * `conduit-sqlite.ts`, …); only the consolidated `cleo.db` lineages run * bracketed. */ export declare const LEGACY_LINEAGES: ReadonlySet; /** A legacy lineage was handed to the runner. */ export declare class LegacyLineageRefusedError extends Error { readonly lineage: string; readonly code = "E_MIGRATION_RUNNER_LEGACY_LINEAGE"; constructor(lineage: string); } /** What {@link runBracketedMigrations} did. */ export interface MigrationRunReport { /** Names of the migrations applied, in order. */ readonly applied: string[]; /** Names of the files that ran with `foreign_keys = OFF` (rebuilds). */ readonly rebuilds: string[]; } /** A migration file that introduced a foreign-key violation (rolled back). */ export declare class MigrationForeignKeyError extends Error { readonly migration: string; readonly violations: readonly string[]; readonly code = "E_MIGRATION_FK_VIOLATION"; constructor(migration: string, violations: readonly string[]); } /** `foreign_keys` could not be restored on a handle after a bracket. */ export declare class ForeignKeysNotRestoredError extends Error { readonly code = "E_STORE_FK_OFF"; constructor(expected: number, actual: number); } /** * The tables a migration file rebuilds or drops: `CREATE TABLE __new_x` * (→ `x`), `DROP TABLE x`, `ALTER TABLE x RENAME TO y` (→ `x`, `y`). * Empty for a file that rebuilds nothing. */ export declare function rebuiltTables(statements: readonly string[]): string[]; /** * Where each table a migration file renames ends up: every `ALTER TABLE a RENAME TO b`, in statement * order and composed, so `a → b` then `b → c` gives `a → c` (T13027). Unrenamed tables are absent. */ export declare function renamedTables(statements: readonly string[]): Map; /** * Drop every view and trigger that can never compile because a table or column * it reads no longer exists, so a migration's `RENAME` / `DROP COLUMN` is not * refused for a schema object nothing can use (T13631: a store whose stamped * T9686-B2 never dropped `releases_view` kept it over the dropped * `release_manifests`, and 2026.10.10's first table rebuild failed every open). * * The check is SQLite's own: a throwaway rename inside a savepoint reports the * first object that fails validation. Only a missing-dependency error drops an * object; any other error leaves the schema as is for the migration to report. * Call inside the migration's transaction, so a failed migration restores the * dropped objects too. * * @param db - The store handle, inside an open transaction. * @returns The dropped objects, as `view ` / `trigger `. */ export declare function dropOrphanSchemaObjects(db: DatabaseSync): string[]; /** The tables a migration's DML writes (INSERT / UPDATE / DELETE / REPLACE). */ export declare function dmlTables(statements: readonly string[]): string[]; /** * The FK scope of a rebuild file: the given tables plus every table with a * foreign key to one of them (children), restricted to tables that exist. */ export declare function foreignKeyScope(db: DatabaseSync, tables: readonly string[]): string[]; /** * The foreign-key violations of `tables`, as a multiset keyed by rowid-free * identity (R5-2): (table, the child's primary-key values normalized by * column affinity — or the whole row for a table without a declared PK, * parent table, the child's FK values). Duplicates count. * * FK column NAMES are not part of the key: a rebuild may rename an FK column * while the violation stays the same row and value (T13027). Two orphaned * FKs of one row with equal values are still counted twice. */ export declare function foreignKeyViolations(db: DatabaseSync, tables: readonly string[]): Map; /** * A before-snapshot re-keyed to the names its tables have after a file's * renames (T13027): child and parent tables move through `renames` when the * new name exists afterwards. A table renamed away and dropped (`x → __old_x` * in a rebuild) keeps its name, since its rows now live under the old name. */ export declare function renameViolations(before: Map, renames: ReadonlyMap, exists: (table: string) => boolean): Map; /** Keys whose count in `after` exceeds their count in `before`. */ export declare function newViolations(before: Map, after: Map): string[]; /** * Apply every pending migration of each lineage, in order, with the journal * `migrateSync` would leave, one bracket per file. * * @param nativeDb - The store handle. * @param db - The drizzle wrapper of `nativeDb` (for drizzle's journal upgrade). * @param lineages - The lineages sharing the journal, in apply order. * @param hooks - Optional D4 hooks. */ export declare function runBracketedMigrations(nativeDb: DatabaseSync, db: NodeSQLiteDatabase, lineages: readonly MigrationLineage[], hooks?: MigrationBracketHooks): MigrationRunReport; /** * Same as {@link runBracketedMigrations} for one lineage, in the shape the * callers of `reconcileJournal` + `migrateWithRetry` use today. */ export declare function migrateBracketed(db: NodeSQLiteDatabase, nativeDb: DatabaseSync, migrationsFolder: string, existenceTable: string, logSubsystem: string, siblings?: readonly string[], hooks?: MigrationBracketHooks): MigrationRunReport; //# sourceMappingURL=migration-runner.d.ts.map