import { Client, ClientConfig } from 'pg'; // Source database configuration const sourceConfig: ClientConfig = { host: '10.85.92.236', port: 5433, database: 'dev', user: 'rabbit', password: 'g39j90p11', }; // Target database configuration (Supabase) const targetConfig: ClientConfig = { host: 'aws-1-us-east-1.pooler.supabase.com', port: 6543, database: 'postgres', user: 'postgres.ffbgowwvcqmdtvvabmnh', password: '!a_KW.-6Grb-X?#', ssl: { rejectUnauthorized: false }, }; // Tables in dependency order (tables with no dependencies first) const MIGRATION_ORDER = [ // Layer 1: No foreign key dependencies 'user_profiles', 'inventory_devices', 'subscription_plans', 'weather_stations', 'camera_devices', 'analysis_results', // Independent table // Layer 2: Depends on Layer 1 'user_identities', // -> user_profiles 'devices', // -> user_profiles 'user_subscriptions', // -> user_profiles, subscription_plans 'weather_observations', // -> weather_stations 'weather_forecasts', // -> weather_stations // Layer 3: Depends on Layer 2 'device_registrations', // -> user_profiles, devices 'device_certificates', // -> devices 'device_configurations', // -> devices 'device_security_events', // -> devices 'raw_events', // -> devices, user_profiles 'subscription_history', // -> user_subscriptions 'payment_records', // -> user_subscriptions // Layer 4: Depends on Layer 3 'validated_events', // -> raw_events, devices, user_profiles ]; async function getTableColumns(client: Client, tableName: string): Promise { const result = await client.query(` SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' ORDER BY ordinal_position `, [tableName]); return result.rows.map(row => row.column_name); } async function getJsonColumns(client: Client, tableName: string): Promise { const result = await client.query(` SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' AND data_type IN ('json', 'jsonb') `, [tableName]); return result.rows.map(row => row.column_name); } // Column name mappings: source column -> target column const COLUMN_MAPPINGS: Record> = { weather_forecasts: { weather_station_id: 'station_id', }, }; async function migrateTable( sourceClient: Client, targetClient: Client, tableName: string ): Promise { console.log(`\nMigrating table: ${tableName}`); // Get source data const sourceData = await sourceClient.query(`SELECT * FROM ${tableName}`); const rowCount = sourceData.rows.length; if (rowCount === 0) { console.log(` No data to migrate`); return 0; } console.log(` Found ${rowCount} rows`); // Get target table columns to ensure we only insert columns that exist const targetColumns = await getTableColumns(targetClient, tableName); if (targetColumns.length === 0) { console.log(` WARNING: Table ${tableName} does not exist in target database, skipping`); return 0; } // Get column mappings for this table const columnMapping = COLUMN_MAPPINGS[tableName] || {}; const reverseMapping: Record = {}; for (const [source, target] of Object.entries(columnMapping)) { reverseMapping[target] = source; } // Filter source columns to only those that exist in target (with mapping) const sourceColumns = Object.keys(sourceData.rows[0] || {}); const columnsToMigrate: { source: string; target: string }[] = []; for (const sourceCol of sourceColumns) { const targetCol = columnMapping[sourceCol] || sourceCol; if (targetColumns.includes(targetCol)) { columnsToMigrate.push({ source: sourceCol, target: targetCol }); } } if (columnsToMigrate.length === 0) { console.log(` WARNING: No common columns found, skipping`); return 0; } // Get JSON/JSONB columns for proper serialization const jsonColumns = await getJsonColumns(targetClient, tableName); if (jsonColumns.length > 0) { console.log(` JSON columns: ${jsonColumns.join(', ')}`); } const targetColumnNames = columnsToMigrate.map(c => c.target); console.log(` Migrating columns: ${targetColumnNames.join(', ')}`); // Clear existing data in target table await targetClient.query(`DELETE FROM ${tableName}`); console.log(` Cleared existing data in target`); // Build insert query with parameterized placeholders const columnList = targetColumnNames.join(', '); let insertedCount = 0; for (const row of sourceData.rows) { // Serialize JSON columns properly, read from source column name const values = columnsToMigrate.map(({ source, target }) => { const value = row[source]; if (jsonColumns.includes(target) && value !== null && typeof value === 'object') { return JSON.stringify(value); } return value; }); const placeholders = columnsToMigrate.map((_, i) => `$${i + 1}`).join(', '); try { await targetClient.query( `INSERT INTO ${tableName} (${columnList}) VALUES (${placeholders})`, values ); insertedCount++; } catch (error: any) { console.error(` Error inserting row: ${error.message}`); console.error(` Row data: ${JSON.stringify(row)}`); } } console.log(` Successfully inserted ${insertedCount}/${rowCount} rows`); return insertedCount; } async function resetSequences(client: Client, tableName: string) { try { // Find sequence for this table const result = await client.query(` SELECT column_name, column_default FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' AND column_default LIKE 'nextval%' `, [tableName]); for (const row of result.rows) { const columnName = row.column_name; // Get max value and reset sequence const maxResult = await client.query(`SELECT MAX(${columnName}) as max_val FROM ${tableName}`); const maxVal = maxResult.rows[0].max_val; if (maxVal !== null) { // Extract sequence name from column_default const match = row.column_default.match(/nextval\('([^']+)'/); if (match) { const sequenceName = match[1]; await client.query(`SELECT setval('${sequenceName}', $1, true)`, [maxVal]); console.log(` Reset sequence ${sequenceName} to ${maxVal}`); } } } } catch (error: any) { // Ignore errors - some tables may not have sequences } } async function migrateData() { const sourceClient = new Client(sourceConfig); const targetClient = new Client(targetConfig); try { console.log('Connecting to source database...'); await sourceClient.connect(); console.log('Connected to source database!'); console.log('Connecting to target database (Supabase)...'); await targetClient.connect(); console.log('Connected to target database!'); // Disable triggers temporarily console.log('\nDisabling triggers...'); await targetClient.query('SET session_replication_role = replica'); let totalMigrated = 0; const results: { table: string; count: number }[] = []; for (const tableName of MIGRATION_ORDER) { try { const count = await migrateTable(sourceClient, targetClient, tableName); results.push({ table: tableName, count }); totalMigrated += count; } catch (error: any) { console.error(`Error migrating ${tableName}: ${error.message}`); results.push({ table: tableName, count: -1 }); } } // Re-enable triggers console.log('\nRe-enabling triggers...'); await targetClient.query('SET session_replication_role = DEFAULT'); // Reset sequences console.log('\nResetting sequences...'); for (const tableName of MIGRATION_ORDER) { await resetSequences(targetClient, tableName); } // Print summary console.log('\n========================================'); console.log('Migration Summary:'); console.log('========================================'); for (const result of results) { const status = result.count >= 0 ? `${result.count} rows` : 'FAILED'; console.log(`${result.table.padEnd(25)} : ${status}`); } console.log('========================================'); console.log(`Total rows migrated: ${totalMigrated}`); console.log('========================================'); } catch (error: any) { console.error('Migration failed:', error.message); throw error; } finally { await sourceClient.end(); await targetClient.end(); } } migrateData().catch(console.error);