/** * Drizzle journal monotonicity guard. * * Background — twice already we hit this regression: a `pnpm migrate` * silently skipped a freshly-generated migration because its `when` * timestamp was older than the previous migration's `when`. Drizzle's * migrator orders the entries by `when` (not by `idx`) and only * applies entries whose `when` is strictly greater than the latest * row's `created_at` in `pgboss... drizzle.__drizzle_migrations`. * * Symptom: migrate prints "Migrations applied." while the schema in * the live DB is missing whatever 0012 / 0013 were supposed to add. * Web 500's on every authenticated request because the code expects * the new columns. * * This module is the first line of defence: * - `assertJournalMonotonic(entries)` is a pure check the test * suite runs against the committed journal file. CI fails on a * bad commit before it can ship. * - migrate.ts calls it on boot. If the live journal in source * control has slipped out of monotonic order, migrate refuses * to run and prints the offending entries with the smallest * bump that would unbreak each one. */ export interface JournalEntry { idx: number; tag: string; when: number; } export interface JournalCheckResult { ok: boolean; /** Entries whose `when` is <= the previous entry's `when`. */ violations: Array<{ idx: number; tag: string; when: number; /** The previous entry's when — the new bound that this one must beat. */ previousWhen: number; previousTag: string; /** A `when` value that would make THIS entry monotonic again. */ suggestedWhen: number; }>; } /** * Walk the journal entries in idx order and report any whose `when` * is not strictly greater than the previous entry's `when`. The * journal can have any starting timestamp; we only care about the * relative ordering matching idx. Equal timestamps are also a * violation — drizzle requires strictly greater. */ export function assertJournalMonotonic(entries: JournalEntry[]): JournalCheckResult { const sorted = [...entries].sort((a, b) => a.idx - b.idx); const violations: JournalCheckResult["violations"] = []; for (let i = 1; i < sorted.length; i++) { const prev = sorted[i - 1]!; const cur = sorted[i]!; if (cur.when <= prev.when) { violations.push({ idx: cur.idx, tag: cur.tag, when: cur.when, previousWhen: prev.when, previousTag: prev.tag, suggestedWhen: prev.when + 1000, }); } } return { ok: violations.length === 0, violations }; } /** Format the check result into a multi-line human message. */ export function formatJournalViolations(result: JournalCheckResult): string { if (result.ok) return ""; const lines: string[] = [ "Drizzle journal is not monotonic — migrate would silently skip these entries:", ]; for (const v of result.violations) { lines.push( ` ${v.tag} (idx ${v.idx}) when=${v.when} <= ${v.previousTag}.when=${v.previousWhen}`, ); lines.push( ` fix: set ${v.tag}.when to >= ${v.suggestedWhen} in ` + `packages/db/migrations/meta/_journal.json`, ); } return lines.join("\n"); }