Files
skills/docs/api-stack/api/api-host/src/plugin-ledger/migrations.md
2026-05-29 08:20:56 +01:00

3.1 KiB

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

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

// 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.