/** * SQLite to PostgreSQL Data Migration * * Streams all data from the SQLite testdata.db into PostgreSQL. * Uses batch INSERTs for performance. * * Usage: * node migrate-data.js Migrate all tables * node migrate-data.js --skip-tsvector Skip tsvector rebuild (faster, trigger handles it) * node migrate-data.js --table test_records Migrate only one table */ const path = require('path'); const Database = require('better-sqlite3'); const db = require('./db'); const SQLITE_PATH = path.join(__dirname, 'testdata.db'); const BATCH_SIZE = 5000; async function migrateTestRecords(sqlite) { console.log('\n--- Migrating test_records ---'); const total = sqlite.prepare('SELECT COUNT(*) as cnt FROM test_records').get().cnt; console.log(` Source records: ${total.toLocaleString()}`); // Disable triggers during bulk load for performance await db.execute('ALTER TABLE test_records DISABLE TRIGGER trg_search_vector'); const stmt = sqlite.prepare('SELECT * FROM test_records ORDER BY id'); let migrated = 0; let batch = []; for (const row of stmt.iterate()) { batch.push(row); if (batch.length >= BATCH_SIZE) { await insertTestRecordsBatch(batch); migrated += batch.length; batch = []; process.stdout.write(`\r Migrated: ${migrated.toLocaleString()} / ${total.toLocaleString()}`); } } // Flush remaining if (batch.length > 0) { await insertTestRecordsBatch(batch); migrated += batch.length; } console.log(`\r Migrated: ${migrated.toLocaleString()} / ${total.toLocaleString()}`); // Rebuild search_vector for all rows console.log(' Rebuilding search_vector (this may take a few minutes)...'); await db.execute(` UPDATE test_records SET search_vector = to_tsvector('english', COALESCE(serial_number, '') || ' ' || COALESCE(model_number, '') || ' ' || COALESCE(raw_data, '') ) `); console.log(' search_vector rebuilt.'); // Re-enable trigger await db.execute('ALTER TABLE test_records ENABLE TRIGGER trg_search_vector'); // Reset sequence to max id await db.execute(`SELECT setval('test_records_id_seq', (SELECT COALESCE(MAX(id), 1) FROM test_records))`); return migrated; } async function insertTestRecordsBatch(batch) { // Build a multi-row INSERT const cols = ['id', 'log_type', 'model_number', 'serial_number', 'test_date', 'test_station', 'overall_result', 'raw_data', 'source_file', 'import_date', 'datasheet_exported_at', 'forweb_exported_at', 'work_order']; const values = []; const params = []; let paramIdx = 0; for (const row of batch) { const placeholders = cols.map(() => { paramIdx++; return `$${paramIdx}`; }); values.push(`(${placeholders.join(',')})`); params.push( row.id, row.log_type, row.model_number, row.serial_number, row.test_date, row.test_station, row.overall_result, row.raw_data, row.source_file, row.import_date, row.datasheet_exported_at, row.forweb_exported_at, row.work_order ); } const sql = `INSERT INTO test_records (${cols.join(',')}) VALUES ${values.join(',')} ON CONFLICT (log_type, model_number, serial_number, test_date, test_station) DO NOTHING`; await db.execute(sql, params); } async function migrateWorkOrders(sqlite) { console.log('\n--- Migrating work_orders ---'); const rows = sqlite.prepare('SELECT * FROM work_orders ORDER BY id').all(); console.log(` Source records: ${rows.length.toLocaleString()}`); let migrated = 0; const cols = ['wo_number', 'wo_date', 'program', 'version', 'lib_version', 'test_station', 'source_file', 'import_date']; for (let i = 0; i < rows.length; i += BATCH_SIZE) { const batch = rows.slice(i, i + BATCH_SIZE); const values = []; const params = []; let paramIdx = 0; for (const row of batch) { const placeholders = cols.map(() => { paramIdx++; return `$${paramIdx}`; }); values.push(`(${placeholders.join(',')})`); params.push(row.wo_number, row.wo_date, row.program, row.version, row.lib_version, row.test_station, row.source_file, row.import_date); } await db.execute( `INSERT INTO work_orders (${cols.join(',')}) VALUES ${values.join(',')} ON CONFLICT (wo_number, test_station) DO NOTHING`, params ); migrated += batch.length; } console.log(` Migrated: ${migrated.toLocaleString()}`); return migrated; } async function migrateWorkOrderLines(sqlite) { console.log('\n--- Migrating work_order_lines ---'); const total = sqlite.prepare('SELECT COUNT(*) as cnt FROM work_order_lines').get().cnt; console.log(` Source records: ${total.toLocaleString()}`); const stmt = sqlite.prepare('SELECT * FROM work_order_lines ORDER BY id'); let migrated = 0; let batch = []; for (const row of stmt.iterate()) { batch.push(row); if (batch.length >= BATCH_SIZE) { await insertWoLinesBatch(batch); migrated += batch.length; batch = []; process.stdout.write(`\r Migrated: ${migrated.toLocaleString()} / ${total.toLocaleString()}`); } } if (batch.length > 0) { await insertWoLinesBatch(batch); migrated += batch.length; } console.log(`\r Migrated: ${migrated.toLocaleString()} / ${total.toLocaleString()}`); return migrated; } async function insertWoLinesBatch(batch) { const cols = ['wo_number', 'serial_number', 'status', 'model_number', 'ds_filename', 'test_date', 'test_time', 'test_station']; const values = []; const params = []; let paramIdx = 0; for (const row of batch) { const placeholders = cols.map(() => { paramIdx++; return `$${paramIdx}`; }); values.push(`(${placeholders.join(',')})`); params.push(row.wo_number, row.serial_number, row.status, row.model_number, row.ds_filename, row.test_date, row.test_time, row.test_station); } await db.execute( `INSERT INTO work_order_lines (${cols.join(',')}) VALUES ${values.join(',')} ON CONFLICT (wo_number, serial_number, test_date, test_time) DO NOTHING`, params ); } async function main() { const args = process.argv.slice(2); const tableArg = args.indexOf('--table'); const targetTable = tableArg >= 0 ? args[tableArg + 1] : null; console.log('========================================'); console.log('SQLite -> PostgreSQL Data Migration'); console.log('========================================'); console.log(`SQLite: ${SQLITE_PATH}`); console.log(`Start: ${new Date().toISOString()}`); const sqlite = new Database(SQLITE_PATH, { readonly: true }); try { if (!targetTable || targetTable === 'test_records') { await migrateTestRecords(sqlite); } if (!targetTable || targetTable === 'work_orders') { await migrateWorkOrders(sqlite); } if (!targetTable || targetTable === 'work_order_lines') { await migrateWorkOrderLines(sqlite); } // VACUUM ANALYZE console.log('\n--- Running VACUUM ANALYZE ---'); await db.execute('VACUUM ANALYZE test_records'); await db.execute('VACUUM ANALYZE work_orders'); await db.execute('VACUUM ANALYZE work_order_lines'); console.log(' Done.'); // Verify counts console.log('\n--- Verification ---'); const pgTestCount = await db.queryOne('SELECT COUNT(*) as cnt FROM test_records'); const pgWoCount = await db.queryOne('SELECT COUNT(*) as cnt FROM work_orders'); const pgWolCount = await db.queryOne('SELECT COUNT(*) as cnt FROM work_order_lines'); const sqliteTestCount = sqlite.prepare('SELECT COUNT(*) as cnt FROM test_records').get().cnt; const sqliteWoCount = sqlite.prepare('SELECT COUNT(*) as cnt FROM work_orders').get().cnt; const sqliteWolCount = sqlite.prepare('SELECT COUNT(*) as cnt FROM work_order_lines').get().cnt; console.log(` test_records: SQLite=${sqliteTestCount.toLocaleString()} PG=${parseInt(pgTestCount.cnt).toLocaleString()} ${parseInt(pgTestCount.cnt) === sqliteTestCount ? '[OK]' : '[MISMATCH]'}`); console.log(` work_orders: SQLite=${sqliteWoCount.toLocaleString()} PG=${parseInt(pgWoCount.cnt).toLocaleString()} ${parseInt(pgWoCount.cnt) === sqliteWoCount ? '[OK]' : '[MISMATCH]'}`); console.log(` work_order_lines: SQLite=${sqliteWolCount.toLocaleString()} PG=${parseInt(pgWolCount.cnt).toLocaleString()} ${parseInt(pgWolCount.cnt) === sqliteWolCount ? '[OK]' : '[MISMATCH]'}`); } finally { sqlite.close(); await db.close(); } console.log(`\n========================================`); console.log(`Migration Complete`); console.log(`========================================`); console.log(`End: ${new Date().toISOString()}`); } main().catch(err => { console.error('Migration failed:', err); process.exit(1); });