You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq/scripts/migrate-pg-to-pg.ts

76 lines
3.3 KiB
TypeScript

// Go-live data correction: the earlier migrate-sqlite-to-pg.ts run pulled from local SQLite,
// which turned out to be stale/near-empty — the real working data was in a local Postgres
// instance instead. This wipes the TARGET (cloud) hq database's public schema and reloads it
// from the real SOURCE (local Postgres). Both engines are Postgres here, so every column type
// matches exactly — no conversion needed.
// SOURCE_DATABASE_URL and TARGET_DATABASE_URL must both be set explicitly, no defaults, so
// this can never accidentally point at (or wipe) the wrong database.
//
// Run from repo root:
// SOURCE_DATABASE_URL=postgres://hq:<local-pw>@127.0.0.1:5432/hq ^
// TARGET_DATABASE_URL=postgres://postgres:<cloud-pw>@13.232.53.115:5432/hq ^
// npx tsx apps/hq/scripts/migrate-pg-to-pg.ts
import { Pool } from 'pg'
import { openPgDb, type PgDb } from '../src/db-pg'
const SKIP_TABLES = new Set(['stg_client', 'stg_invoice', 'schema_migrations'])
async function main(): Promise<void> {
const sourceUrl = process.env['SOURCE_DATABASE_URL']
const targetUrl = process.env['TARGET_DATABASE_URL']
if (sourceUrl === undefined || sourceUrl === '') throw new Error('Set SOURCE_DATABASE_URL (local Postgres).')
if (targetUrl === undefined || targetUrl === '') throw new Error('Set TARGET_DATABASE_URL (cloud Postgres).')
if (sourceUrl === targetUrl) throw new Error('SOURCE and TARGET are identical — refusing.')
// Wipe the target's public schema completely, then let the app's own migrations rebuild
// it clean — guarantees the target schema matches this codebase exactly, not a stale copy.
const wipePool = new Pool({ connectionString: targetUrl, max: 1 })
await wipePool.query('DROP SCHEMA public CASCADE')
await wipePool.query('CREATE SCHEMA public')
await wipePool.end()
console.log('Target schema wiped.')
const target = (await openPgDb(targetUrl)) as PgDb // recreates schema via migrations, empty
const source = (await openPgDb(sourceUrl)) as PgDb // idempotent no-op if already migrated
const tables = await source.all<{ tablename: string }>(`SELECT tablename FROM pg_tables WHERE schemaname='public'`)
const client = await target.pool.connect()
try {
await client.query('BEGIN')
// Load in discovery order and ignore FK/trigger checks — safe: one transaction into a
// target we just wiped and confirmed empty.
await client.query('SET LOCAL session_replication_role = replica')
for (const { tablename: name } of tables) {
if (SKIP_TABLES.has(name)) {
console.log(`skip ${name}`)
continue
}
const rows = await source.all<Record<string, unknown>>(`SELECT * FROM ${name}`)
if (rows.length === 0) {
console.log(`${name}: 0 rows`)
continue
}
const cols = Object.keys(rows[0]!)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(',')
const insertSql = `INSERT INTO ${name} (${cols.join(',')}) VALUES (${placeholders})`
for (const row of rows) {
await client.query(insertSql, cols.map((c) => row[c]))
}
console.log(`${name}: ${rows.length} row(s) migrated`)
}
await client.query('COMMIT')
} catch (err) {
await client.query('ROLLBACK')
throw err
} finally {
client.release()
}
console.log('Migration complete.')
}
main().catch((e: unknown) => {
console.error(e)
process.exit(1)
})