/** * Import a dump produced by export-data.mjs into the current database. * Tables are truncated first (order-independent via session_replication_role) * and columns are cast back to their real types using information_schema. * * Usage (inside the api container): * node scripts/import-data.mjs */ import { PrismaClient, Prisma } from '@prisma/client'; import { readFileSync } from 'node:fs'; const prisma = new PrismaClient(); function toLiteral(value, udtName) { if (value === null) return Prisma.sql`NULL`; const target = Prisma.raw(`"${udtName}"`); switch (udtName) { case 'int2': case 'int4': case 'int8': return Prisma.sql`${BigInt(value)}::${target}`; case 'float4': case 'float8': case 'numeric': return Prisma.sql`${Number(value)}::${target}`; case 'bool': return Prisma.sql`${!!value}::${target}`; case 'timestamptz': case 'timestamp': return Prisma.sql`${new Date(value).toISOString()}::${target}`; case 'date': return Prisma.sql`${String(value)}::${target}`; case 'jsonb': case 'json': return Prisma.sql`${typeof value === 'string' ? value : JSON.stringify(value)}::${target}`; case 'bytea': return Prisma.sql`${Buffer.from(value, 'base64')}::bytea`; default: // text, varchar, enums and anything else: pass as text and cast return Prisma.sql`${String(value)}::${target}`; } } async function main() { const file = process.argv[2]; if (!file) { console.error('Usage: node scripts/import-data.mjs '); process.exit(1); } const dump = JSON.parse(readFileSync(file, 'utf8')); // Suspend FK checks during bulk load (postgres superuser not required for // session_replication_role in the compose postgres where app user owns db). await prisma.$executeRawUnsafe(`SET session_replication_role = replica`); const summary = {}; for (const [table, rows] of Object.entries(dump.tables)) { if (rows.length === 0) { summary[table] = 0; continue; } await prisma.$executeRawUnsafe(`TRUNCATE TABLE "${table}" CASCADE`); const colTypes = {}; const info = await prisma.$queryRawUnsafe( `SELECT column_name, udt_name FROM information_schema.columns WHERE table_name = '${table}'`, ); for (const c of info) colTypes[c.column_name] = c.udt_name; const columns = Object.keys(rows[0]); const colList = Prisma.raw(columns.map((c) => `"${c}"`).join(', ')); const CHUNK = 200; for (let i = 0; i < rows.length; i += CHUNK) { const tuples = rows.slice(i, i + CHUNK).map( (r) => Prisma.sql`(${Prisma.join( columns.map((c) => toLiteral(r[c], colTypes[c])), )})`, ); await prisma.$executeRaw( Prisma.sql`INSERT INTO ${Prisma.raw(`"${table}"`)} (${colList}) VALUES ${Prisma.join(tuples)}`, ); } // Keep sequences ahead of imported serial ids // Keep serial sequences ahead of imported ids const idCol = columns.find( (c) => c === 'id' || c === `${table.replace(/s$/, '')}_id`, ); if (idCol) { await prisma.$executeRawUnsafe( `SELECT setval(pg_get_serial_sequence('"${table}"', '${idCol}'), COALESCE((SELECT MAX("${idCol}") FROM "${table}"), 1))`, ); } summary[table] = rows.length; } await prisma.$executeRawUnsafe(`SET session_replication_role = DEFAULT`); console.log('Imported:', JSON.stringify(summary, null, 2)); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(() => prisma.$disconnect());