From 6671de6f6e16d4813876363aa1818c142a6259e6 Mon Sep 17 00:00:00 2001 From: Daniel Grant <1670902+djgrant@users.noreply.github.com> Date: Mon, 29 Jun 2020 15:49:08 +0100 Subject: [PATCH 1/3] Implement custom migration table name --- README.md | 18 ++++++ src/__tests__/migrate.ts | 68 +++++++++++++++++--- src/__unit__/migration-file/index.ts | 8 +-- src/files-loader.ts | 15 ++++- src/load-sql-from-js.ts | 4 +- src/migrate.ts | 43 ++++++++----- src/migration-file.ts | 13 +++- src/migrations/0_create-migrations-table.js | 9 +++ src/migrations/0_create-migrations-table.sql | 6 -- src/types.ts | 1 + 10 files changed, 143 insertions(+), 42 deletions(-) create mode 100644 src/migrations/0_create-migrations-table.js delete mode 100644 src/migrations/0_create-migrations-table.sql diff --git a/README.md b/README.md index 754eb33..9a820f9 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,24 @@ async function() { } ``` +You can pass a custom migration table name: + +```typescript +await migrate(dbConfig, "path/to/migration/files", { + migrationTableName: "my_migrations", +}) +``` + +This could, alternatively, be a table in an existing schema: + +```typescript +await createDb(databaseName, {client}) +await client.query("CREATE SCHEMA IF NOT EXISTS my_schema") +await migrate(dbConfig, "path/to/migration/files", { + migrationTableName: "my_schema.migrations", +}) +``` + ## Design decisions ### No down migrations diff --git a/src/__tests__/migrate.ts b/src/__tests__/migrate.ts index 30357c4..a08804e 100644 --- a/src/__tests__/migrate.ts +++ b/src/__tests__/migrate.ts @@ -190,6 +190,57 @@ test("with pool client", async (t) => { } }) +test("with custom migration table name", async (t) => { + const databaseName = "migration-test-concurrent-no-tx" + const dbConfig = { + database: databaseName, + user: "postgres", + password: PASSWORD, + host: "localhost", + port, + } + + const migrateWithCustomMigrationTable = () => + migrate(dbConfig, "src/__tests__/fixtures/success-first", { + migrationTableName: "my_migrations", + }) + + await createDb(databaseName, dbConfig) + await migrateWithCustomMigrationTable() + + t.truthy(await doesTableExist(dbConfig, "my_migrations")) + t.truthy(await doesTableExist(dbConfig, "success")) + + await migrateWithCustomMigrationTable() +}) + +test("with custom migration table name in a custom schema", async (t) => { + const databaseName = "migration-test-concurrent-no-tx" + const dbConfig = { + database: databaseName, + user: "postgres", + password: PASSWORD, + host: "localhost", + port, + } + + const migrateWithCustomMigrationTable = () => + migrate(dbConfig, "src/__tests__/fixtures/success-first", { + migrationTableName: "my_schema.my_migrations", + }) + + const pool = new pg.Pool(dbConfig) + + await pool.query("CREATE SCHEMA my_schema") + await createDb(databaseName, dbConfig) + await migrateWithCustomMigrationTable() + + t.truthy(await doesTableExist(dbConfig, "my_schema.my_migrations")) + t.truthy(await doesTableExist(dbConfig, "success")) + + await migrateWithCustomMigrationTable() +}) + test("successful first migration", (t) => { const databaseName = "migration-test-success-first" const dbConfig = { @@ -636,12 +687,7 @@ function doesTableExist(dbConfig: pg.ClientConfig, tableName: string) { .connect() .then(() => client.query(SQL` - SELECT EXISTS ( - SELECT 1 - FROM pg_catalog.pg_class c - WHERE c.relname = ${tableName} - AND c.relkind = 'r' - ); + SELECT to_regclass(${tableName}) as matching_tables `), ) .then((result) => { @@ -649,15 +695,19 @@ function doesTableExist(dbConfig: pg.ClientConfig, tableName: string) { return client .end() .then(() => { - return result.rows.length > 0 && result.rows[0].exists + return ( + result.rows.length > 0 && result.rows[0].matching_tables !== null + ) }) .catch((error) => { console.log("Async error in 'doesTableExist", error) - return result.rows.length > 0 && result.rows[0].exists + return ( + result.rows.length > 0 && result.rows[0].matching_tables !== null + ) }) } catch (error) { console.log("Sync error in 'doesTableExist", error) - return result.rows.length > 0 && result.rows[0].exists + return result.rows.length > 0 && result.rows[0].matching_tables !== null } }) } diff --git a/src/__unit__/migration-file/index.ts b/src/__unit__/migration-file/index.ts index 6190966..249f966 100644 --- a/src/__unit__/migration-file/index.ts +++ b/src/__unit__/migration-file/index.ts @@ -3,8 +3,8 @@ import {load} from "../../migration-file" test("Hashes of JS files should be the same when the SQL is the same", async (t) => { const [js1, js2] = await Promise.all([ - load(__dirname + "/fixtures/different-js-same-sql-1/1_js.js"), - load(__dirname + "/fixtures/different-js-same-sql-2/1_js.js"), + load({filePath: __dirname + "/fixtures/different-js-same-sql-1/1_js.js"}), + load({filePath: __dirname + "/fixtures/different-js-same-sql-2/1_js.js"}), ]) t.is(js1.hash, js2.hash) @@ -12,8 +12,8 @@ test("Hashes of JS files should be the same when the SQL is the same", async (t) test("Hashes of JS files should be different when the SQL is different", async (t) => { const [js1, js2] = await Promise.all([ - load(__dirname + "/fixtures/same-js-different-sql-1/1_js.js"), - load(__dirname + "/fixtures/same-js-different-sql-2/1_js.js"), + load({filePath: __dirname + "/fixtures/same-js-different-sql-1/1_js.js"}), + load({filePath: __dirname + "/fixtures/same-js-different-sql-2/1_js.js"}), ]) t.not(js1.hash, js2.hash) diff --git a/src/files-loader.ts b/src/files-loader.ts index 67592f1..fb6123a 100644 --- a/src/files-loader.ts +++ b/src/files-loader.ts @@ -11,6 +11,7 @@ const isValidFile = (fileName: string) => /\.(sql|js)$/gi.test(fileName) export const load = async ( directory: string, log: Logger, + setupContext: {migrationTableName: string}, ): Promise> => { log(`Loading migrations from: ${directory}`) @@ -19,9 +20,17 @@ export const load = async ( if (fileNames != null) { const migrationFiles = [ - path.join(__dirname, "migrations/0_create-migrations-table.sql"), - ...fileNames.map((fileName) => path.resolve(directory, fileName)), - ].filter(isValidFile) + { + filePath: path.join( + __dirname, + "migrations/0_create-migrations-table.js", + ), + context: setupContext, + }, + ...fileNames.map((fileName) => ({ + filePath: path.resolve(directory, fileName), + })), + ].filter(({filePath}) => isValidFile(filePath)) const unorderedMigrations = await Promise.all( migrationFiles.map(loadMigrationFile), diff --git a/src/load-sql-from-js.ts b/src/load-sql-from-js.ts index 0ae03e4..b9b74cc 100644 --- a/src/load-sql-from-js.ts +++ b/src/load-sql-from-js.ts @@ -1,6 +1,6 @@ import * as path from "path" -export const loadSqlFromJs = (filePath: string): string => { +export const loadSqlFromJs = (filePath: string, context?: {}): string => { const migrationModule = require(filePath) if (!migrationModule.generateSql) { throw new Error(`Invalid javascript migration file: '${path.basename( @@ -8,7 +8,7 @@ export const loadSqlFromJs = (filePath: string): string => { )}'. It must to export a 'generateSql' function.`) } - const generatedValue = migrationModule.generateSql() + const generatedValue = migrationModule.generateSql(context) if (typeof generatedValue !== "string") { throw new Error(`Invalid javascript migration file: '${path.basename( filePath, diff --git a/src/migrate.ts b/src/migrate.ts index 414c99f..58e3f32 100644 --- a/src/migrate.ts +++ b/src/migrate.ts @@ -18,6 +18,11 @@ export async function migrate( migrationsDirectory: string, config: Config = {}, ): Promise> { + const migrationTableName = + typeof config.migrationTableName === "string" + ? config.migrationTableName + : "migrations" + const log = config.logger != null ? config.logger @@ -32,13 +37,16 @@ export async function migrate( if (typeof migrationsDirectory !== "string") { throw new Error("Must pass migrations directory as a string") } - const intendedMigrations = await load(migrationsDirectory, log) + + const intendedMigrations = await load(migrationsDirectory, log, { + migrationTableName, + }) if ("client" in dbConfig) { // we have been given a client to use, it should already be connected return withAdvisoryLock( log, - runMigrations(intendedMigrations, log), + runMigrations(intendedMigrations, log, migrationTableName), )(dbConfig.client) } @@ -59,17 +67,22 @@ export async function migrate( const runWith = withConnection( log, - withAdvisoryLock(log, runMigrations(intendedMigrations, log)), + withAdvisoryLock( + log, + runMigrations(intendedMigrations, log, migrationTableName), + ), ) return runWith(client) } -function runMigrations(intendedMigrations: Array, log: Logger) { +function runMigrations( + intendedMigrations: Array, + log: Logger, + migrationTableName: string, +) { return async (client: BasicPgClient) => { try { - const migrationTableName = "migrations" - log("Starting migrations") const appliedMigrations = await fetchAppliedMigrationFromDB( @@ -190,13 +203,13 @@ function logResult(completedMigrations: Array, log: Logger) { } /** Check whether table exists in postgres - http://stackoverflow.com/a/24089729 */ -async function doesTableExist(client: BasicPgClient, tableName: string) { - const result = await client.query(SQL`SELECT EXISTS ( - SELECT 1 - FROM pg_catalog.pg_class c - WHERE c.relname = ${tableName} - AND c.relkind = 'r' -);`) - - return result.rows.length > 0 && result.rows[0].exists +async function doesTableExist( + client: BasicPgClient, + migrationTableName: string, +) { + const result = await client.query(SQL` + SELECT to_regclass(${migrationTableName}) as matching_tables + `) + + return result.rows.length > 0 && result.rows[0].matching_tables !== null } diff --git a/src/migration-file.ts b/src/migration-file.ts index 8df62ff..ffc48ec 100644 --- a/src/migration-file.ts +++ b/src/migration-file.ts @@ -18,12 +18,13 @@ const getSqlStringLiteral = ( filePath: string, contents: string, type: "js" | "sql", + context?: {}, ) => { switch (type) { case "sql": return contents case "js": - return loadSqlFromJs(filePath) + return loadSqlFromJs(filePath, context) default: { const exhaustiveCheck: never = type return exhaustiveCheck @@ -31,13 +32,19 @@ const getSqlStringLiteral = ( } } -export const load = async (filePath: string) => { +export const load = async ({ + filePath, + context, +}: { + filePath: string + context?: {} +}) => { const fileName = getFileName(filePath) try { const {id, name, type} = parseFileName(fileName) const contents = await getFileContents(filePath) - const sql = getSqlStringLiteral(filePath, contents, type) + const sql = getSqlStringLiteral(filePath, contents, type, context) const hash = hashString(fileName + sql) return { diff --git a/src/migrations/0_create-migrations-table.js b/src/migrations/0_create-migrations-table.js new file mode 100644 index 0000000..0536c37 --- /dev/null +++ b/src/migrations/0_create-migrations-table.js @@ -0,0 +1,9 @@ +module.exports = { + generateSql: ({migrationTableName}) => ` + CREATE TABLE IF NOT EXISTS ${migrationTableName} ( + id integer PRIMARY KEY, + name varchar(100) UNIQUE NOT NULL, + hash varchar(40) NOT NULL, -- sha1 hex encoded hash of the file name and contents, to ensure it hasn't been altered since applying the migration + executed_at timestamp DEFAULT current_timestamp + )`, +} diff --git a/src/migrations/0_create-migrations-table.sql b/src/migrations/0_create-migrations-table.sql deleted file mode 100644 index 9974966..0000000 --- a/src/migrations/0_create-migrations-table.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS migrations ( - id integer PRIMARY KEY, - name varchar(100) UNIQUE NOT NULL, - hash varchar(40) NOT NULL, -- sha1 hex encoded hash of the file name and contents, to ensure it hasn't been altered since applying the migration - executed_at timestamp DEFAULT current_timestamp -); diff --git a/src/types.ts b/src/types.ts index f496bc4..681b499 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,6 +39,7 @@ export type Config = Partial export interface FullConfig { readonly logger: Logger + readonly migrationTableName: string } export class MigrationError extends Error { From 617a59fb49f8eb2d387b90d0e53e6811fc880c9e Mon Sep 17 00:00:00 2001 From: Daniel Grant <1670902+djgrant@users.noreply.github.com> Date: Mon, 29 Jun 2020 16:50:41 +0100 Subject: [PATCH 2/3] Fix tests --- package.json | 2 +- src/__tests__/migrate.ts | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 8f26564..91bf79a 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "check-formatting": "./node_modules/.bin/prettier '**/*.ts' --list-different", "fix-formatting": "./node_modules/.bin/prettier '**/*.ts' --write", "lint": "npm run tslint && npm run check-formatting", - "tslint": "tslint 'src/**/*.ts' --type-check --project tsconfig.json --format verbose", + "tslint": "tslint 'src/**/*.ts' --project tsconfig.json --format verbose", "test-integration": "ava --config ava.config.integration.cjs", "test-unit": "ava --config ava.config.unit.cjs", "test": "npm run test-unit && npm run lint && npm run test-integration", diff --git a/src/__tests__/migrate.ts b/src/__tests__/migrate.ts index a08804e..be2edf5 100644 --- a/src/__tests__/migrate.ts +++ b/src/__tests__/migrate.ts @@ -191,7 +191,7 @@ test("with pool client", async (t) => { }) test("with custom migration table name", async (t) => { - const databaseName = "migration-test-concurrent-no-tx" + const databaseName = "migration-test-custom-migration-table" const dbConfig = { database: databaseName, user: "postgres", @@ -215,7 +215,7 @@ test("with custom migration table name", async (t) => { }) test("with custom migration table name in a custom schema", async (t) => { - const databaseName = "migration-test-concurrent-no-tx" + const databaseName = "migration-test-custom-schema-custom-migration-table" const dbConfig = { database: databaseName, user: "postgres", @@ -231,14 +231,18 @@ test("with custom migration table name in a custom schema", async (t) => { const pool = new pg.Pool(dbConfig) - await pool.query("CREATE SCHEMA my_schema") - await createDb(databaseName, dbConfig) - await migrateWithCustomMigrationTable() + try { + await createDb(databaseName, dbConfig) + await pool.query("CREATE SCHEMA IF NOT EXISTS my_schema") + await migrateWithCustomMigrationTable() - t.truthy(await doesTableExist(dbConfig, "my_schema.my_migrations")) - t.truthy(await doesTableExist(dbConfig, "success")) + t.truthy(await doesTableExist(dbConfig, "my_schema.my_migrations")) + t.truthy(await doesTableExist(dbConfig, "success")) - await migrateWithCustomMigrationTable() + await migrateWithCustomMigrationTable() + } finally { + await pool.end() + } }) test("successful first migration", (t) => { From 21b531a394834e8d1bfbfe58888f548cf8db7514 Mon Sep 17 00:00:00 2001 From: Daniel Grant <1670902+djgrant@users.noreply.github.com> Date: Mon, 29 Jun 2020 19:49:16 +0100 Subject: [PATCH 3/3] Ensure initial migration hash does not change --- .../migrations/1_success.sql | 3 ++ .../success-existing-db/restore.sql.js | 11 +++++++ src/__tests__/migrate.ts | 25 ++++++++++++++++ src/__unit__/migration-file/index.ts | 8 ++--- src/files-loader.ts | 25 +++++++--------- src/initial-migration.ts | 29 +++++++++++++++++++ src/migrate.ts | 6 ++-- src/migration-file.ts | 12 ++------ src/migrations/0_create-migrations-table.js | 9 ------ 9 files changed, 90 insertions(+), 38 deletions(-) create mode 100644 src/__tests__/fixtures/success-existing-db/migrations/1_success.sql create mode 100644 src/__tests__/fixtures/success-existing-db/restore.sql.js create mode 100644 src/initial-migration.ts delete mode 100644 src/migrations/0_create-migrations-table.js diff --git a/src/__tests__/fixtures/success-existing-db/migrations/1_success.sql b/src/__tests__/fixtures/success-existing-db/migrations/1_success.sql new file mode 100644 index 0000000..598172f --- /dev/null +++ b/src/__tests__/fixtures/success-existing-db/migrations/1_success.sql @@ -0,0 +1,3 @@ +CREATE TABLE success ( + id integer +); diff --git a/src/__tests__/fixtures/success-existing-db/restore.sql.js b/src/__tests__/fixtures/success-existing-db/restore.sql.js new file mode 100644 index 0000000..90a438f --- /dev/null +++ b/src/__tests__/fixtures/success-existing-db/restore.sql.js @@ -0,0 +1,11 @@ +module.exports = ` +CREATE TABLE migrations ( + id integer PRIMARY KEY, + name character varying(100) NOT NULL UNIQUE, + hash character varying(40) NOT NULL, + executed_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO migrations ("id","name","hash","executed_at") +VALUES (0,E'create-migrations-table',E'e18db593bcde2aca2a408c4d1100f6abba2195df',E'2020-06-29 18:38:05.064546'); +` diff --git a/src/__tests__/migrate.ts b/src/__tests__/migrate.ts index be2edf5..c517360 100644 --- a/src/__tests__/migrate.ts +++ b/src/__tests__/migrate.ts @@ -339,6 +339,31 @@ test("successful complex js migration", (t) => { }) }) +test("successful migration on an existing database", async (t) => { + const databaseName = "migration-test-success-existing-db" + const dbConfig = { + database: databaseName, + user: "postgres", + password: PASSWORD, + host: "localhost", + port, + } + + const pool = new pg.Pool(dbConfig) + + try { + await createDb(databaseName, dbConfig) + await pool.query(require("./fixtures/success-existing-db/restore.sql")) + await migrate( + dbConfig, + "src/__tests__/fixtures/success-existing-db/migrations", + ) + t.truthy(await doesTableExist(dbConfig, "success")) + } finally { + await pool.end() + } +}) + test("bad arguments - no db config", (t) => { // tslint:disable-next-line no-any return t.throwsAsync((migrate as any)()).then((err) => { diff --git a/src/__unit__/migration-file/index.ts b/src/__unit__/migration-file/index.ts index 249f966..6190966 100644 --- a/src/__unit__/migration-file/index.ts +++ b/src/__unit__/migration-file/index.ts @@ -3,8 +3,8 @@ import {load} from "../../migration-file" test("Hashes of JS files should be the same when the SQL is the same", async (t) => { const [js1, js2] = await Promise.all([ - load({filePath: __dirname + "/fixtures/different-js-same-sql-1/1_js.js"}), - load({filePath: __dirname + "/fixtures/different-js-same-sql-2/1_js.js"}), + load(__dirname + "/fixtures/different-js-same-sql-1/1_js.js"), + load(__dirname + "/fixtures/different-js-same-sql-2/1_js.js"), ]) t.is(js1.hash, js2.hash) @@ -12,8 +12,8 @@ test("Hashes of JS files should be the same when the SQL is the same", async (t) test("Hashes of JS files should be different when the SQL is different", async (t) => { const [js1, js2] = await Promise.all([ - load({filePath: __dirname + "/fixtures/same-js-different-sql-1/1_js.js"}), - load({filePath: __dirname + "/fixtures/same-js-different-sql-2/1_js.js"}), + load(__dirname + "/fixtures/same-js-different-sql-1/1_js.js"), + load(__dirname + "/fixtures/same-js-different-sql-2/1_js.js"), ]) t.not(js1.hash, js2.hash) diff --git a/src/files-loader.ts b/src/files-loader.ts index fb6123a..8136502 100644 --- a/src/files-loader.ts +++ b/src/files-loader.ts @@ -2,6 +2,7 @@ import * as fs from "fs" import * as path from "path" import {promisify} from "util" import {load as loadMigrationFile} from "./migration-file" +import {loadInitialMigration} from "./initial-migration" import {Logger, Migration} from "./types" const readDir = promisify(fs.readdir) @@ -11,7 +12,7 @@ const isValidFile = (fileName: string) => /\.(sql|js)$/gi.test(fileName) export const load = async ( directory: string, log: Logger, - setupContext: {migrationTableName: string}, + migrationTableName: string, ): Promise> => { log(`Loading migrations from: ${directory}`) @@ -19,25 +20,21 @@ export const load = async ( log(`Found migration files: ${fileNames}`) if (fileNames != null) { - const migrationFiles = [ - { - filePath: path.join( - __dirname, - "migrations/0_create-migrations-table.js", - ), - context: setupContext, - }, - ...fileNames.map((fileName) => ({ - filePath: path.resolve(directory, fileName), - })), - ].filter(({filePath}) => isValidFile(filePath)) + const migrationFiles = fileNames + .map((fileName) => path.resolve(directory, fileName)) + .filter(isValidFile) const unorderedMigrations = await Promise.all( migrationFiles.map(loadMigrationFile), ) + const initialMigration = await loadInitialMigration(migrationTableName) + // Arrange in ID order - return unorderedMigrations.sort((a, b) => a.id - b.id) + return [ + initialMigration, + ...unorderedMigrations.sort((a, b) => a.id - b.id), + ] } return [] diff --git a/src/initial-migration.ts b/src/initial-migration.ts new file mode 100644 index 0000000..ba08075 --- /dev/null +++ b/src/initial-migration.ts @@ -0,0 +1,29 @@ +import {hashString} from "./migration-file" + +export const loadInitialMigration = async (migrationTableName: string) => { + // Since the hash of the initial migration is distributed across users' databases + // the values `fileName` and `sql` must NEVER change! + const fileName = "0_create-migrations-table.sql" + const sql = getInitialMigrationSql(migrationTableName) + const hash = hashString(fileName + sql) + + return { + id: 0, + name: "create-migrations-table", + contents: sql, + fileName, + hash, + sql, + } +} + +// Formatting must not change to ensure content hash remains the same +export const getInitialMigrationSql = ( + migrationTableName: string, +) => `CREATE TABLE IF NOT EXISTS ${migrationTableName} ( + id integer PRIMARY KEY, + name varchar(100) UNIQUE NOT NULL, + hash varchar(40) NOT NULL, -- sha1 hex encoded hash of the file name and contents, to ensure it hasn't been altered since applying the migration + executed_at timestamp DEFAULT current_timestamp +); +` diff --git a/src/migrate.ts b/src/migrate.ts index 58e3f32..54f7124 100644 --- a/src/migrate.ts +++ b/src/migrate.ts @@ -38,9 +38,11 @@ export async function migrate( throw new Error("Must pass migrations directory as a string") } - const intendedMigrations = await load(migrationsDirectory, log, { + const intendedMigrations = await load( + migrationsDirectory, + log, migrationTableName, - }) + ) if ("client" in dbConfig) { // we have been given a client to use, it should already be connected diff --git a/src/migration-file.ts b/src/migration-file.ts index ffc48ec..ef85a87 100644 --- a/src/migration-file.ts +++ b/src/migration-file.ts @@ -11,7 +11,7 @@ const getFileName = (filePath: string) => path.basename(filePath) const getFileContents = async (filePath: string) => readFile(filePath, "utf8") -const hashString = (s: string) => +export const hashString = (s: string) => crypto.createHash("sha1").update(s, "utf8").digest("hex") const getSqlStringLiteral = ( @@ -32,19 +32,13 @@ const getSqlStringLiteral = ( } } -export const load = async ({ - filePath, - context, -}: { - filePath: string - context?: {} -}) => { +export const load = async (filePath: string) => { const fileName = getFileName(filePath) try { const {id, name, type} = parseFileName(fileName) const contents = await getFileContents(filePath) - const sql = getSqlStringLiteral(filePath, contents, type, context) + const sql = getSqlStringLiteral(filePath, contents, type) const hash = hashString(fileName + sql) return { diff --git a/src/migrations/0_create-migrations-table.js b/src/migrations/0_create-migrations-table.js deleted file mode 100644 index 0536c37..0000000 --- a/src/migrations/0_create-migrations-table.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - generateSql: ({migrationTableName}) => ` - CREATE TABLE IF NOT EXISTS ${migrationTableName} ( - id integer PRIMARY KEY, - name varchar(100) UNIQUE NOT NULL, - hash varchar(40) NOT NULL, -- sha1 hex encoded hash of the file name and contents, to ensure it hasn't been altered since applying the migration - executed_at timestamp DEFAULT current_timestamp - )`, -}