@clickup/pg-mig
Version:
PostgreSQL schema migration tool with microsharding and clustering support
198 lines (180 loc) • 5.72 kB
text/typescript
import { basename } from "path";
import compact from "lodash/compact";
import type { MigrateOptions } from "../cli";
import { Dest } from "../internal/Dest";
import { Grid } from "../internal/Grid";
import type { Chain } from "../internal/Patch";
import { Patch } from "../internal/Patch";
import {
ProgressPrinterStream,
ProgressPrinterTTY,
} from "../internal/ProgressPrinter";
import type { Registry } from "../internal/Registry";
import {
printError,
printSuccess,
printText,
renderGrid,
renderLatestVersions,
renderPatchSummary,
} from "../internal/render";
const MIN_TTY_ROWS = 5;
/**
* Applies or undoes migrations.
*/
export async function actionUndoOrApply(
options: MigrateOptions,
hostDests: Dest[],
registry: Registry,
): Promise<{ success: boolean; hasMoreWork: boolean }> {
const digest = registry.getDigest();
if (options.action.type === "apply" && options.createDB) {
for (const dest of hostDests) {
await dest
.createDB((e: unknown) =>
printText(
`PostgreSQL host ${dest.getName()} is not yet up; waiting (${e})...`,
),
)
.then(
(status) =>
status === "created" &&
printText(`Database ${dest.getName()} did not exist; created.`),
);
}
}
if (options.action.type === "undo" && !options.action.version) {
printText(
await renderLatestVersions(
hostDests,
registry,
options.validShardSchemasSql,
),
);
printError("Please provide a migration version to undo.");
return { success: false, hasMoreWork: false };
}
const patch = new Patch(hostDests, registry, {
undo: options.action.type === "undo" ? options.action.version : undefined,
validShardSchemasSql: options.validShardSchemasSql,
});
const chains = await patch.getChains();
// If we are going to undo something, reset the digest in the DB before
// running the down migrations, so if we fail partially, the digest in the DB
// will be reset.
if (options.action.type === "undo" && chains.length > 0 && !options.dry) {
await Dest.saveDigests(hostDests, { reset: "before-undo" });
}
const beforeAfterFiles = compact([
registry.beforeFile?.fileName,
registry.afterFile?.fileName,
]);
if (
chains.length === 0 &&
(await Dest.checkRerunFingerprint(hostDests, beforeAfterFiles)) &&
!options.force
) {
// If we have nothing to apply, save the digest in case it was not saved
// previously, to keep the invariant.
if (options.action.type === "apply" && !options.dry) {
await Dest.saveDigests(hostDests, { digest });
}
printText(
await renderLatestVersions(
hostDests,
registry,
options.validShardSchemasSql,
),
);
printText(renderPatchSummary(chains, []));
printSuccess("Nothing to do.");
return { success: true, hasMoreWork: false };
}
if (options.dry) {
printText(
await renderLatestVersions(
hostDests,
registry,
options.validShardSchemasSql,
),
);
printText(renderPatchSummary(chains, beforeAfterFiles));
printSuccess("Dry-run mode.");
return { success: true, hasMoreWork: false };
}
printText(renderPatchSummary(chains, beforeAfterFiles));
// Remember that if we crash below (e.g. in after.sql), we'll need to run
// before.sql+after.sql on retry even if there are no new migration versions
await Dest.saveRerunFingerprint(hostDests, beforeAfterFiles, "reset");
const grid = new Grid(
chains,
options.parallelism ?? 10,
registry.beforeFile
? hostDests.map<Chain>((dest) => ({
type: "dn",
dest,
migrations: [
{
version: basename(registry.beforeFile!.fileName),
file: registry.beforeFile!,
newVersions: null,
},
],
}))
: [],
registry.afterFile
? hostDests.map<Chain>((dest) => ({
type: "up",
dest,
migrations: [
{
version: basename(registry.afterFile!.fileName),
file: registry.afterFile!,
newVersions: null,
},
],
}))
: [],
);
const progress =
process.stdout.isTTY &&
process.stdout.rows &&
process.stdout.rows >= MIN_TTY_ROWS
? new ProgressPrinterTTY()
: new ProgressPrinterStream();
const success = await grid.run(
progress.throttle(() =>
progress.print(renderGrid(grid, progress.skipEmptyLines()).lines),
),
);
progress.clear();
const { lines, errors, warnings } = renderGrid(grid, true);
if (errors.length > 0) {
printError("\n###\n### FAILED. See complete error list below.\n###\n");
printText(lines.join("\n"));
printError(`Failed with ${errors.length} error(s).`);
} else if (warnings.length > 0) {
printText(
"\n###\n### SUCCEEDED with warnings. See complete warning list below.\n###\n",
);
printText(lines.join("\n"));
printSuccess(`Succeeded with ${warnings.length} warning(s).`);
} else {
printSuccess("Succeeded.");
}
if (!success) {
return { success: false, hasMoreWork: false };
}
await Dest.saveRerunFingerprint(hostDests, beforeAfterFiles, "up-to-date");
if (options.action.type === "apply") {
if ((await patch.getChains()).length > 0) {
return { success: true, hasMoreWork: true };
} else {
await Dest.saveDigests(hostDests, { digest });
return { success: true, hasMoreWork: false };
}
} else {
await Dest.saveDigests(hostDests, { reset: "after-undo" });
return { success: true, hasMoreWork: false };
}
}