first commit
This commit is contained in:
83
docs/api-stack/api/api-host/src/plugin-ledger/migrations.md
Normal file
83
docs/api-stack/api/api-host/src/plugin-ledger/migrations.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user