diff --git a/README.md b/README.md index 92d18a8..da2b4a1 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,8 @@ Options: --shadow Applies migrations to shadow DB. [boolean] [default: false] --erase This is your double opt-in to make it clear this DELETES EVERYTHING. [boolean] [default: false] + --force Terminate all existing connections to the database. + [boolean] [default: false] ``` diff --git a/__tests__/reset.test.ts b/__tests__/reset.test.ts new file mode 100644 index 0000000..9dff0f3 --- /dev/null +++ b/__tests__/reset.test.ts @@ -0,0 +1,63 @@ +import { reset } from "../src/commands/reset"; +import { withClient } from "../src/pgReal"; + +jest.mock("../src/commands/migrate"); + +jest.mock("../src/pgReal", () => { + const actual = jest.requireActual("../src/pgReal"); + const allAutoMocked = jest.createMockFromModule("../src/pgReal"); + + return { + ...allAutoMocked, + withClient: jest.fn(), + escapeIdentifier: actual.escapeIdentifier, + }; +}); + +let mockPgClient: { + query: jest.Mock; +}; + +describe("reset", () => { + beforeEach(async () => { + mockPgClient = { + query: jest.fn(), + }; + + (withClient as any).mockImplementation( + async (_connString: any, _settings: any, callback: any) => { + await callback(mockPgClient); + }, + ); + }); + + it("calls DROP DATABASE without FORCE when force is false", async () => { + await reset( + { + connectionString: "test_db", + }, + false, + false, + ); + + expect(mockPgClient.query).toHaveBeenNthCalledWith( + 1, + 'DROP DATABASE IF EXISTS "test_db";', + ); + }); + + it("calls DROP DATABASE with FORCE when force is true", async () => { + await reset( + { + connectionString: "test_db", + }, + false, + true, + ); + + expect(mockPgClient.query).toHaveBeenNthCalledWith( + 1, + 'DROP DATABASE IF EXISTS "test_db" WITH (FORCE);', + ); + }); +}); diff --git a/src/commands/commit.ts b/src/commands/commit.ts index 41bede9..770eecc 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -95,7 +95,7 @@ export async function _commit( Message: message ? message : undefined, ...omit(headers, ["Previous", "Hash", "Message"]), }); - await _reset(parsedSettings, true); + await _reset(parsedSettings, true, false); const newMigrationFilepath = `${committedMigrationsFolder}/${newMigrationFilename}`; await fsp.writeFile(newMigrationFilepath, finalBody); await fsp.chmod(newMigrationFilepath, "440"); diff --git a/src/commands/reset.ts b/src/commands/reset.ts index e70d69d..d49df25 100644 --- a/src/commands/reset.ts +++ b/src/commands/reset.ts @@ -9,11 +9,13 @@ import { _migrate } from "./migrate"; interface ResetArgv extends CommonArgv { shadow: boolean; erase: boolean; + force: boolean; } export async function _reset( parsedSettings: ParsedSettings, shadow: boolean, + force: boolean, ): Promise { const connectionString = shadow ? parsedSettings.shadowConnectionString @@ -34,9 +36,15 @@ export async function _reset( } const databaseOwner = parsedSettings.databaseOwner; const logSuffix = shadow ? "[shadow]" : ""; - await pgClient.query( - `DROP DATABASE IF EXISTS ${escapeIdentifier(databaseName)};`, - ); + if (force) { + await pgClient.query( + `DROP DATABASE IF EXISTS ${escapeIdentifier(databaseName)} WITH (FORCE);`, + ); + } else { + await pgClient.query( + `DROP DATABASE IF EXISTS ${escapeIdentifier(databaseName)};`, + ); + } parsedSettings.logger.info( `graphile-migrate${logSuffix}: dropped database '${databaseName}'`, ); @@ -63,9 +71,13 @@ export async function _reset( await _migrate(parsedSettings, shadow); } -export async function reset(settings: Settings, shadow = false): Promise { +export async function reset( + settings: Settings, + shadow = false, + force = false, +): Promise { const parsedSettings = await parseSettings(settings, shadow); - return _reset(parsedSettings, shadow); + return _reset(parsedSettings, shadow, force); } export const resetCommand: CommandModule, ResetArgv> = { @@ -85,6 +97,11 @@ export const resetCommand: CommandModule, ResetArgv> = { description: "This is your double opt-in to make it clear this DELETES EVERYTHING.", }, + force: { + type: "boolean", + default: false, + description: "Terminate all existing connections to the database.", + }, }, handler: async (argv) => { if (!argv.erase) { @@ -94,6 +111,10 @@ export const resetCommand: CommandModule, ResetArgv> = { ); process.exit(2); } - await reset(await getSettings({ configFile: argv.config }), argv.shadow); + await reset( + await getSettings({ configFile: argv.config }), + argv.shadow, + argv.force, + ); }, }; diff --git a/src/commands/uncommit.ts b/src/commands/uncommit.ts index 01fd188..97f535b 100644 --- a/src/commands/uncommit.ts +++ b/src/commands/uncommit.ts @@ -56,7 +56,7 @@ export async function _uncommit(parsedSettings: ParsedSettings): Promise { ); // Reset shadow - await _reset(parsedSettings, true); + await _reset(parsedSettings, true, false); await _migrate(parsedSettings, true, true); }