Skip to content

Instantly share code, notes, and snippets.

@gburtini
Last active June 11, 2025 15:35
Show Gist options
  • Select an option

  • Save gburtini/7e34842c567dd80ee834de74e7b79edd to your computer and use it in GitHub Desktop.

Select an option

Save gburtini/7e34842c567dd80ee834de74e7b79edd to your computer and use it in GitHub Desktop.
// MIT licensed (© 2024)
// https://opensource.org/license/mit
//
// Source: https://gist.github.com/gburtini/7e34842c567dd80ee834de74e7b79edd
import fs from "fs";
import config from "../../drizzle.config";
import path from "path";
import { exec, 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 (const file of files) {
const match = file.match(/^(\d+)_/);
if (!match?.[1]) continue;
const migration = parseInt(match[1]);
if (migration === lastMigration || foundConflict) {
// everything after the first conflict is a conflict
foundConflict = true;
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}`);
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");
}
@anthonyjoeseph
Copy link

anthonyjoeseph commented Jun 10, 2025

Just seeing your comment - good to know about the --name parameter! All other db migration tools I've ever used auto-generated their migration names, so I prefer your solution

but I think you need to keep the one that is actually already in main (because it may have already run)

I agree, but that's not necessarily the one that gets excluded in this case - it just excludes the first one alphabetically (since you sort them on line 18)

And in any case, the conflicts array should include the one in "main" (i.e. the one in _journal.json) so it can be output on line 105

@anthonyjoeseph
Copy link

anthonyjoeseph commented Jun 10, 2025

oh wait a minute - I totally missed your point about --name. If you just always use the name "migration" as in 0009-migration.sql or whatever then you wouldn't ever need to run this script with FORCE_FIX=false

Still, I think FORCE_FIX=true is useful, especially when used mid-merge

@anthonyjoeseph
Copy link

anthonyjoeseph commented Jun 10, 2025

I guess there could be a modified version of this script that would work better with --name

  • It would always force-fix
  • In case of a merge conflict it would git checkout --theirs the .sql scripts as well as the meta stuff - the whole config.out directory

And I guess the argument against --name - using random filenames instead - is that uninformed/junior devs might try to manually reconcile merge conflicts in bad ways. Like there's a world where someone merges stuff into an existing migration.sql file and runs drizzle-kit generate and it works so they try to push it

It's a stretch, I know. The more I think about drizzle using random filenames, the less sense it makes. I can't think of another orm that does that - they're usually timestamp-based

@anthonyjoeseph
Copy link

anthonyjoeseph commented Jun 11, 2025

I wrote a new script for usage with the --name parameter - gist. This is what I'll actually recommend to my team

I also found a new bug in the original script - snapshots without matching _journal.json entries should be deleted. Here's my patch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment