From 84016dd5800be80fa6fc0380cbb68d2f35ee707f Mon Sep 17 00:00:00 2001 From: Austin Shelby Date: Fri, 14 Mar 2025 03:24:38 +0200 Subject: [PATCH 1/2] add-reset-force-option --- README.md | 2 ++ __tests__/reset.test.ts | 62 ++++++++++++++++++++++++++++++++++++++++ src/commands/commit.ts | 2 +- src/commands/reset.ts | 33 +++++++++++++++++---- src/commands/uncommit.ts | 2 +- 5 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 __tests__/reset.test.ts 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..652b1bd --- /dev/null +++ b/__tests__/reset.test.ts @@ -0,0 +1,62 @@ +import { _reset } from "../src/commands/reset"; +import { ParsedSettings, parseSettings } from "../src/settings"; +import { withClient } from "../src/pgReal"; + +jest.mock("../src/commands/migrate"); + +jest.mock("../src/pgReal", () => ({ + withClient: jest.fn(), + escapeIdentifier: (id: string) => `"${id}"`, +})); + +let parsedSettings: ParsedSettings; + +let mockPgClient: { + query: jest.Mock; +}; + +describe("_reset", () => { + beforeEach(async () => { + parsedSettings = await parseSettings({ + connectionString: "test_db", + rootConnectionString: "[rootConnectionString]", + + placeholders: { + ":DATABASE_AUTHENTICATOR": "[DATABASE_AUTHENTICATOR]", + ":DATABASE_AUTHENTICATOR_PASSWORD": "[DATABASE_AUTHENTICATOR_PASSWORD]", + }, + beforeReset: [], + beforeAllMigrations: [], + beforeCurrent: [], + afterReset: [], + afterAllMigrations: [], + afterCurrent: [], + }); + + 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(parsedSettings, false, false); + + expect(mockPgClient.query).toHaveBeenCalledWith( + 'DROP DATABASE IF EXISTS "test_db";', + ); + }); + + it("calls DROP DATABASE with FORCE when force is true", async () => { + await _reset(parsedSettings, false, true); + + expect(mockPgClient.query).toHaveBeenCalledWith( + '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); } From 5c507c8420607aab8649d2563dbcd14273bdbac5 Mon Sep 17 00:00:00 2001 From: Austin Shelby Date: Fri, 14 Mar 2025 05:05:33 +0200 Subject: [PATCH 2/2] cleanup test --- __tests__/reset.test.ts | 57 +++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/__tests__/reset.test.ts b/__tests__/reset.test.ts index 652b1bd..9dff0f3 100644 --- a/__tests__/reset.test.ts +++ b/__tests__/reset.test.ts @@ -1,38 +1,25 @@ -import { _reset } from "../src/commands/reset"; -import { ParsedSettings, parseSettings } from "../src/settings"; +import { reset } from "../src/commands/reset"; import { withClient } from "../src/pgReal"; jest.mock("../src/commands/migrate"); -jest.mock("../src/pgReal", () => ({ - withClient: jest.fn(), - escapeIdentifier: (id: string) => `"${id}"`, -})); +jest.mock("../src/pgReal", () => { + const actual = jest.requireActual("../src/pgReal"); + const allAutoMocked = jest.createMockFromModule("../src/pgReal"); -let parsedSettings: ParsedSettings; + return { + ...allAutoMocked, + withClient: jest.fn(), + escapeIdentifier: actual.escapeIdentifier, + }; +}); let mockPgClient: { query: jest.Mock; }; -describe("_reset", () => { +describe("reset", () => { beforeEach(async () => { - parsedSettings = await parseSettings({ - connectionString: "test_db", - rootConnectionString: "[rootConnectionString]", - - placeholders: { - ":DATABASE_AUTHENTICATOR": "[DATABASE_AUTHENTICATOR]", - ":DATABASE_AUTHENTICATOR_PASSWORD": "[DATABASE_AUTHENTICATOR_PASSWORD]", - }, - beforeReset: [], - beforeAllMigrations: [], - beforeCurrent: [], - afterReset: [], - afterAllMigrations: [], - afterCurrent: [], - }); - mockPgClient = { query: jest.fn(), }; @@ -45,17 +32,31 @@ describe("_reset", () => { }); it("calls DROP DATABASE without FORCE when force is false", async () => { - await _reset(parsedSettings, false, false); + await reset( + { + connectionString: "test_db", + }, + false, + false, + ); - expect(mockPgClient.query).toHaveBeenCalledWith( + expect(mockPgClient.query).toHaveBeenNthCalledWith( + 1, 'DROP DATABASE IF EXISTS "test_db";', ); }); it("calls DROP DATABASE with FORCE when force is true", async () => { - await _reset(parsedSettings, false, true); + await reset( + { + connectionString: "test_db", + }, + false, + true, + ); - expect(mockPgClient.query).toHaveBeenCalledWith( + expect(mockPgClient.query).toHaveBeenNthCalledWith( + 1, 'DROP DATABASE IF EXISTS "test_db" WITH (FORCE);', ); });