/** * Migration: Switch from Serial to UUID primary keys * * Phase 3 of 3 for migrating Serial primary keys to UUID * This migration performs the actual switch: * 1. Drop old foreign key constraints * 2. Drop old primary keys * 3. Drop old columns * 4. Rename new columns * 5. Add new primary keys and foreign keys * * WARNING: This is a destructive migration. Ensure you have backups! * * @type {import('node-pg-migrate').ColumnDefinitions | undefined} */ export const shorthands = undefined; /** * @param pgm {import('node-pg-migrate').MigrationBuilder} * @param run {() => void | undefined} * @returns {Promise | void} */ export const up = (pgm) => { console.log('Phase 3: Switching to UUID primary keys...'); console.log('WARNING: This is a destructive migration. Ensure backups exist!'); // ===================================================== // Step 1: Drop existing foreign key constraints // ===================================================== console.log(' Step 1: Dropping existing foreign key constraints...'); // Drop FK constraints (dependent tables first) pgm.dropConstraint('payment_records', 'payment_records_user_subscription_id_fkey', { ifExists: true, }); pgm.dropConstraint('subscription_history', 'subscription_history_user_subscription_id_fkey', { ifExists: true, }); pgm.dropConstraint('user_subscriptions', 'user_subscriptions_subscription_plan_id_fkey', { ifExists: true, }); pgm.dropConstraint('weather_forecasts', 'weather_forecasts_station_id_fkey', { ifExists: true, }); pgm.dropConstraint('weather_observations', 'weather_observations_weather_station_id_fkey', { ifExists: true, }); // ===================================================== // Step 2: Drop old indexes on FK columns // ===================================================== console.log(' Step 2: Dropping old indexes...'); // Drop indexes that reference old columns pgm.dropIndex('weather_forecasts', 'station_id', { ifExists: true }); pgm.dropIndex('weather_observations', 'weather_station_id', { ifExists: true }); pgm.dropIndex('user_subscriptions', 'subscription_plan_id', { ifExists: true }); pgm.dropIndex('subscription_history', 'user_subscription_id', { ifExists: true }); pgm.dropIndex('payment_records', 'user_subscription_id', { ifExists: true }); // ===================================================== // Step 3: Drop old primary keys // ===================================================== console.log(' Step 3: Dropping old primary keys...'); const tablesToMigrate = [ 'analysis_results', 'weather_stations', 'weather_forecasts', 'weather_observations', 'subscription_plans', 'user_subscriptions', 'subscription_history', 'payment_records', 'camera_devices', ]; tablesToMigrate.forEach((table) => { pgm.dropConstraint(table, `${table}_pkey`, { ifExists: true }); }); // ===================================================== // Step 4: Drop old columns and rename new columns // ===================================================== console.log(' Step 4: Dropping old columns and renaming new columns...'); // For each table, drop old id and rename new_id to id tablesToMigrate.forEach((table) => { // Drop old id column pgm.dropColumn(table, 'id', { ifExists: true }); // Rename new_id to id pgm.renameColumn(table, 'new_id', 'id'); // Make id NOT NULL pgm.alterColumn(table, 'id', { notNull: true }); }); // Rename FK columns pgm.dropColumn('weather_forecasts', 'station_id', { ifExists: true }); pgm.renameColumn('weather_forecasts', 'new_station_id', 'station_id'); pgm.dropColumn('weather_observations', 'weather_station_id', { ifExists: true }); pgm.renameColumn('weather_observations', 'new_weather_station_id', 'weather_station_id'); pgm.dropColumn('user_subscriptions', 'subscription_plan_id', { ifExists: true }); pgm.renameColumn('user_subscriptions', 'new_subscription_plan_id', 'subscription_plan_id'); pgm.dropColumn('subscription_history', 'user_subscription_id', { ifExists: true }); pgm.renameColumn('subscription_history', 'new_user_subscription_id', 'user_subscription_id'); pgm.dropColumn('payment_records', 'user_subscription_id', { ifExists: true }); pgm.renameColumn('payment_records', 'new_user_subscription_id', 'user_subscription_id'); // ===================================================== // Step 5: Add new primary key constraints // ===================================================== console.log(' Step 5: Adding new primary key constraints...'); tablesToMigrate.forEach((table) => { pgm.addConstraint(table, `${table}_pkey`, { primaryKey: 'id' }); }); // ===================================================== // Step 6: Add new foreign key constraints // ===================================================== console.log(' Step 6: Adding new foreign key constraints...'); pgm.addConstraint('weather_forecasts', 'weather_forecasts_station_id_fkey', { foreignKeys: { columns: 'station_id', references: 'weather_stations(id)', onDelete: 'CASCADE', }, }); pgm.addConstraint('weather_observations', 'weather_observations_weather_station_id_fkey', { foreignKeys: { columns: 'weather_station_id', references: 'weather_stations(id)', onDelete: 'CASCADE', }, }); pgm.addConstraint('user_subscriptions', 'user_subscriptions_subscription_plan_id_fkey', { foreignKeys: { columns: 'subscription_plan_id', references: 'subscription_plans(id)', onDelete: 'CASCADE', }, }); pgm.addConstraint('subscription_history', 'subscription_history_user_subscription_id_fkey', { foreignKeys: { columns: 'user_subscription_id', references: 'user_subscriptions(id)', onDelete: 'CASCADE', }, }); pgm.addConstraint('payment_records', 'payment_records_user_subscription_id_fkey', { foreignKeys: { columns: 'user_subscription_id', references: 'user_subscriptions(id)', onDelete: 'CASCADE', }, }); // ===================================================== // Step 7: Recreate indexes on FK columns // ===================================================== console.log(' Step 7: Recreating indexes on FK columns...'); pgm.createIndex('weather_forecasts', 'station_id', { name: 'idx_weather_forecasts_station_id', }); pgm.createIndex('weather_observations', 'weather_station_id', { name: 'idx_weather_observations_station_id', }); pgm.createIndex('user_subscriptions', 'subscription_plan_id', { name: 'idx_user_subscriptions_plan_id', }); pgm.createIndex('subscription_history', 'user_subscription_id', { name: 'idx_subscription_history_sub_id', }); pgm.createIndex('payment_records', 'user_subscription_id', { name: 'idx_payment_records_sub_id', }); // ===================================================== // Step 8: Clean up temporary indexes // ===================================================== console.log(' Step 8: Cleaning up temporary indexes...'); tablesToMigrate.forEach((table) => { pgm.dropIndex(table, [], { name: `idx_${table}_new_id`, ifExists: true }); }); // Drop temporary indexes from Phase 2 pgm.dropIndex('weather_forecasts', [], { name: 'idx_weather_forecasts_new_station_id', ifExists: true, }); pgm.dropIndex('weather_observations', [], { name: 'idx_weather_observations_new_station_id', ifExists: true, }); pgm.dropIndex('user_subscriptions', [], { name: 'idx_user_subscriptions_new_plan_id', ifExists: true, }); pgm.dropIndex('subscription_history', [], { name: 'idx_subscription_history_new_sub_id', ifExists: true, }); pgm.dropIndex('payment_records', [], { name: 'idx_payment_records_new_sub_id', ifExists: true, }); console.log('Phase 3 complete: All tables now use UUID primary keys.'); console.log(''); console.log('IMPORTANT: The _migration_id_mapping table has been preserved for reference.'); console.log('You may drop it manually after verifying the migration: DROP TABLE _migration_id_mapping;'); }; /** * @param pgm {import('node-pg-migrate').MigrationBuilder} * @param run {() => void | undefined} * @returns {Promise | void} */ export const down = (pgm) => { console.log('Rolling back Phase 3...'); console.log('WARNING: This rollback requires the _migration_id_mapping table to exist!'); // This rollback is complex and requires the mapping table // In production, consider using a backup restore instead pgm.sql(` -- Check if mapping table exists DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '_migration_id_mapping') THEN RAISE EXCEPTION 'Cannot rollback: _migration_id_mapping table does not exist. Restore from backup.'; END IF; END $$; `); // The full rollback would be very complex. // In practice, recommend restoring from backup before Phase 3. console.log(''); console.log('ROLLBACK NOTE: For Phase 3, it is recommended to restore from a backup'); console.log('taken before the migration rather than attempting to reverse the changes.'); console.log(''); console.log('If you must rollback, the _migration_id_mapping table contains the'); console.log('old_id -> new_id mappings needed to reconstruct the serial IDs.'); throw new Error( 'Phase 3 rollback requires manual intervention or backup restore. ' + 'See migration comments for details.', ); };