Skip to content

Instantly share code, notes, and snippets.

@anthonyjoeseph
Last active June 11, 2025 16:36
Show Gist options
  • Save anthonyjoeseph/102c0e3ea8496fe111029a8b8a95cc3a to your computer and use it in GitHub Desktop.
Save anthonyjoeseph/102c0e3ea8496fe111029a8b8a95cc3a to your computer and use it in GitHub Desktop.
undo-drizzle-migration-during-merge.ts
// MIT licensed (© 2024)
// https://opensource.org/license/mit
//
// Source: https://gist.github.com/anthonyjoeseph/102c0e3ea8496fe111029a8b8a95cc3a
// motivation:
// `drizzle-kit generate` has a default behavior that causes headaches
// it will randomly generate names for migration files
// e.g. "0012_swift_rhino.sql"
// this means that migration conflicts across branches are easy to miss
// e.g. one branch might have a migration called "0012_swift_rhino.sql"
// while another branch might have "0012_cold_inertia.sql"
// while the snapshots & _journal.json will have conflicts, the sql files won't
// and it's not immediately obvious how to resolve any of this
// usage:
// you should use the `--name` flag with drizzle-kit generate
// like so: `drizzle-kit generate --name migration`
// this will generate migrations that always look like this: "0023_migration.sql"
// forcing merge conflicts across branches
//
// in case of a merge conflict,
// use this script to undo your own migration
//
// it will run `git checkout --theirs ${path.join(config.out)}`
// and `git add ${path.join(config.out)}`
// and then delete all of the migrations and snapshots
// that don't match the incoming branch
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
import config from "../../drizzle.config";
const gitRoot = execSync("git rev-parse --show-toplevel").toString().trim();
if (!fs.existsSync(path.join(gitRoot, ".git", "MERGE_HEAD"))) {
console.log("no 'git merge' in process");
process.exit(0);
}
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 snapshotFiles = fs.readdirSync(path.join(config.out, "meta")).sort();
console.log("Resetting migration files to the state of the current branch.");
execSync(`git checkout --theirs ${path.join(config.out)}`);
execSync(`git add ${path.join(config.out)}`);
// read the journal to identify which migrations to keep
const journal = JSON.parse(
fs.readFileSync(path.join(config.out, "meta", "_journal.json"), "utf8"),
) as {
entries: { idx: number; tag: string }[];
};
const sortedIdxs = journal.entries.sort(({ idx }, { idx: idx2 }) => idx - idx2);
const highestIdx = sortedIdxs[sortedIdxs.length - 1]?.idx;
if (highestIdx === undefined) {
console.error("malformed _journal.json");
process.exit(1);
}
console.log(`highest idx in _journal.json: ${highestIdx}`);
const unusedMigrations = files.filter((filename) => {
if (!filename.endsWith(".sql")) return false;
const [idx] = filename.split("_");
return !idx || parseInt(idx) > highestIdx;
});
const unusedSnapshots = snapshotFiles.filter((filename) => {
if (filename === "_journal.json") return false;
const [idx] = filename.split("_");
return !idx || parseInt(idx) > highestIdx;
});
for (const unusedMigration of unusedMigrations) {
console.log(`deleting unused migration ${unusedMigration}`);
fs.unlinkSync(path.join(config.out, unusedMigration));
}
for (const unusedSnapshot of unusedSnapshots) {
console.log(`deleting unused snapshot ${unusedSnapshot}`);
fs.unlinkSync(path.join(config.out, "meta", unusedSnapshot));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment