Skip to content

Commit

Permalink
Change strategy for creating database
Browse files Browse the repository at this point in the history
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 ThomWright#57
  • Loading branch information
ThomWright committed Jul 20, 2021
1 parent 0785dd6 commit 5a73373
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 44 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 5.3.0

- [DEPRECATION] Deprecate `createDb`
- Add `ensureDatabaseExists` to check/create database in `migrate`

## 5.1.0

- Validate migration ordering when loading files (instead of when applying migrations)
Expand Down
48 changes: 19 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ There are two ways to use the API.
Either, pass a database connection config object:

```typescript
import {createDb, migrate} from "postgres-migrations"
import {migrate} from "postgres-migrations"

async function() {
const dbConfig = {
Expand All @@ -36,20 +36,24 @@ async function() {
password: "password",
host: "localhost",
port: 5432,

// Default: false for backwards-compatibility
// This might change!
ensureDatabaseExists: true

// Default: "postgres"
// Used when checking/creating "database-name"
defaultDatabase: "postgres"
}

await createDb(databaseName, {
...dbConfig,
defaultDatabase: "postgres", // defaults to "postgres"
})
await migrate(dbConfig, "path/to/migration/files")
}
```

Or, pass a `pg` client:

```typescript
import {createDb, migrate} from "postgres-migrations"
import {migrate} from "postgres-migrations"

async function() {
const dbConfig = {
Expand All @@ -60,27 +64,13 @@ async function() {
port: 5432,
}

{
const client = new pg.Client({
...dbConfig,
database: "postgres",
})
await client.connect()
try {
await createDb(databaseName, {client})
} finally {
await client.end()
}
}

{
const client = new pg.Client(dbConfig) // or a Pool, or a PoolClient
await client.connect()
try {
await migrate({client}, "path/to/migration/files")
} finally {
await client.end()
}
// Note: when passing a client, it is assumed that the database already exists
const client = new pg.Client(dbConfig) // or a Pool, or a PoolClient
await client.connect()
try {
await migrate({client}, "path/to/migration/files")
} finally {
await client.end()
}
}
```
Expand Down Expand Up @@ -251,10 +241,10 @@ If you want sane date handling, it is recommended you use the following code sni
```js
const pg = require("pg")

const parseDate = val =>
const parseDate = (val) =>
val === null ? null : moment(val).format("YYYY-MM-DD")
const DATATYPE_DATE = 1082
pg.types.setTypeParser(DATATYPE_DATE, val => {
pg.types.setTypeParser(DATATYPE_DATE, (val) => {
return val === null ? null : parseDate(val)
})
```
Expand Down
3 changes: 3 additions & 0 deletions src/__tests__/fixtures/ensure-exists/1_success.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE TABLE success (
id integer
);
63 changes: 61 additions & 2 deletions src/__tests__/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import test from "ava"
import * as pg from "pg"
import SQL from "sql-template-strings"
import {createDb, migrate} from "../"
import {createDb, migrate, MigrateDBConfig} from "../"
import {PASSWORD, startPostgres, stopPostgres} from "./fixtures/docker-postgres"

const CONTAINER_NAME = "pg-migrations-test-migrate"
Expand Down Expand Up @@ -384,7 +384,7 @@ test("bad arguments - incorrect port", (t) => {
})
})

test("no database", (t) => {
test("no database - ensureDatabaseExists = undefined", (t) => {
return t
.throwsAsync(
migrate(
Expand All @@ -406,6 +406,65 @@ test("no database", (t) => {
})
})

test("no database - ensureDatabaseExists = true", (t) => {
const databaseName = "migration-test-no-db-ensure-exists"
const dbConfig: MigrateDBConfig = {
database: databaseName,
user: "postgres",
password: PASSWORD,
host: "localhost",
port,

ensureDatabaseExists: true,
}

return migrate(dbConfig, "src/__tests__/fixtures/ensure-exists")
.then(() => doesTableExist(dbConfig, "success"))
.then((exists) => {
t.truthy(exists)
})
})

test("existing database - ensureDatabaseExists = true", (t) => {
const databaseName = "migration-test-existing-db-ensure-exists"
const dbConfig: MigrateDBConfig = {
database: databaseName,
user: "postgres",
password: PASSWORD,
host: "localhost",
port,

ensureDatabaseExists: true,
}

return createDb(databaseName, dbConfig)
.then(() => migrate(dbConfig, "src/__tests__/fixtures/ensure-exists"))
.then(() => doesTableExist(dbConfig, "success"))
.then((exists) => {
t.truthy(exists)
})
})

test("no database - ensureDatabaseExists = true, bad default database", (t) => {
const databaseName = "migration-test-ensure-exists-nope"
const dbConfig: MigrateDBConfig = {
database: databaseName,
user: "postgres",
password: PASSWORD,
host: "localhost",
port,

ensureDatabaseExists: true,
defaultDatabase: "nopenopenope",
}

return t
.throwsAsync(migrate(dbConfig, "src/__tests__/fixtures/ensure-exists"))
.then((err) => {
t.regex(err.message, /database "nopenopenope" does not exist/)
})
})

test("no migrations dir", (t) => {
const databaseName = "migration-test-no-dir"
const dbConfig = {
Expand Down
9 changes: 6 additions & 3 deletions src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {withConnection} from "./with-connection"

const DUPLICATE_DATABASE = "42P04"

/**
* @deprecated Use `migrate` instead with `ensureDatabaseExists: true`.
*/
export async function createDb(
dbName: string,
dbConfig: CreateDBConfig,
Expand All @@ -25,7 +28,7 @@ export async function createDb(
}

if ("client" in dbConfig) {
return betterCreate(dbName, log)(dbConfig.client)
return runCreateQuery(dbName, log)(dbConfig.client)
}

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

const runWith = withConnection(log, betterCreate(dbName, log))
const runWith = withConnection(log, runCreateQuery(dbName, log))

return runWith(client)
}

function betterCreate(dbName: string, log: Logger) {
export function runCreateQuery(dbName: string, log: Logger) {
return async (client: BasicPgClient): Promise<void> => {
await client
.query(`CREATE DATABASE "${dbName.replace(/\"/g, '""')}"`)
Expand Down
58 changes: 49 additions & 9 deletions src/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as pg from "pg"
import SQL from "sql-template-strings"
import {runCreateQuery} from "./create"
import {loadMigrationFiles} from "./files-loader"
import {runMigration} from "./run-migration"
import {
Expand All @@ -14,6 +15,17 @@ import {validateMigrationHashes} from "./validation"
import {withConnection} from "./with-connection"
import {withAdvisoryLock} from "./with-lock"

/**
* Run the migrations.
*
* If `dbConfig.ensureDatabaseExists` is true then `dbConfig.database` will be created if it
* does not exist.
*
* @param dbConfig Details about how to connect to the database
* @param migrationsDirectory Directory containing the SQL migration files
* @param config Extra configuration
* @returns Details about the migrations which were run
*/
export async function migrate(
dbConfig: MigrateDBConfig,
migrationsDirectory: string,
Expand Down Expand Up @@ -53,17 +65,45 @@ export async function migrate(
throw new Error("Database config problem")
}

const client = new pg.Client(dbConfig)
client.on("error", (err) => {
log(`pg client emitted an error: ${err.message}`)
})
if (dbConfig.ensureDatabaseExists === true) {
// Check whether database exists
const {user, password, host, port} = dbConfig
const client = new pg.Client({
database:
dbConfig.defaultDatabase != null
? dbConfig.defaultDatabase
: "postgres",
user,
password,
host,
port,
})

const runWith = withConnection(log, async (connectedClient) => {
const result = await connectedClient.query({
text: "SELECT 1 FROM pg_database WHERE datname=$1",
values: [dbConfig.database],
})
if (result.rowCount !== 1) {
await runCreateQuery(dbConfig.database, log)(connectedClient)
}
})

await runWith(client)
}
{
const client = new pg.Client(dbConfig)
client.on("error", (err) => {
log(`pg client emitted an error: ${err.message}`)
})

const runWith = withConnection(
log,
withAdvisoryLock(log, runMigrations(intendedMigrations, log)),
)
const runWith = withConnection(
log,
withAdvisoryLock(log, runMigrations(intendedMigrations, log)),
)

return runWith(client)
return runWith(client)
}
}

function runMigrations(intendedMigrations: Array<Migration>, log: Logger) {
Expand Down
22 changes: 21 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ export interface ClientParams {
readonly client: pg.Client | pg.PoolClient | pg.Pool
}

export type EnsureDatabase =
| {
/**
* Might default to `true` in future versions
* @default false
*/
readonly ensureDatabaseExists: true
/**
* The database to connect to when creating a database (if necessary).
* @default postgres
*/
readonly defaultDatabase?: string
}
| {
readonly ensureDatabaseExists?: false
}

/**
* @deprecated Use `migrate` instead with `ensureDatabaseExists: true`.
*/
export type CreateDBConfig =
| (ConnectionParams & {
/** The database to connect to when creating the new database. */
Expand All @@ -31,7 +51,7 @@ export type CreateDBConfig =
export type MigrateDBConfig =
| (ConnectionParams & {
readonly database: string
})
} & EnsureDatabase)
| ClientParams

export type Logger = (msg: string) => void
Expand Down

0 comments on commit 5a73373

Please sign in to comment.