/** * Migration ledger — [E00-S03-T03]. * * The ledger is the PostgreSQL table that records applied migrations, so the * migration runner (built on this boundary — the advisory lock E00-S03-T04 * and the failure diagnostic E00-S03-T05) can tell which migrations have * already been applied and apply the rest exactly once. * * The ledger lives in `database-postgres` — the single workspace package * allowed to import the PostgreSQL driver (E00-S03-T02) — and talks to the * database exclusively through the package-owned `pg` Pool, so no other * package needs the driver to read or write migration state. * * Table: `schema_migrations` * version text — the migration identifier (table primary key) * applied_at timestamptz — when the migration was applied (not null, now()) * * Every operation is idempotent: `ensure()` only creates the table when it is * missing, and `record()` is a conflict-tolerant insert, so a rerun never * errors and never double-applies. The migration version is always passed as * a bound parameter (`$1`) — the SQL never interpolates it, so the only * interpolated value is the compile-time table-name constant. Rollback note * from the issue: `DROP TABLE schema_migrations` resets migration state. */ import type { Pool } from 'pg'; /** Name of the migration ledger table. */ export const MIGRATION_LEDGER_TABLE = 'schema_migrations'; /** * DDL that creates the migration ledger table. Idempotent (create-if-absent): * the first run against an empty database creates the ledger ("migration * ledger is created"); every later run is a no-op. */ export const MIGRATION_LEDGER_DDL = ` CREATE TABLE IF NOT EXISTS ${MIGRATION_LEDGER_TABLE} ( version text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now() ) `; /** * The migration ledger: records which migrations have been applied to the * database, backed by the `schema_migrations` table. Instances are cheap and * share the caller's pool; the ledger performs no locking (advisory lock is * E00-S03-T04) and no diagnostics (E00-S03-T05). */ export class MigrationLedger { private readonly pool: Pool; /** @param pool The package-owned PostgreSQL pool (`pg.Pool`). */ constructor(pool: Pool) { this.pool = pool; } /** * Creates the ledger table if it does not exist. Idempotent: calling it on * an empty database creates the ledger; calling it again is a no-op. */ async ensure(): Promise { await this.pool.query(MIGRATION_LEDGER_DDL); } /** * Records a migration as applied. Idempotent: recording the same version * twice keeps exactly one row, so a rerun never double-applies. The version * is a bound parameter (`$1`). */ async record(version: string): Promise { await this.pool.query( `INSERT INTO ${MIGRATION_LEDGER_TABLE} (version) VALUES ($1) ON CONFLICT (version) DO NOTHING`, [version], ); } /** True when the given migration version is recorded in the ledger. */ async has(version: string): Promise { const result = await this.pool.query( `SELECT 1 FROM ${MIGRATION_LEDGER_TABLE} WHERE version = $1`, [version], ); return (result.rowCount ?? 0) > 0; } /** Versions recorded in the ledger, oldest applied first. */ async applied(): Promise { const result = await this.pool.query( `SELECT version FROM ${MIGRATION_LEDGER_TABLE} ORDER BY applied_at, version`, ); return result.rows.map((row) => row.version as string); } }