/** * Ensures every drizzle/*.sql migration file has a matching entry in meta/_journal.json. * Run: bun scripts/verify-drizzle-journal.ts */ import { readdirSync, readFileSync } from "fs"; import path from "path"; const drizzleDir = path.resolve(import.meta.dir, "../drizzle"); const journalPath = path.join(drizzleDir, "meta/_journal.json"); const journal = JSON.parse(readFileSync(journalPath, "utf8")) as { entries: Array<{ idx: number; tag: string; when: number }>; }; const sqlTags = readdirSync(drizzleDir) .filter((name) => /^\d+_.+\.sql$/.test(name)) .map((name) => name.replace(/\.sql$/, "")) .sort(); const journalTags = journal.entries.map((entry) => entry.tag).sort(); const missingFromJournal = sqlTags.filter((tag) => !journalTags.includes(tag)); const missingSql = journalTags.filter((tag) => !sqlTags.includes(tag)); const idxSequence = journal.entries.map((entry) => entry.idx); const expectedIdx = journal.entries.map((_, i) => i); const badIdx = idxSequence.some((idx, i) => idx !== expectedIdx[i]); // drizzle's migrator gates on the single highest `when` already recorded in // the target DB — it doesn't check hashes per-migration. If `when` values // ever go non-increasing (e.g. a migration got deleted/renumbered after // being applied to some environment), later entries can silently be skipped // forever even though drizzle reports success. Keep this strictly increasing. // // 0008 and 0011 are known pre-existing exceptions from before this check // existed (see git log on those files) — both already guard every statement // with IF NOT EXISTS specifically because of this, so a skip is harmless. // Don't add new exceptions here; fix the timestamp instead. const KNOWN_NON_MONOTONIC_TAGS = new Set([ "0008_payments_recurring_public_links", "0011_time_entry_invoice_id", ]); const nonMonotonic = journal.entries.some( (entry, i) => i > 0 && entry.when <= journal.entries[i - 1]!.when && !KNOWN_NON_MONOTONIC_TAGS.has(entry.tag), ); let failed = false; if (missingFromJournal.length > 0) { console.error("[verify-drizzle-journal] SQL files missing from journal:"); for (const tag of missingFromJournal) console.error(` - ${tag}`); failed = true; } if (missingSql.length > 0) { console.error("[verify-drizzle-journal] Journal entries without SQL files:"); for (const tag of missingSql) console.error(` - ${tag}`); failed = true; } if (badIdx) { console.error("[verify-drizzle-journal] Journal idx values are not sequential from 0"); failed = true; } if (nonMonotonic) { console.error( "[verify-drizzle-journal] Journal `when` timestamps are not strictly increasing. " + "drizzle-orm's migrator only compares against the single highest `when` already " + "applied in the target DB, so a lower or equal value here can cause migrations to " + "be silently skipped on databases that already ran a later timestamp.", ); failed = true; } if (failed) { process.exit(1); } console.log( `[verify-drizzle-journal] OK — ${sqlTags.length} migrations match journal entries`, );