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.
77 lines
3.2 KiB
TypeScript
77 lines
3.2 KiB
TypeScript
// Go-live data carry-over: copies every row from the local SQLite dev database into the
|
|
// target (cloud) Postgres database, preserving values and ids as-is — this is a one-time
|
|
// full copy, not the "born in Postgres" empty-start path in docs/DEPLOY-HQ.md.
|
|
// Column types are a straight 1:1 map on this schema (money is bigint paise, flags stay
|
|
// integer, JSON payloads stay text — see migrations-pg.ts header), so no value conversion
|
|
// is needed; every column is copied through unchanged.
|
|
// Refuses to run if the target already has staff_user rows, so a re-run (or pointing at
|
|
// the wrong DATABASE_URL) can't silently duplicate or clobber data.
|
|
// stg_client/stg_invoice are APEX-import scratch tables, not real data — skipped.
|
|
//
|
|
// Run from repo root, source = local ./data/hq.db (or HQ_DATA_DIR), target = DATABASE_URL:
|
|
// DATABASE_URL=postgres://postgres:<pw>@<host>:5432/hq npx tsx apps/hq/scripts/migrate-sqlite-to-pg.ts
|
|
import { openDb, type SqliteDb } from '../src/db'
|
|
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 pgUrl = process.env['DATABASE_URL']
|
|
if (pgUrl === undefined || pgUrl === '') {
|
|
throw new Error('Set DATABASE_URL to the target (cloud) Postgres connection string.')
|
|
}
|
|
|
|
const sqlite = openDb(process.env['HQ_DATA_DIR']) as SqliteDb
|
|
const pg = (await openPgDb(pgUrl)) as PgDb // schema is created by migrations here; seedIfEmpty is NOT called
|
|
|
|
const existingStaff = (await pg.get<{ n: number }>(`SELECT COUNT(*) AS n FROM staff_user`))!.n
|
|
if (existingStaff > 0) {
|
|
throw new Error(
|
|
`Target already has ${existingStaff} staff_user row(s) — refusing to import over existing data. ` +
|
|
`Wrong DATABASE_URL, or this has already been migrated.`,
|
|
)
|
|
}
|
|
|
|
const tables = sqlite.raw
|
|
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`)
|
|
.all() as { name: string }[]
|
|
|
|
const client = await pg.pool.connect()
|
|
try {
|
|
await client.query('BEGIN')
|
|
// Load in table-discovery order and ignore FK/trigger checks — safe here because the
|
|
// whole load is one transaction into a database we just confirmed is empty.
|
|
await client.query('SET LOCAL session_replication_role = replica')
|
|
for (const { name } of tables) {
|
|
if (SKIP_TABLES.has(name)) {
|
|
console.log(`skip ${name}`)
|
|
continue
|
|
}
|
|
const rows = sqlite.raw.prepare(`SELECT * FROM ${name}`).all() as Record<string, unknown>[]
|
|
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)
|
|
})
|