# `migrations.ts` ## Purpose `HostMigrationRunner` is the forward-only idempotent migration engine the API host runs against its plugin ledger DB. It owns the closed set of host-side schema migrations — every table the host uses to track plugins, journal rollback entries, and reserve future saga + 2PC storage. The runner records every applied migration in a sentinel `__host_migrations` table so re-instantiating it against an already-migrated DB is a true no-op. The runner is "forward only" by design — there is no downgrade path. Out-of-order or backwards-only declarations raise `LEDGER.MIGRATION_OUT_OF_ORDER` so config drift between deployments fails loudly before any rows are touched. Migration callback failures raise `LEDGER.MIGRATION_FAILED` and the runner deliberately does NOT record the failed id, so the next attempt re-runs the migration. ## Features - Forward-only ledger with a `__host_migrations` sentinel table. - Two migrations shipped: `core-tables` (the active plugin + journal schema) and `saga-2pc-reserved` (Stage-6 reserved schemas). - Idempotent re-apply: the runner only executes a migration whose id has no row in `__host_migrations`. - Out-of-order detection: applied ids that aren't declared by the current runner, or pending ids ≤ the max applied id, are both rejected. - Injectable clock for deterministic `applied_at` timestamps. ## Usage: Basic ```ts import { Database } from "bun:sqlite"; import { HostMigrationRunner } from "@suaveplan/api-host/plugin-ledger"; const db = new Database(":memory:"); const runner = new HostMigrationRunner(db); const applied = runner.migrate(); // [1, 2] runner.migrate(); // [] ``` ## Usage: Advanced ```ts // Inject a fake clock + a slimmer migration list for tests. import { HOST_LEDGER_MIGRATIONS } from "@suaveplan/api-host/plugin-ledger"; const onlyCore = HOST_LEDGER_MIGRATIONS.filter((m) => m.id === 1); const runner = new HostMigrationRunner( db, onlyCore, () => new Date("2026-05-25T00:00:00Z") ); runner.migrate(); const applied = runner.listApplied(); // [1] ``` ## API Reference - `new HostMigrationRunner(db, migrations?, now?)` — defaults to `HOST_LEDGER_MIGRATIONS` and `() => new Date()`. - `migrate(): readonly number[]` — returns the ids run by this call. - `listApplied(): readonly number[]` — every previously-applied id ascending. - `HOST_LEDGER_MIGRATIONS` — closed forward-only migration set. - `LedgerMigration { id, name, up(db) }` — declaration shape. ## Implementation Notes Each migration's `up` callback runs DDL through `db.run(sql)` (the single-statement DDL path on a SQLite-shaped handle). Multi-statement migrations declare each statement separately rather than relying on the driver to split — this keeps the runner driver-agnostic. The sentinel insert (`INSERT INTO __host_migrations`) runs AFTER the `up` callback returns. If the callback throws, the runner does not record the id, so the next `migrate()` retries the migration. A single migration is not transactional with respect to its own DDL — SQLite's `CREATE TABLE IF NOT EXISTS` is idempotent within a single migration, so a partial first-pass apply still succeeds on retry.