chitfund/backend/scripts/migrate-pg-to-sqlite.js

196 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Copy all LuckyChit data from PostgreSQL → SQLite (local file).
*
* Prerequisites:
* 1. PostgreSQL reachable with your old data (set SOURCE_DATABASE_URL or DB_* below).
* 2. SQLite schema already exists: run `npm start` once with DB_DIALECT=sqlite so tables are created,
* or the script will exit with instructions.
*
* Usage (from backend/):
* node scripts/migrate-pg-to-sqlite.js
*
* Env (backend/.env):
* SOURCE_DATABASE_URL=postgresql://user:pass@localhost:5432/luckychit (recommended for migrate)
* Or set DATABASE_URL to your Postgres URL temporarily.
* SQLITE_STORAGE=./data/luckychit.sqlite (destination; default shown)
*
* If SOURCE_DATABASE_URL and DATABASE_URL are unset, builds URL from:
* DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
*/
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
const { Sequelize, QueryTypes } = require('sequelize');
const TABLES_ORDER = [
'users',
'chit_groups',
'group_members',
'payments',
'payment_audit_events',
'monthly_draws',
'notifications',
];
const TABLES_DELETE_REVERSE = [...TABLES_ORDER].reverse();
function getPostgresUrl() {
const u = process.env.SOURCE_DATABASE_URL;
if (u && String(u).startsWith('postgres')) return u;
const du = process.env.DATABASE_URL;
if (du && String(du).startsWith('postgres')) return du;
const host = process.env.DB_HOST;
const port = process.env.DB_PORT || 5432;
const name = process.env.DB_NAME;
const user = process.env.DB_USER;
const pass = process.env.DB_PASSWORD ?? '';
if (host && name && user !== undefined) {
const userEnc = encodeURIComponent(user);
const passEnc = encodeURIComponent(pass);
return `postgresql://${userEnc}:${passEnc}@${host}:${port}/${name}`;
}
throw new Error(
'Could not build PostgreSQL URL. Set SOURCE_DATABASE_URL=postgresql://... ' +
'or DATABASE_URL (postgres), or DB_HOST, DB_NAME, DB_USER, DB_PASSWORD.'
);
}
function getSqlitePath() {
const raw = process.env.SQLITE_STORAGE || path.join('data', 'luckychit.sqlite');
return path.isAbsolute(raw) ? raw : path.join(__dirname, '..', raw);
}
function serializeForSqlite(value) {
if (value === null || value === undefined) return null;
if (value instanceof Date) return value.toISOString();
if (typeof value === 'boolean') return value ? 1 : 0;
if (typeof value === 'bigint') return Number(value);
if (Buffer.isBuffer(value)) return value.toString('hex');
if (typeof value === 'object') return JSON.stringify(value);
return value;
}
async function tableExists(sqlite, name) {
const rows = await sqlite.query(
`SELECT name FROM sqlite_master WHERE type='table' AND name=:name`,
{ replacements: { name }, type: QueryTypes.SELECT }
);
return Array.isArray(rows) && rows.length > 0;
}
async function fetchPostgresTable(pg, table) {
try {
return await pg.query(`SELECT * FROM "${table}"`, { type: QueryTypes.SELECT });
} catch (e) {
if (e.parent && e.parent.code === '42P01') {
console.warn(` ⚠️ Postgres: table "${table}" does not exist — skipping.`);
return [];
}
throw e;
}
}
async function clearSqliteTable(sqlite, table) {
await sqlite.query(`DELETE FROM "${table}"`, { type: QueryTypes.DELETE });
}
async function insertRows(sqlite, table, rows) {
if (!rows.length) {
console.log(` ${table}: 0 rows`);
return;
}
const cols = Object.keys(rows[0]);
const colList = cols.map((c) => `"${c}"`).join(', ');
const placeholders = cols.map(() => '?').join(', ');
const sql = `INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`;
const t = await sqlite.transaction();
try {
for (const row of rows) {
const values = cols.map((c) => serializeForSqlite(row[c]));
await sqlite.query(sql, { replacements: values, transaction: t });
}
await t.commit();
console.log(` ${table}: ${rows.length} rows copied`);
} catch (e) {
await t.rollback();
throw e;
}
}
async function main() {
console.log('🔄 PostgreSQL → SQLite migration\n');
const pgUrl = getPostgresUrl();
const sqlitePath = getSqlitePath();
console.log(` Source (Postgres): ${pgUrl.replace(/:[^:@]+@/, ':****@')}`);
console.log(` Dest (SQLite): ${sqlitePath}\n`);
const pg = new Sequelize(pgUrl, {
dialect: 'postgres',
logging: false,
pool: { max: 5, min: 0, idle: 10000 },
});
const sqlite = new Sequelize({
dialect: 'sqlite',
storage: sqlitePath,
logging: false,
define: { underscored: true, freezeTableName: true },
});
await pg.authenticate();
console.log('✅ Connected to PostgreSQL');
await sqlite.authenticate();
console.log('✅ Connected to SQLite');
for (const t of TABLES_ORDER) {
if (!(await tableExists(sqlite, t))) {
console.error(
`\n❌ SQLite table "${t}" is missing. Create schema first:\n` +
` Set DB_DIALECT=sqlite in backend/.env, then run: npm start\n` +
` (or ensure Sequelize sync has created tables.)\n`
);
await pg.close();
await sqlite.close();
process.exit(1);
}
}
console.log('\n🗑 Clearing SQLite tables (child → parent order)…');
await sqlite.transaction(async (transaction) => {
await sqlite.query('PRAGMA foreign_keys = OFF', { transaction });
for (const table of TABLES_DELETE_REVERSE) {
await sqlite.query(`DELETE FROM "${table}"`, { type: QueryTypes.DELETE, transaction });
}
await sqlite.query('PRAGMA foreign_keys = ON', { transaction });
});
console.log(' Done.\n');
console.log('📥 Copying from PostgreSQL → SQLite…\n');
for (const table of TABLES_ORDER) {
const rows = await fetchPostgresTable(pg, table);
await insertRows(sqlite, table, rows);
}
await pg.close();
await sqlite.close();
console.log('\n✅ Migration finished. Point DB_DIALECT=sqlite at this file and restart the API.');
}
main().catch((err) => {
console.error('\n❌ Migration failed:', err.message);
if (err.parent) console.error(err.parent.message);
process.exit(1);
});