# `migrations/run-migrations` — per-engine migration runner _(no submodules)_ ## Purpose `runMigrations` applies a plugin's pending migrations from a directory of `*.sql` files, recording applied versions in the engine-side `__plugin_migrations(plugin, version, applied_at)` table. It is the single entry point used by `installing` step of the plugin lifecycle FSM; the plugin host calls it once per `(plugin, engine)` pair during boot. The runner is engine-neutral: it issues only standard SQL (`CREATE TABLE IF NOT EXISTS`, `SELECT`, `INSERT`, plus the migration body). When the underlying adapter exposes `transaction`, each migration is applied inside an atomic transaction; on failure the transaction rolls back and the row in `__plugin_migrations` is not written. ## Features - Lexicographic file ordering (`001_init.sql` before `002_*.sql`). - Idempotent re-run — applied versions are tracked in `__plugin_migrations`; a second call with the same file set applies zero migrations. - Per-migration `transaction { body + INSERT }` for engines that support transactions; flat `execute + execute` fall-back for those that don't (e.g. some Surreal modes). - OTel instruments: `api.db.migrations_applied_total{plugin}` counter + `api.db.migration_duration_ms{plugin}` histogram. - Injectable `fs: MigrationsFsReader` — tests use an in-memory map, production wires `node:fs/promises`. - On any failure: `DB_ADAPTER.MIGRATION_FAILED` with cause + details including the failing file name. ## Usage: Basic ```ts import { runMigrations } from "@suaveplan/api-db-adapter/migrations"; import { promises as fs } from "node:fs"; const result = await runMigrations({ plugin: "audit", capabilities: [], admin, migrationsDir: "./db/migrations", fs: { readdir: (dir) => fs.readdir(dir), readFile: (path) => fs.readFile(path, "utf8"), }, }); // result.applied = ["001_init.sql", "002_add_invoices.sql"] // result.skipped = [] // first run ``` ## Usage: Advanced ```ts const memFs = { readdir: async () => ["001.sql", "002.sql"], readFile: async (path: string) => path.endsWith("001.sql") ? "CREATE TABLE users(id TEXT)" : "ALTER TABLE users ADD COLUMN email TEXT", }; const first = await runMigrations({ plugin: "user", capabilities: [], admin, migrationsDir: "/dev/null", fs: memFs, }); // first.applied.length === 2 const second = await runMigrations({ plugin: "user", capabilities: [], admin, migrationsDir: "/dev/null", fs: memFs, }); // second.applied.length === 0, second.skipped.length === 2 ``` ## API Reference - `runMigrations(options): Promise`. - `RunMigrationsOptions` — `{ plugin, capabilities, admin, migrationsDir, fs }`. - `RunMigrationsResult` — `{ applied: string[]; skipped: string[] }`. - `MigrationsFsReader` — `{ readdir(dir); readFile(path) }`. - `getMigrationInstruments() / resetMigrationInstruments()`. ## Implementation Notes The runner intentionally does not parse SQL inside the migration body — that is the engine's job. Failed migrations rely on `transaction` for atomic rollback; on engines without transactions, partial state is possible if the body succeeds but the `INSERT` into `__plugin_migrations` fails. That is out of scope for the runner and the responsibility of operator runbooks (`Surrealdb` mostly). The `applied_at` column is an ISO-8601 timestamp, matching the `MigrationCursor.appliedAt` shape on `@suaveplan/api-types/db`.