import type { AppliedMigration, AvailableMigration, Migrator, } from "../types/Migrator.js"; import { init, migrations } from "./migrations/index.js"; import { pgToDate } from "./cast.js"; import { WrappedPool } from "./pool.js"; import { sql, toPositional } from "./sql.js"; export class PostgresMigrator implements Migrator { #pool: WrappedPool; constructor(pool: WrappedPool) { this.#pool = pool; } async #listAppliedMigrations(): Promise< { id: number; name: string; applied_at: string }[] | null > { const client = await this.#pool._pool.connect(); try { const existsResult = await client.query<{ exists: boolean }>( `SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '_migrations' ) AS "exists"` ); if (!existsResult.rows[0]?.exists) return null; const result = await client.query<{ id: number; name: string; applied_at: Date; }>( `SELECT "id", "name", "applied_at" FROM "_migrations" ORDER BY "id" ASC` ); return result.rows.map((x) => ({ ...x, applied_at: pgToDate(x.applied_at), })); } finally { client.release(); } } async listAppliedMigrations(): Promise { return (await this.#listAppliedMigrations()) ?? []; } listAvailableMigrations(): AvailableMigration[] { return migrations.map((m) => ({ id: m.id, name: m.name })); } async migrateTo(id: number): Promise { const target = migrations.find((x) => x.id === id); if (!target) throw new Error(`No known migration with id ${id}`); let current = await this.#listAppliedMigrations(); if (!current) { const client = await this.#pool._pool.connect(); try { await client.query(init); } finally { client.release(); } current = []; } for (const { id, name } of current) { const migration = migrations.find((x) => x.id === id); if (!migration) throw new Error(`Applied migration ${id} (${name}) not known`); if (migration.name !== name) throw new Error( `Applied migration ${id} (${name}) has a different name from expected (${migration.name})` ); } const start = migrations.findIndex((x) => x.id === current.at(-1)?.id) + 1; const end = migrations.indexOf(target) + 1; if (end < start) { throw new Error( `Cannot migrate backward, at ${current.at(-1)?.id ?? 0}, going to ${target.id}` ); } const toApply = migrations.slice(start, end); if (!toApply.length) { console.log("Nothing to do"); return; } for (const migration of toApply) { console.log(`Applying migration ${migration.id} (${migration.name})`); const client = await this.#pool._pool.connect(); try { await client.query("BEGIN"); for (const part of migration.parts) { console.log(`- Applying part ${part.id} (${part.name})`); if ("sql" in part) { await client.query(part.sql); } else { await part.apply(this.#pool); } } const recordSql = sql` INSERT INTO "_migrations"("id", "name") VALUES (${{ int: migration.id }}, ${{ text: migration.name }}) `; await client.query(toPositional(recordSql._sql), recordSql._vars); await client.query("COMMIT"); } catch (e) { await client.query("ROLLBACK"); throw e; } finally { client.release(); } } console.log("Done migrating database"); } }