/** * One-time migration: MariaDB → PostgreSQL * * Reads all data from the existing MariaDB database and inserts into PostgreSQL. * Requires both DATABASE_URL (postgres) and MARIA_* env vars. * * Usage: node --env-file=.env scripts/migrate-data.ts */ import mysql from "mysql2/promise"; import postgres from "postgres"; const MARIA_HOST = process.env.MARIA_HOST ?? "127.0.0.1"; const MARIA_PORT = Number(process.env.MARIA_PORT ?? 3306); const MARIA_USER = process.env.MARIA_USER ?? "serve"; const MARIA_PASS = process.env.MARIA_PASS ?? ""; const MARIA_DB = process.env.MARIA_DB ?? "serve"; const PG_URL = process.env.DATABASE_URL; if (!PG_URL) { console.error("DATABASE_URL is required"); process.exit(1); } const maria = await mysql.createConnection({ host: MARIA_HOST, port: MARIA_PORT, user: MARIA_USER, password: MARIA_PASS, database: MARIA_DB, timezone: "+00:00", }); const pgUrl = new URL(PG_URL); const socketHost = pgUrl.searchParams.get("host"); const pg = postgres({ host: socketHost ?? pgUrl.hostname, port: Number(pgUrl.port) || 5432, database: pgUrl.pathname.slice(1), username: pgUrl.username, password: pgUrl.password, }); // Order: rounds → round_users → movies → votes → round_history (FK constraint order) console.log("Migrating rounds..."); const [roundRows] = await maria.execute("SELECT * FROM rounds"); for (const r of roundRows as mysql.RowDataPacket[]) { await pg` INSERT INTO rounds (uuid, phase, setup_done, created_at, updated_at) VALUES (${r.uuid}, ${r.phase}, ${Boolean(r.setup_done)}, ${r.created_at}, ${r.updated_at}) ON CONFLICT (uuid) DO NOTHING `; } console.log(` ${(roundRows as mysql.RowDataPacket[]).length} rounds`); console.log("Migrating round_users..."); const [userRows] = await maria.execute("SELECT * FROM round_users"); for (const u of userRows as mysql.RowDataPacket[]) { await pg` INSERT INTO round_users (round_uuid, name, done_phase1, done_phase2, sort_order) VALUES (${u.round_uuid}, ${u.name}, ${Boolean(u.done_phase1)}, ${Boolean(u.done_phase2)}, ${u.sort_order}) ON CONFLICT (round_uuid, name) DO NOTHING `; } console.log(` ${(userRows as mysql.RowDataPacket[]).length} users`); console.log("Migrating movies..."); const [movieRows] = await maria.execute("SELECT * FROM movies ORDER BY id"); for (const m of movieRows as mysql.RowDataPacket[]) { await pg` INSERT INTO movies (id, round_uuid, title, added_by, added_at) VALUES (${m.id}, ${m.round_uuid}, ${m.title}, ${m.added_by}, ${m.added_at}) ON CONFLICT DO NOTHING `; } console.log(` ${(movieRows as mysql.RowDataPacket[]).length} movies`); // Reset the serial sequence for movies.id await pg`SELECT setval('movies_id_seq', COALESCE((SELECT MAX(id) FROM movies), 0) + 1)`; console.log("Migrating votes..."); const [voteRows] = await maria.execute("SELECT * FROM votes"); for (const v of voteRows as mysql.RowDataPacket[]) { await pg` INSERT INTO votes (round_uuid, user_name, movie_title, rating) VALUES (${v.round_uuid}, ${v.user_name}, ${v.movie_title}, ${v.rating}) ON CONFLICT (round_uuid, user_name, movie_title) DO NOTHING `; } console.log(` ${(voteRows as mysql.RowDataPacket[]).length} votes`); console.log("Migrating round_history..."); const [historyRows] = await maria.execute( "SELECT * FROM round_history ORDER BY id", ); for (const h of historyRows as mysql.RowDataPacket[]) { await pg` INSERT INTO round_history (id, round_uuid, winner, movies_json, created_at) VALUES (${h.id}, ${h.round_uuid}, ${h.winner}, ${h.movies_json}, ${h.created_at}) ON CONFLICT DO NOTHING `; } console.log( ` ${(historyRows as mysql.RowDataPacket[]).length} history entries`, ); // Reset the serial sequence for round_history.id await pg`SELECT setval('round_history_id_seq', COALESCE((SELECT MAX(id) FROM round_history), 0) + 1)`; console.log("Migration complete!"); await maria.end(); await pg.end();