graphile-migrate
Version:
Opinionated SQL-powered migration tool for PostgreSQL
185 lines • 8.63 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const chokidar = require("chokidar");
const actions_1 = require("../actions");
const instrumentation_1 = require("../instrumentation");
const migration_1 = require("../migration");
const pg_1 = require("../pg");
const settings_1 = require("../settings");
const migrate_1 = require("./migrate");
const pgMinify = require("pg-minify");
const current_1 = require("../current");
const _common_1 = require("./_common");
function _makeCurrentMigrationRunner(parsedSettings, _once = false, shadow = false) {
async function run() {
const currentLocation = await current_1.getCurrentMigrationLocation(parsedSettings);
const body = await current_1.readCurrentMigration(parsedSettings, currentLocation);
let migrationsAreEquivalent = false;
try {
parsedSettings.logger.info(`[${new Date().toISOString()}]: Running current.sql`);
const start = process.hrtime();
const connectionString = shadow
? parsedSettings.shadowConnectionString
: parsedSettings.connectionString;
if (!connectionString) {
throw new Error("Could not determine connection string for running commands");
}
await pg_1.withClient(connectionString, parsedSettings, (lockingPgClient, context) => pg_1.withTransaction(lockingPgClient, async () => {
// 1: lock graphile_migrate.current so no concurrent migrations can occur
await lockingPgClient.query("lock graphile_migrate.current in EXCLUSIVE mode");
// 2: Get last current.sql from graphile_migrate.current
const { rows: [previousCurrent], } = await lockingPgClient.query(`
select *
from graphile_migrate.current
where filename = 'current.sql'
`);
// 3: minify and compare last ran current.sql with this _COMPILED_ current.sql.
const previousBody = previousCurrent && previousCurrent.content;
const { sql: currentBodyFromDryRun } = await migration_1.runStringMigration(lockingPgClient, parsedSettings, context, body, "current.sql", undefined, true);
const previousBodyMinified = previousBody
? pgMinify(previousBody)
: null;
const currentBodyMinified = pgMinify(currentBodyFromDryRun);
migrationsAreEquivalent =
currentBodyMinified === previousBodyMinified;
// 4: if different
if (!migrationsAreEquivalent) {
await actions_1.executeActions(parsedSettings, shadow, parsedSettings.beforeCurrent);
// 4a: invert previous current; on success delete from graphile_migrate.current; on failure rollback and abort
if (previousBody) {
await migration_1.reverseMigration(lockingPgClient, previousBody);
}
// COMMIT ─ because we need to commit that the migration was reversed
await lockingPgClient.query("commit");
await lockingPgClient.query("begin");
// Re-establish a lock ASAP to continue with migration
await lockingPgClient.query("lock graphile_migrate.current in EXCLUSIVE mode");
// 4b: run this current (in its own independent transaction) if not empty
if (currentBodyMinified !== "") {
await pg_1.withClient(connectionString, parsedSettings, (independentPgClient, context) => migration_1.runStringMigration(independentPgClient, parsedSettings, context, body, "current.sql", undefined));
}
}
else {
parsedSettings.logger.info(`[${new Date().toISOString()}]: current.sql unchanged, skipping migration`);
}
// 5: update graphile_migrate.current with latest content
// (NOTE: we update even if the minified versions don't differ since
// the comments may have changed.)
await lockingPgClient.query({
name: "current-insert",
text: `
insert into graphile_migrate.current(content)
values ($1)
on conflict (filename)
do update
set content = excluded.content, date = excluded.date
`,
values: [currentBodyFromDryRun],
});
}));
const interval = process.hrtime(start);
const duration = interval[0] * 1e3 + interval[1] * 1e-6;
if (!migrationsAreEquivalent) {
await actions_1.executeActions(parsedSettings, shadow, parsedSettings.afterCurrent);
}
const interval2 = process.hrtime(start);
const duration2 = interval2[0] * 1e3 + interval2[1] * 1e-6;
parsedSettings.logger.info(`[${new Date().toISOString()}]: Finished (${duration2.toFixed(0)}ms${duration2 - duration >= 5
? `; excluding actions: ${duration.toFixed(0)}ms`
: ""})`);
}
catch (e) {
instrumentation_1.logDbError(parsedSettings, e);
throw e;
}
}
return run;
}
exports._makeCurrentMigrationRunner = _makeCurrentMigrationRunner;
async function _watch(parsedSettings, once = false, shadow = false) {
await migrate_1._migrate(parsedSettings, shadow);
const currentLocation = await current_1.getCurrentMigrationLocation(parsedSettings);
if (!currentLocation.exists) {
await current_1.writeCurrentMigration(parsedSettings, currentLocation, parsedSettings.blankMigrationContent.trim() + "\n");
}
const run = _makeCurrentMigrationRunner(parsedSettings, once, shadow);
if (once) {
return run();
}
else {
let running = false;
let runAgain = false;
const queue = () => {
if (running) {
runAgain = true;
}
running = true;
run()
.catch(error => {
if (!error["_gmlogged"]) {
parsedSettings.logger.error(`Error occurred whilst processing migration: ${error.message}`, { error });
}
})
.finally(() => {
running = false;
if (runAgain) {
runAgain = false;
queue();
}
});
};
const watcher = chokidar.watch(currentLocation.path, {
/*
* Without `usePolling`, on Linux, you can prevent the watching from
* working by issuing `git stash && sleep 2 && git stash pop`. This is
* annoying.
*/
usePolling: true,
/*
* Some editors stream the writes out a little at a time, we want to wait
* for the write to finish before triggering.
*/
awaitWriteFinish: {
stabilityThreshold: 200,
pollInterval: 100,
},
/*
* We don't want to run the queue too many times during startup; so we
* call it once on the 'ready' event.
*/
ignoreInitial: true,
});
watcher.on("add", queue);
watcher.on("change", queue);
watcher.on("unlink", queue);
watcher.once("ready", queue);
return Promise.resolve();
}
}
exports._watch = _watch;
async function watch(settings, once = false, shadow = false) {
const parsedSettings = await settings_1.parseSettings(settings, shadow);
return _watch(parsedSettings, once, shadow);
}
exports.watch = watch;
exports.watchCommand = {
command: "watch",
aliases: [],
describe: "Runs any un-executed committed migrations and then runs and watches the current migration, re-running it on any change. For development.",
builder: {
once: {
type: "boolean",
default: false,
description: "Runs the current migration and then exits.",
},
shadow: {
type: "boolean",
default: false,
description: "Applies changes to shadow DB.",
},
},
handler: async (argv) => {
await watch(await _common_1.getSettings({ configFile: argv.config }), argv.once, argv.shadow);
},
};
//# sourceMappingURL=watch.js.map