Files
beenvoice/scripts/verify-drizzle-journal.ts
T

86 lines
3.0 KiB
TypeScript

/**
* 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`,
);