Skip to content

Commit 439df3e

Browse files
add dryRun to migrate
1 parent dbfc5cc commit 439df3e

File tree

8 files changed

+192
-5
lines changed

8 files changed

+192
-5
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,32 @@ async function() {
7575
}
7676
```
7777

78+
### Config
79+
80+
#### `logger`
81+
82+
This will be used for all logging from `postgres-migrations`. The function will be called passing a
83+
string argument with each call.
84+
85+
#### `dryRun`
86+
87+
Setting `dryRun` to `true` will log the filenames and SQL of all transactions to be run, without running them. It validates that the file names are the appropriate format, but it does not perform any validation of the migrations themselves so they may still fail after running.
88+
89+
The logs look like this:
90+
91+
```
92+
Migrations to run:
93+
1_first_migration.sql
94+
2_second_migration.js
95+
96+
CREATE TABLE first_migration_table (
97+
id integer
98+
);
99+
100+
ALTER TABLE first_migration_table
101+
ADD second_migration_column integer;
102+
```
103+
78104
### Validating migration files
79105

80106
Occasionally, if two people are working on the same codebase independently, they might both create a migration at the same time. For example, `5_add-table.sql` and `5_add-column.sql`. If these both get pushed, there will be a conflict.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TABLE dry_run_false (
2+
id integer
3+
);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TABLE dry_run_no_migrations (
2+
id integer
3+
);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TABLE dry_run_true (
2+
id integer
3+
);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports.generateSql = () =>`
2+
ALTER TABLE dry_run_true
3+
ADD new_column integer;`

src/__tests__/migrate.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import * as pg from "pg"
44
import SQL from "sql-template-strings"
55
import {createDb, migrate, MigrateDBConfig} from "../"
66
import {PASSWORD, startPostgres, stopPostgres} from "./fixtures/docker-postgres"
7+
import * as sinon from "sinon"
8+
import {SinonSpy} from "sinon"
79

810
const CONTAINER_NAME = "pg-migrations-test-migrate"
911

@@ -284,6 +286,119 @@ test("successful complex js migration", (t) => {
284286
})
285287
})
286288

289+
test("with dryRun true", (t) => {
290+
const logSpy = sinon.spy()
291+
const databaseName = "migration-with-dryRun-true-test"
292+
const dbConfig = {
293+
database: databaseName,
294+
user: "postgres",
295+
password: PASSWORD,
296+
host: "localhost",
297+
port,
298+
}
299+
300+
const expectedLog = `
301+
Migrations to run:
302+
1_dry_run_true_first.sql
303+
2_dry_run_true_second.js
304+
305+
CREATE TABLE dry_run_true (
306+
id integer
307+
);
308+
309+
ALTER TABLE dry_run_true
310+
ADD new_column integer;
311+
`
312+
313+
return createDb(databaseName, dbConfig)
314+
.then(() => migrate(dbConfig, "src/__tests__/fixtures/empty"))
315+
.then(() =>
316+
migrate(dbConfig, "src/__tests__/fixtures/dry-run-true", {
317+
dryRun: true,
318+
logger: logSpy,
319+
}),
320+
)
321+
.then(() => doesTableExist(dbConfig, "dry_run_true"))
322+
.then((exists) => {
323+
t.falsy(exists)
324+
t.assert(
325+
logSpy.calledWith(expectedLog),
326+
`expected logger to have been called with ${expectedLog} but was called with ${allArgsForCalls(
327+
logSpy,
328+
)}`,
329+
)
330+
})
331+
})
332+
333+
test("with dryRun true but no migrations to run", (t) => {
334+
const logSpy = sinon.spy()
335+
const databaseName = "migration-with-dryRun-no-migrations-test"
336+
const dbConfig = {
337+
database: databaseName,
338+
user: "postgres",
339+
password: PASSWORD,
340+
host: "localhost",
341+
port,
342+
}
343+
344+
const expectedLog = "\nNo new migrations to run\n"
345+
346+
return createDb(databaseName, dbConfig)
347+
.then(() =>
348+
migrate(dbConfig, "src/__tests__/fixtures/dry-run-no-migrations"),
349+
)
350+
.then(() => doesTableExist(dbConfig, "dry_run_no_migrations"))
351+
.then((exists) => {
352+
t.truthy(exists)
353+
})
354+
.then(() =>
355+
migrate(dbConfig, "src/__tests__/fixtures/dry-run-no-migrations", {
356+
dryRun: true,
357+
logger: logSpy,
358+
}),
359+
)
360+
.then(() => {
361+
t.assert(
362+
logSpy.calledWith(expectedLog),
363+
`expected logger to have been called with ${expectedLog} but was called with ${allArgsForCalls(
364+
logSpy,
365+
)}`,
366+
)
367+
})
368+
})
369+
370+
test("with dryRun false", (t) => {
371+
const logSpy = sinon.spy()
372+
const databaseName = "migration-with-dryRun-false-test"
373+
const dbConfig = {
374+
database: databaseName,
375+
user: "postgres",
376+
password: PASSWORD,
377+
host: "localhost",
378+
port,
379+
}
380+
381+
const unexpectedLog = "CREATE TABLE dry_run_false"
382+
383+
return createDb(databaseName, dbConfig)
384+
.then(() =>
385+
migrate(dbConfig, "src/__tests__/fixtures/dry-run-false", {
386+
dryRun: false,
387+
logger: logSpy,
388+
}),
389+
)
390+
.then(() => doesTableExist(dbConfig, "dry_run_false"))
391+
.then((exists) => {
392+
t.truthy(exists)
393+
t.assert(
394+
logSpy.neverCalledWithMatch(unexpectedLog),
395+
`expected logger not to be called with string matching ${unexpectedLog} but was called with ${allArgsForCalls(
396+
logSpy,
397+
)}`,
398+
)
399+
})
400+
})
401+
287402
test("bad arguments - no db config", (t) => {
288403
// tslint:disable-next-line no-any
289404
return t.throwsAsync((migrate as any)()).then((err) => {
@@ -720,3 +835,10 @@ function doesTableExist(dbConfig: pg.ClientConfig, tableName: string) {
720835
}
721836
})
722837
}
838+
839+
function allArgsForCalls(spy: SinonSpy) {
840+
return spy
841+
.getCalls()
842+
.map((call) => call.args)
843+
.join(" | ")
844+
}

src/migrate.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export async function migrate(
3838
//
3939
}
4040

41+
const dryRun = config.dryRun === undefined ? false : config.dryRun
42+
4143
if (dbConfig == null) {
4244
throw new Error("No config object")
4345
}
@@ -51,7 +53,7 @@ export async function migrate(
5153
// we have been given a client to use, it should already be connected
5254
return withAdvisoryLock(
5355
log,
54-
runMigrations(intendedMigrations, log),
56+
runMigrations(intendedMigrations, log, dryRun),
5557
)(dbConfig.client)
5658
}
5759

@@ -99,15 +101,19 @@ export async function migrate(
99101

100102
const runWith = withConnection(
101103
log,
102-
withAdvisoryLock(log, runMigrations(intendedMigrations, log)),
104+
withAdvisoryLock(log, runMigrations(intendedMigrations, log, dryRun)),
103105
)
104106

105107
return runWith(client)
106108
}
107109
}
108110

109-
function runMigrations(intendedMigrations: Array<Migration>, log: Logger) {
110-
return async (client: BasicPgClient) => {
111+
function runMigrations(
112+
intendedMigrations: Array<Migration>,
113+
log: Logger,
114+
dryRun: boolean,
115+
) {
116+
return async (client: BasicPgClient): Promise<Array<Migration>> => {
111117
try {
112118
const migrationTableName = "migrations"
113119

@@ -125,7 +131,12 @@ function runMigrations(intendedMigrations: Array<Migration>, log: Logger) {
125131
intendedMigrations,
126132
appliedMigrations,
127133
)
128-
const completedMigrations = []
134+
const completedMigrations: Array<Migration> = []
135+
136+
if (dryRun) {
137+
logDryRun(migrationsToRun, log)
138+
return completedMigrations
139+
}
129140

130141
for (const migration of migrationsToRun) {
131142
log(`Starting migration: ${migration.id} ${migration.name}`)
@@ -211,3 +222,18 @@ async function doesTableExist(client: BasicPgClient, tableName: string) {
211222

212223
return result.rows.length > 0 && result.rows[0].exists
213224
}
225+
226+
function logDryRun(migrations: Array<Migration>, log: Logger) {
227+
if (migrations.length === 0) {
228+
log("\nNo new migrations to run\n")
229+
} else {
230+
const logString =
231+
"\nMigrations to run:\n" +
232+
migrations.map((m) => ` ${m.fileName}`).join("\n") +
233+
"\n\n" +
234+
migrations.map((m) => m.sql.trim()).join("\n\n") +
235+
"\n"
236+
237+
log(logString)
238+
}
239+
}

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export type Config = Partial<FullConfig>
5959

6060
export interface FullConfig {
6161
readonly logger: Logger
62+
readonly dryRun: boolean
6263
}
6364

6465
export class MigrationError extends Error {

0 commit comments

Comments
 (0)