Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion bin/node-pg-migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import type { DotenvConfigOptions } from 'dotenv';
// Import as node-pg-migrate, so tsup does not self-reference as '../dist'
// otherwise this could not be imported by esm
// @ts-ignore: when a clean was made, the types are not present in the first run
import { Migration, runner as migrationRunner } from 'node-pg-migrate';
import {
Migration,
runner as migrationRunner,
PG_MIGRATE_LOCK_ID,
} from 'node-pg-migrate';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: I'm not sure what's going on here, when I try to run everything locally and rebuild everything it works

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems rebasing got rid of the links to the old action runs

I think I had to do this because when I added an import to the file that contains the constant directly, the src was not converted to dist, so it failed to import

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trying something out with #1408

import { readFileSync } from 'node:fs';
import { register } from 'node:module';
import { join, resolve } from 'node:path';
Expand Down Expand Up @@ -64,6 +68,7 @@ const configFileArg = 'config-file';
const ignorePatternArg = 'ignore-pattern';
const singleTransactionArg = 'single-transaction';
const lockArg = 'lock';
const lockValueArg = 'lock-value';
const timestampArg = 'timestamp';
const dryRunArg = 'dry-run';
const fakeArg = 'fake';
Expand Down Expand Up @@ -213,6 +218,11 @@ const parser = yargs(process.argv.slice(2))
describe: 'When false, disables locking mechanism and checks',
type: 'boolean',
},
[lockValueArg]: {
default: PG_MIGRATE_LOCK_ID,
describe: 'The value to use for the lock',
type: 'number',
},
[rejectUnauthorizedArg]: {
defaultDescription: 'false',
describe: 'Sets rejectUnauthorized SSL option',
Expand Down Expand Up @@ -551,6 +561,7 @@ if (action === 'create') {
const TIMESTAMP = argv[timestampArg];
const rejectUnauthorized = argv[rejectUnauthorizedArg];
const noLock = !argv[lockArg];
const lockValue = argv[lockValueArg];
if (noLock) {
console.log('no lock');
}
Expand Down Expand Up @@ -616,6 +627,7 @@ if (action === 'create') {
direction,
singleTransaction,
noLock,
lockValue,
fake,
decamelize: DECAMELIZE,
};
Expand Down
1 change: 1 addition & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ which takes options argument with the following structure (similar to [command l
| `createSchema` | `boolean` | Creates the configured schema if it doesn't exist |
| `createMigrationsSchema` | `boolean` | Creates the configured migration schema if it doesn't exist |
| `noLock` | `boolean` | Disables locking mechanism and checks |
| `lockValue` | `number` | Value to use for the lock |
| `fake` | `boolean` | Mark migrations as run without actually performing them (use with caution!) |
| `dryRun` | `boolean` | |
| `log` | `function` | Redirect log messages to this function, rather than `console` |
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export type {
ViewOptions,
} from './operations/views';
export { PgType } from './pgType';
export { runner } from './runner';
export { PG_MIGRATE_LOCK_ID, runner } from './runner';
export type { RunnerOption } from './runner';
export { PgLiteral, isPgLiteral } from './utils';
export type { PgLiteralValue } from './utils';
25 changes: 18 additions & 7 deletions src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ export interface RunnerOptionConfig {
*/
noLock?: boolean;

/**
* Value to use for the lock.
*/
lockValue?: number;

/**
* Mark migrations as run without actually performing them (use with caution!).
*/
Expand Down Expand Up @@ -155,7 +160,7 @@ export type RunnerOption = RunnerOptionConfig &
/**
* Random but well-known identifier shared by all instances of `node-pg-migrate`.
*/
const PG_MIGRATE_LOCK_ID = 7_241_865_325_823_964;
export const PG_MIGRATE_LOCK_ID = 7_241_865_325_823_964;

const idColumn = 'id';
const nameColumn = 'name';
Expand Down Expand Up @@ -202,19 +207,25 @@ async function loadMigrations(
}
}

async function lock(db: DBConnection): Promise<void> {
async function lock(
db: DBConnection,
lockValue: number = PG_MIGRATE_LOCK_ID
): Promise<void> {
const [result] = await db.select(
`SELECT pg_try_advisory_lock(${PG_MIGRATE_LOCK_ID}) AS "lockObtained"`
`SELECT pg_try_advisory_lock(${lockValue}) AS "lockObtained"`
);

if (!result.lockObtained) {
throw new Error('Another migration is already running');
}
}

async function unlock(db: DBConnection): Promise<void> {
async function unlock(
db: DBConnection,
lockValue: number = PG_MIGRATE_LOCK_ID
): Promise<void> {
const [result] = await db.select(
`SELECT pg_advisory_unlock(${PG_MIGRATE_LOCK_ID}) AS "lockReleased"`
`SELECT pg_advisory_unlock(${lockValue}) AS "lockReleased"`
);

if (!result.lockReleased) {
Expand Down Expand Up @@ -405,7 +416,7 @@ export async function runner(options: RunnerOption): Promise<RunMigration[]> {
await db.createConnection();

if (!options.noLock) {
await lock(db);
await lock(db, options.lockValue);
}

if (options.schema) {
Expand Down Expand Up @@ -483,7 +494,7 @@ export async function runner(options: RunnerOption): Promise<RunMigration[]> {
} finally {
if (db.connected()) {
if (!options.noLock) {
await unlock(db).catch((error: unknown) => {
await unlock(db, options.lockValue).catch((error: unknown) => {
logger.warn((error as Error).message);
});
}
Expand Down
53 changes: 53 additions & 0 deletions test/runner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,57 @@ describe('runner', () => {
).resolves.not.toThrow();
expect(executedMigrations).toHaveLength(0);
});

it('should use the provided lock value', async () => {
const customLockValue = 12345;
const dbClient = {
query: vi.fn((query) => {
switch (query) {
case `SELECT pg_try_advisory_lock(${customLockValue}) AS "lockObtained"`: {
return Promise.resolve({
rows: [{ lockObtained: true }], // lock obtained with custom value
});
}

case "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'pgmigrations'": {
return Promise.resolve({
rows: [{}], // migration table exists
});
}

case "SELECT constraint_name FROM information_schema.table_constraints WHERE table_schema = 'public' AND table_name = 'pgmigrations' AND constraint_type = 'PRIMARY KEY'": {
return Promise.resolve({
rows: [{ constraint_name: 'pk_constraint' }], // primary key exists
});
}

case 'SELECT name FROM "public"."pgmigrations" ORDER BY run_on, id': {
return Promise.resolve({
rows: [], // no migrations executed
});
}

default: {
return Promise.resolve({ rows: [{}] }); // bypass other queries
}
}
}),
} as unknown as ClientBase;

await expect(
runner({
dbClient,
migrationsTable: 'pgmigrations',
dir: 'test/cockroach',
direction: 'up',
lockValue: customLockValue,
})
).resolves.not.toThrow();

// Verify that the query with custom lock value was called
expect(dbClient.query).toHaveBeenCalledWith(
`SELECT pg_try_advisory_lock(${customLockValue}) AS "lockObtained"`,
undefined
);
});
});