Skip to content

Commit 5a73373

Browse files
committed
Change strategy for creating database
Use 'ensureDatabaseExists' flag on the migrate function. I _think_ this makes the API smaller/simpler. This implementation also checks whether the database exists before trying to create it, which means it _should_ work on readonly replicas. Fixes #57
1 parent 0785dd6 commit 5a73373

File tree

7 files changed

+164
-44
lines changed

7 files changed

+164
-44
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 5.3.0
4+
5+
- [DEPRECATION] Deprecate `createDb`
6+
- Add `ensureDatabaseExists` to check/create database in `migrate`
7+
38
## 5.1.0
49

510
- Validate migration ordering when loading files (instead of when applying migrations)

README.md

+19-29
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ There are two ways to use the API.
2727
Either, pass a database connection config object:
2828

2929
```typescript
30-
import {createDb, migrate} from "postgres-migrations"
30+
import {migrate} from "postgres-migrations"
3131

3232
async function() {
3333
const dbConfig = {
@@ -36,20 +36,24 @@ async function() {
3636
password: "password",
3737
host: "localhost",
3838
port: 5432,
39+
40+
// Default: false for backwards-compatibility
41+
// This might change!
42+
ensureDatabaseExists: true
43+
44+
// Default: "postgres"
45+
// Used when checking/creating "database-name"
46+
defaultDatabase: "postgres"
3947
}
4048

41-
await createDb(databaseName, {
42-
...dbConfig,
43-
defaultDatabase: "postgres", // defaults to "postgres"
44-
})
4549
await migrate(dbConfig, "path/to/migration/files")
4650
}
4751
```
4852

4953
Or, pass a `pg` client:
5054

5155
```typescript
52-
import {createDb, migrate} from "postgres-migrations"
56+
import {migrate} from "postgres-migrations"
5357

5458
async function() {
5559
const dbConfig = {
@@ -60,27 +64,13 @@ async function() {
6064
port: 5432,
6165
}
6266

63-
{
64-
const client = new pg.Client({
65-
...dbConfig,
66-
database: "postgres",
67-
})
68-
await client.connect()
69-
try {
70-
await createDb(databaseName, {client})
71-
} finally {
72-
await client.end()
73-
}
74-
}
75-
76-
{
77-
const client = new pg.Client(dbConfig) // or a Pool, or a PoolClient
78-
await client.connect()
79-
try {
80-
await migrate({client}, "path/to/migration/files")
81-
} finally {
82-
await client.end()
83-
}
67+
// Note: when passing a client, it is assumed that the database already exists
68+
const client = new pg.Client(dbConfig) // or a Pool, or a PoolClient
69+
await client.connect()
70+
try {
71+
await migrate({client}, "path/to/migration/files")
72+
} finally {
73+
await client.end()
8474
}
8575
}
8676
```
@@ -251,10 +241,10 @@ If you want sane date handling, it is recommended you use the following code sni
251241
```js
252242
const pg = require("pg")
253243

254-
const parseDate = val =>
244+
const parseDate = (val) =>
255245
val === null ? null : moment(val).format("YYYY-MM-DD")
256246
const DATATYPE_DATE = 1082
257-
pg.types.setTypeParser(DATATYPE_DATE, val => {
247+
pg.types.setTypeParser(DATATYPE_DATE, (val) => {
258248
return val === null ? null : parseDate(val)
259249
})
260250
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TABLE success (
2+
id integer
3+
);

src/__tests__/migrate.ts

+61-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import test from "ava"
33
import * as pg from "pg"
44
import SQL from "sql-template-strings"
5-
import {createDb, migrate} from "../"
5+
import {createDb, migrate, MigrateDBConfig} from "../"
66
import {PASSWORD, startPostgres, stopPostgres} from "./fixtures/docker-postgres"
77

88
const CONTAINER_NAME = "pg-migrations-test-migrate"
@@ -384,7 +384,7 @@ test("bad arguments - incorrect port", (t) => {
384384
})
385385
})
386386

387-
test("no database", (t) => {
387+
test("no database - ensureDatabaseExists = undefined", (t) => {
388388
return t
389389
.throwsAsync(
390390
migrate(
@@ -406,6 +406,65 @@ test("no database", (t) => {
406406
})
407407
})
408408

409+
test("no database - ensureDatabaseExists = true", (t) => {
410+
const databaseName = "migration-test-no-db-ensure-exists"
411+
const dbConfig: MigrateDBConfig = {
412+
database: databaseName,
413+
user: "postgres",
414+
password: PASSWORD,
415+
host: "localhost",
416+
port,
417+
418+
ensureDatabaseExists: true,
419+
}
420+
421+
return migrate(dbConfig, "src/__tests__/fixtures/ensure-exists")
422+
.then(() => doesTableExist(dbConfig, "success"))
423+
.then((exists) => {
424+
t.truthy(exists)
425+
})
426+
})
427+
428+
test("existing database - ensureDatabaseExists = true", (t) => {
429+
const databaseName = "migration-test-existing-db-ensure-exists"
430+
const dbConfig: MigrateDBConfig = {
431+
database: databaseName,
432+
user: "postgres",
433+
password: PASSWORD,
434+
host: "localhost",
435+
port,
436+
437+
ensureDatabaseExists: true,
438+
}
439+
440+
return createDb(databaseName, dbConfig)
441+
.then(() => migrate(dbConfig, "src/__tests__/fixtures/ensure-exists"))
442+
.then(() => doesTableExist(dbConfig, "success"))
443+
.then((exists) => {
444+
t.truthy(exists)
445+
})
446+
})
447+
448+
test("no database - ensureDatabaseExists = true, bad default database", (t) => {
449+
const databaseName = "migration-test-ensure-exists-nope"
450+
const dbConfig: MigrateDBConfig = {
451+
database: databaseName,
452+
user: "postgres",
453+
password: PASSWORD,
454+
host: "localhost",
455+
port,
456+
457+
ensureDatabaseExists: true,
458+
defaultDatabase: "nopenopenope",
459+
}
460+
461+
return t
462+
.throwsAsync(migrate(dbConfig, "src/__tests__/fixtures/ensure-exists"))
463+
.then((err) => {
464+
t.regex(err.message, /database "nopenopenope" does not exist/)
465+
})
466+
})
467+
409468
test("no migrations dir", (t) => {
410469
const databaseName = "migration-test-no-dir"
411470
const dbConfig = {

src/create.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {withConnection} from "./with-connection"
44

55
const DUPLICATE_DATABASE = "42P04"
66

7+
/**
8+
* @deprecated Use `migrate` instead with `ensureDatabaseExists: true`.
9+
*/
710
export async function createDb(
811
dbName: string,
912
dbConfig: CreateDBConfig,
@@ -25,7 +28,7 @@ export async function createDb(
2528
}
2629

2730
if ("client" in dbConfig) {
28-
return betterCreate(dbName, log)(dbConfig.client)
31+
return runCreateQuery(dbName, log)(dbConfig.client)
2932
}
3033

3134
if (
@@ -50,12 +53,12 @@ export async function createDb(
5053
log(`pg client emitted an error: ${err.message}`)
5154
})
5255

53-
const runWith = withConnection(log, betterCreate(dbName, log))
56+
const runWith = withConnection(log, runCreateQuery(dbName, log))
5457

5558
return runWith(client)
5659
}
5760

58-
function betterCreate(dbName: string, log: Logger) {
61+
export function runCreateQuery(dbName: string, log: Logger) {
5962
return async (client: BasicPgClient): Promise<void> => {
6063
await client
6164
.query(`CREATE DATABASE "${dbName.replace(/\"/g, '""')}"`)

src/migrate.ts

+49-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as pg from "pg"
22
import SQL from "sql-template-strings"
3+
import {runCreateQuery} from "./create"
34
import {loadMigrationFiles} from "./files-loader"
45
import {runMigration} from "./run-migration"
56
import {
@@ -14,6 +15,17 @@ import {validateMigrationHashes} from "./validation"
1415
import {withConnection} from "./with-connection"
1516
import {withAdvisoryLock} from "./with-lock"
1617

18+
/**
19+
* Run the migrations.
20+
*
21+
* If `dbConfig.ensureDatabaseExists` is true then `dbConfig.database` will be created if it
22+
* does not exist.
23+
*
24+
* @param dbConfig Details about how to connect to the database
25+
* @param migrationsDirectory Directory containing the SQL migration files
26+
* @param config Extra configuration
27+
* @returns Details about the migrations which were run
28+
*/
1729
export async function migrate(
1830
dbConfig: MigrateDBConfig,
1931
migrationsDirectory: string,
@@ -53,17 +65,45 @@ export async function migrate(
5365
throw new Error("Database config problem")
5466
}
5567

56-
const client = new pg.Client(dbConfig)
57-
client.on("error", (err) => {
58-
log(`pg client emitted an error: ${err.message}`)
59-
})
68+
if (dbConfig.ensureDatabaseExists === true) {
69+
// Check whether database exists
70+
const {user, password, host, port} = dbConfig
71+
const client = new pg.Client({
72+
database:
73+
dbConfig.defaultDatabase != null
74+
? dbConfig.defaultDatabase
75+
: "postgres",
76+
user,
77+
password,
78+
host,
79+
port,
80+
})
81+
82+
const runWith = withConnection(log, async (connectedClient) => {
83+
const result = await connectedClient.query({
84+
text: "SELECT 1 FROM pg_database WHERE datname=$1",
85+
values: [dbConfig.database],
86+
})
87+
if (result.rowCount !== 1) {
88+
await runCreateQuery(dbConfig.database, log)(connectedClient)
89+
}
90+
})
91+
92+
await runWith(client)
93+
}
94+
{
95+
const client = new pg.Client(dbConfig)
96+
client.on("error", (err) => {
97+
log(`pg client emitted an error: ${err.message}`)
98+
})
6099

61-
const runWith = withConnection(
62-
log,
63-
withAdvisoryLock(log, runMigrations(intendedMigrations, log)),
64-
)
100+
const runWith = withConnection(
101+
log,
102+
withAdvisoryLock(log, runMigrations(intendedMigrations, log)),
103+
)
65104

66-
return runWith(client)
105+
return runWith(client)
106+
}
67107
}
68108

69109
function runMigrations(intendedMigrations: Array<Migration>, log: Logger) {

src/types.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,26 @@ export interface ClientParams {
2121
readonly client: pg.Client | pg.PoolClient | pg.Pool
2222
}
2323

24+
export type EnsureDatabase =
25+
| {
26+
/**
27+
* Might default to `true` in future versions
28+
* @default false
29+
*/
30+
readonly ensureDatabaseExists: true
31+
/**
32+
* The database to connect to when creating a database (if necessary).
33+
* @default postgres
34+
*/
35+
readonly defaultDatabase?: string
36+
}
37+
| {
38+
readonly ensureDatabaseExists?: false
39+
}
40+
41+
/**
42+
* @deprecated Use `migrate` instead with `ensureDatabaseExists: true`.
43+
*/
2444
export type CreateDBConfig =
2545
| (ConnectionParams & {
2646
/** The database to connect to when creating the new database. */
@@ -31,7 +51,7 @@ export type CreateDBConfig =
3151
export type MigrateDBConfig =
3252
| (ConnectionParams & {
3353
readonly database: string
34-
})
54+
} & EnsureDatabase)
3555
| ClientParams
3656

3757
export type Logger = (msg: string) => void

0 commit comments

Comments
 (0)