57 lines
2.9 KiB
JavaScript
57 lines
2.9 KiB
JavaScript
|
|
// One-shot data migration: copy every row from the SQLite data.db into Postgres. Run ONCE at cutover,
|
||
|
|
// with the app stopped, BEFORE switching DB_BACKEND to pg.
|
||
|
|
//
|
||
|
|
// DB_PATH=/data/data.db DATABASE_URL=postgres://user:pass@host/db node db/migrate-sqlite-to-pg.js
|
||
|
|
//
|
||
|
|
// It applies the Postgres schema first, TRUNCATEs the target tables (so a re-run re-copies cleanly), then
|
||
|
|
// bulk-inserts in FK-dependency order. audit_log.id is a GENERATED identity, so its id is not copied (PG
|
||
|
|
// assigns fresh ones — nothing references audit_log.id). Timestamps/flags are plain integers on both sides.
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
const { DatabaseSync } = require('node:sqlite');
|
||
|
|
const { Pool } = require('pg');
|
||
|
|
|
||
|
|
const SQLITE = process.env.DB_PATH || path.join(__dirname, '..', 'data.db');
|
||
|
|
if (!process.env.DATABASE_URL) { console.error('DATABASE_URL is required'); process.exit(1); }
|
||
|
|
|
||
|
|
const src = new DatabaseSync(SQLITE);
|
||
|
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 4 });
|
||
|
|
|
||
|
|
// Parents before children (users→teams, sessions_auth→users, machines→teams); the rest have no FKs.
|
||
|
|
const ORDER = [
|
||
|
|
'teams', 'users', 'machines', 'sessions_auth', 'audit_log', 'sessions_log', 'refresh_tokens',
|
||
|
|
'api_keys', 'webhooks', 'messages', 'message_reactions', 'attachments', 'conversations',
|
||
|
|
'conversation_members', 'call_history', 'user_aliases', 'polls', 'poll_votes', 'scheduled_meetings',
|
||
|
|
'recordings', 'push_subscriptions', 'device_tokens', 'app_installs', 'favorites',
|
||
|
|
];
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
await pool.query(fs.readFileSync(path.join(__dirname, 'schema.pg.sql'), 'utf8')); // ensure schema exists
|
||
|
|
await pool.query('TRUNCATE ' + ORDER.map((t) => '"' + t + '"').join(', ') + ' RESTART IDENTITY CASCADE');
|
||
|
|
|
||
|
|
const totals = {};
|
||
|
|
for (const table of ORDER) {
|
||
|
|
let rows = [];
|
||
|
|
try { rows = src.prepare('SELECT * FROM ' + table).all(); } catch (e) { totals[table] = 'skip(' + e.message + ')'; continue; }
|
||
|
|
if (!rows.length) { totals[table] = 0; continue; }
|
||
|
|
let cols = Object.keys(rows[0]);
|
||
|
|
if (table === 'audit_log') cols = cols.filter((c) => c !== 'id'); // GENERATED — let PG assign
|
||
|
|
const colList = cols.map((c) => '"' + c + '"').join(',');
|
||
|
|
const CHUNK = 400; // keep param count well under Postgres' 65535 limit even for wide tables
|
||
|
|
for (let i = 0; i < rows.length; i += CHUNK) {
|
||
|
|
const batch = rows.slice(i, i + CHUNK);
|
||
|
|
const values = []; const params = [];
|
||
|
|
batch.forEach((r, ri) => {
|
||
|
|
values.push('(' + cols.map((c, ci) => '$' + (ri * cols.length + ci + 1)).join(',') + ')');
|
||
|
|
cols.forEach((c) => params.push(r[c] === undefined ? null : r[c]));
|
||
|
|
});
|
||
|
|
await pool.query('INSERT INTO "' + table + '" (' + colList + ') VALUES ' + values.join(','), params);
|
||
|
|
}
|
||
|
|
totals[table] = rows.length;
|
||
|
|
}
|
||
|
|
console.log('MIGRATED rows:', JSON.stringify(totals, null, 0));
|
||
|
|
await pool.end();
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((e) => { console.error('MIGRATION FAILED:', e && e.message); process.exit(1); });
|