# MigrationRunner _(no submodules)_ ## Purpose `MigrationRunner` is the interface that `DatabaseAdapter.migrate` delegates to in concrete adapter implementations. Separating the migration contract from the adapter contract lets adapters swap migration strategies without changing their top-level API — for example, a SQLite adapter might use Drizzle's migration runner while a SurrealDB adapter uses a custom `.surql` file runner, yet both expose the same `DatabaseAdapter.migrate` signature to callers. The three-method design mirrors the principle of least surprise: `pendingMigrations` is a read-only query (safe to call for status checks without making changes), `applyMigration` is the atomic unit of work, and `run` is the high-level orchestrator that wires them together. ## Features - `pendingMigrations` — read-only: lists unapplied migrations without side effects - `applyMigration` — atomic unit: applies a single named migration by SQL string - `run` — orchestrator: discovers, orders, and applies all pending migrations - `MigrationError` — thrown with the exact migration name on failure - `AbortSignal` on both `applyMigration` and `run` via `DbOperationOptions` - Accepts optional `MigrationConfig` to override folder/table defaults ## Usage: Basic Example ```ts import type { MigrationRunner } from "@suaveplan/db"; import { MigrationError } from "@suaveplan/db"; class FileMigrationRunner implements MigrationRunner { async pendingMigrations(config?) { const applied = await this.loadApplied(config); const all = await this.discoverFiles(config); return all .filter(m => !applied.has(m.name)) .map(m => ({ name: m.name, applied: false })); } async applyMigration(name, sql, opts?) { try { await this.db.execute(sql, [], opts); await this.recordApplied(name); } catch (err) { throw new MigrationError(name, err instanceof Error ? err.message : String(err), { cause: err }); } } async run(config?, opts?) { const pending = await this.pendingMigrations(config); const names: string[] = []; for (const { name } of pending) { const sql = await this.loadSql(name, config); await this.applyMigration(name, sql, opts); names.push(name); } return { applied: names.length, migrations: names }; } } ``` ## Usage: Advanced Example ```ts import type { MigrationRunner } from "@suaveplan/db"; import { MigrationError, withDbSpan } from "@suaveplan/db"; // Wrapping run with OTel tracing async function runMigrations(runner: MigrationRunner): Promise { const result = await withDbSpan("db.migrate", () => runner.run({ migrationsFolder: "./db/migrations" }) ); console.log(`Applied ${result.applied} migration(s):`, result.migrations); } // Status check without applying async function checkMigrationStatus(runner: MigrationRunner): Promise { const pending = await runner.pendingMigrations(); if (pending.length > 0) { console.warn(`${pending.length} unapplied migrations:`, pending.map(m => m.name)); } } ``` ## API Reference ### `MigrationRunner` ```ts interface MigrationRunner { pendingMigrations(config?: MigrationConfig): Promise; applyMigration(name: string, sql: string, opts?: DbOperationOptions): Promise; run(config?: MigrationConfig, opts?: DbOperationOptions): Promise; } ``` - **`pendingMigrations`** — returns only unapplied migrations; never mutates state - **`applyMigration`** — applies one migration; throws `MigrationError` on failure with the migration's name in `error.migration` - **`run`** — discovers pending migrations via `pendingMigrations` and applies them in order via `applyMigration`; returns aggregate `MigrationResult` ## Implementation Notes `MigrationRunner` is intentionally an interface rather than an abstract class. This keeps the dependency graph clean — the runner itself has no implementation, so it imposes no runtime cost. Concrete adapters in `@suaveplan/db-sqlite` and future packages supply the implementation. The `AbortSignal` is only threaded through `applyMigration` and `run` — `pendingMigrations` is a read-only operation and is not expected to take long enough to warrant cancellation in typical use cases. Implementors may add signal support to `pendingMigrations` as a non-breaking extension.