Forked from gburtini/repair-drizzle-migration-conflict.ts
Last active
June 11, 2025 15:04
-
-
Save anthonyjoeseph/6b99beb34d494acd1dfc83a192ed9388 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MIT licensed (© 2024) | |
// https://opensource.org/license/mit | |
// | |
// Source: https://gist.github.com/anthonyjoeseph/6b99beb34d494acd1dfc83a192ed9388/edit | |
// Original (with bug): https://gist.github.com/gburtini/7e34842c567dd80ee834de74e7b79edd | |
import fs from "fs"; | |
import config from "../../drizzle.config"; | |
import path from "path"; | |
import { execSync } from "child_process"; | |
if (!config.out || !fs.existsSync(config.out)) { | |
console.error(`Directory ${config.out} does not exist`); | |
console.error("Maybe your drizzle.config.ts can't be found."); | |
process.exit(1); | |
} | |
const files = fs.readdirSync(config.out).sort(); | |
const rawJournal = fs.readFileSync( | |
path.join(config.out, "meta", "_journal.json"), | |
"utf8", | |
); | |
// a conflict is determined when two files have the same migration number | |
function findAllConflict(files: string[]) { | |
const conflicts = []; | |
let foundConflict = false; | |
let lastMigration = null; | |
for (let i = 0; i < files.length; i++) { | |
const file = files[i] as string; | |
const match = file.match(/^(\d+)_/); | |
if (!match?.[1]) continue; | |
const migration = parseInt(match[1]); | |
if (foundConflict) { | |
conflicts.push(file); | |
} else if (migration === lastMigration) { | |
// everything after the first conflict is a conflict | |
foundConflict = true; | |
conflicts.push(files[i - 1] as string); | |
conflicts.push(file); | |
} | |
lastMigration = migration; | |
} | |
return conflicts; | |
} | |
const conflicts = findAllConflict(files); | |
const firstConflict = conflicts[0]; | |
if (!firstConflict) { | |
console.log("No migration conflicts found."); | |
process.exit(0); | |
} | |
console.error(`Migration conflict found starting at ${pad(firstConflict)}`); | |
// if we have a journal, it may have git conflict markers. don't touch it if it doesn't, so we don't accidentally throw anything away. | |
if (rawJournal.includes("<<<<<<<")) { | |
if (process.env.FORCE_FIX) { | |
console.log("Resetting journal file to the state of the current branch."); | |
execSync(`git checkout --theirs ${path.join(config.out, "meta")}`); | |
} else { | |
console.error("Journal file contains git conflict markers."); | |
console.error("Restore the journal to the state _before_ this change."); | |
console.error( | |
"That is, return to the state that was in main or the parent branch.", | |
); | |
console.error("As a rule, never merge anything in the meta directory."); | |
process.exit(1); | |
} | |
} | |
// read the journal to identify which migrations to keep | |
const reloadedJournal = fs.readFileSync( | |
path.join(config.out, "meta", "_journal.json"), | |
"utf8", | |
); | |
const journal = JSON.parse(reloadedJournal) as { | |
entries: { idx: number; tag: string }[]; | |
}; | |
for (const conflict of conflicts) { | |
const [idx] = conflict.split("_"); | |
const tag = conflict.replace(".sql", ""); | |
if (!idx || !tag) { | |
console.error(`Could not parse migration number and tag from ${conflict}`); | |
process.exit(1); | |
} | |
const journalEntry = journal.entries.find( | |
(entry) => entry.idx === parseInt(idx), | |
) as { | |
idx: number; | |
tag: string; | |
}; | |
if (!journalEntry) { | |
console.error(`Migration ${pad(idx)} not found in journal.`); | |
if (process.env.FORCE_FIX) { | |
execSync("rm -f " + path.join(config.out, conflict)); | |
console.log(`Removed migration ${pad(idx)}: ${conflict}`); | |
execSync( | |
"rm -f " + path.join(config.out, "meta", `${pad(idx)}_snapshot.json`), | |
); | |
console.log(`Removed snapshot ${pad(idx)}: ${pad(idx)}_snapshot.json`); | |
continue; | |
} else { | |
process.exit(1); | |
} | |
} else if (journalEntry.tag === tag) { | |
console.log(`Keeping migration ${pad(idx)}: ${journalEntry.tag}`); | |
continue; | |
} else { | |
if (process.env.FORCE_FIX) { | |
execSync("rm -f " + path.join(config.out, conflict)); | |
console.log(`Removed migration ${pad(idx)}: ${conflict}`); | |
continue; | |
} else { | |
console.error( | |
`Migration ${pad(idx)} is tagged ${journalEntry.tag} in journal.`, | |
); | |
console.error(`Expected tag ${tag} for migration ${pad(idx)}.`); | |
process.exit(1); | |
} | |
} | |
} | |
if (process.env.FORCE_FIX) { | |
execSync("pnpm run db:generate"); | |
console.log("All conflicts resolved. Migrations regenerated."); | |
} else { | |
console.log("All conflicts resolved. Regenerate migrations for your changes."); | |
} | |
console.log( | |
"When you open the PR, ensure there are no changes to historical entries in the journal or meta snapshot files.", | |
); | |
function pad(num: number | string, length = 4) { | |
return num.toString().padStart(length, "0"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment