first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

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