-
Notifications
You must be signed in to change notification settings - Fork 313
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(generator): generate latest migration via pgdump
- Loading branch information
1 parent
a0a8b93
commit 011fe6d
Showing
8 changed files
with
35,786 additions
and
16,467 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { PostgreSqlContainer } from '@testcontainers/postgresql'; | ||
import { spawn } from 'node:child_process'; | ||
import { writeFileSync } from 'node:fs'; | ||
import { resolve } from 'node:path'; | ||
import { Pool } from 'pg'; | ||
import { migrate } from '../../server/src/migrations/migrations'; | ||
import { FileBuilder } from './filebuilder'; | ||
|
||
const SCHEMA_DIR = resolve(__dirname, '../../server/src/migrations/schema'); | ||
|
||
process.on('SIGINT', () => { | ||
console.log('Gracefully quitting process...'); | ||
// @ts-expect-error Not a public method | ||
const activeHandles = process._getActiveHandles() as any[]; | ||
if (activeHandles.length) { | ||
console.log('Active handles:', activeHandles); | ||
} | ||
process.exit(1); | ||
}); | ||
|
||
async function main(): Promise<void> { | ||
// Start clean database... | ||
console.info('Starting Postgres container...'); | ||
const container = await new PostgreSqlContainer().start(); | ||
const pool = new Pool({ connectionString: container.getConnectionUri() }); | ||
const client = await pool.connect(); | ||
console.info('Connected to pool... Running migrations...'); | ||
|
||
// Run migrations | ||
await migrate(client, true); | ||
|
||
// Dump resulting schema | ||
const { stdout } = spawn( | ||
'pg_dump', | ||
[ | ||
'-h', | ||
container.getHost(), | ||
'-p', | ||
container.getPort().toString(), | ||
'-U', | ||
container.getUsername(), | ||
'--schema-only', | ||
'--no-owner', | ||
container.getDatabase(), | ||
], | ||
{ | ||
stdio: ['ignore', 'pipe', 'pipe'], | ||
env: { ...process.env, PGPASSWORD: 'test' }, | ||
} | ||
); | ||
|
||
let dump = ''; | ||
stdout.setEncoding('utf-8'); | ||
stdout.on('data', (data) => { | ||
dump += data; | ||
}); | ||
|
||
await new Promise<void>((resolve) => { | ||
stdout.on('close', () => { | ||
resolve(); | ||
}); | ||
}); | ||
|
||
console.info('Database dump successful. Writing dump to file...'); | ||
|
||
const builder = new FileBuilder(); | ||
buildLatestMigration(builder, dump); | ||
writeFileSync(`${SCHEMA_DIR}/latest.ts`, builder.toString(), 'utf8'); | ||
|
||
console.info('Migration file successfully created.'); | ||
|
||
client.release(); | ||
await pool.end(); | ||
await container.stop(); | ||
} | ||
|
||
function buildLatestMigration(b: FileBuilder, sqlDump: string): void { | ||
b.append("import { PoolClient } from 'pg';"); | ||
b.newLine(); | ||
b.append('export async function run(client: PoolClient): Promise<void> {'); | ||
b.indentCount++; | ||
|
||
b.newLine(); | ||
b.appendNoWrap('await client.query(`'); | ||
b.indentCount--; | ||
|
||
b.appendNoWrap(sqlDump); | ||
|
||
b.append('`);'); | ||
b.append('}'); | ||
} | ||
|
||
main().catch(console.error); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { normalizeErrorString, sleep } from '@medplum/core'; | ||
import { PoolClient } from 'pg'; | ||
import { globalLogger } from '../logger'; | ||
import * as migrations from './schema/index'; | ||
|
||
export async function migrate(client: PoolClient, forceAllSteps = false): Promise<void> { | ||
let version: number; | ||
try { | ||
const result = await client.query<{ id: number; version: number; dataVersion: number }>( | ||
'SELECT "version" FROM "DatabaseMigration"' | ||
); | ||
version = result.rows[0]?.version ?? 0; // First version starts at v1 | ||
globalLogger.info(`Current schema version: v${version}`); | ||
} catch (err) { | ||
globalLogger.debug(`Error during version query: ${normalizeErrorString(err)}`); | ||
globalLogger.info('Database not initialized. Initializing...'); | ||
version = 0; | ||
} | ||
|
||
const migrationKeys = Object.keys(migrations).filter((key) => key.startsWith('v')); | ||
const migrationVersions = migrationKeys.map((key) => Number.parseInt(key.slice(1), 10)).sort((a, b) => a - b); | ||
|
||
if (version === 0 && !forceAllSteps) { | ||
const start = Date.now(); | ||
await migrations.latest.run(client); | ||
const latestVersion = migrationVersions[migrationVersions.length - 1]; | ||
globalLogger.info('Database schema migration', { | ||
version: `v${latestVersion}`, | ||
duration: `${Date.now() - start} ms`, | ||
}); | ||
for (let i = 0; i < 5; i++) { | ||
await sleep(1000); | ||
try { | ||
await client.query('INSERT INTO "DatabaseMigration" ("id", "version", "dataVersion") VALUES (1, $1, 0)', [ | ||
latestVersion, | ||
]); | ||
break; | ||
} catch (_err) { | ||
console.log(`Retrying... Attempt #${i + 1}`); | ||
} | ||
} | ||
} else { | ||
if (version === 0) { | ||
await client.query(`CREATE TABLE IF NOT EXISTS "DatabaseMigration" ( | ||
"id" INTEGER NOT NULL PRIMARY KEY, | ||
"version" INTEGER NOT NULL, | ||
"dataVersion" INTEGER NOT NULL | ||
)`); | ||
await client.query('INSERT INTO "DatabaseMigration" ("id", "version", "dataVersion") VALUES (1, 0, 0)'); | ||
} | ||
for (let i = version + 1; i <= migrationKeys.length; i++) { | ||
const migration = (migrations as Record<string, migrations.Migration>)[`v${i}`]; | ||
if (migration) { | ||
const start = Date.now(); | ||
await migration.run(client); | ||
globalLogger.info('Database schema migration', { version: `v${i}`, duration: `${Date.now() - start} ms` }); | ||
await client.query('UPDATE "DatabaseMigration" SET "version"=$1', [i]); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.