'gold', 3 => 'silver', 2 => 'bronze', ]; public function handle(ArtworkAwardService $service): int { $dryRun = (bool) $this->option('dry-run'); $chunk = max(1, (int) $this->option('chunk')); $skipStats = (bool) $this->option('skip-stats'); $force = (bool) $this->option('force'); if ($dryRun) { $this->warn('[DRY-RUN] No data will be written.'); } // Verify legacy connection is reachable try { DB::connection('legacy')->getPdo(); } catch (\Throwable $e) { $this->error('Cannot connect to legacy database: ' . $e->getMessage()); return self::FAILURE; } if (! DB::connection('legacy')->getSchemaBuilder()->hasTable('users_opinions')) { $this->error('Legacy table `users_opinions` not found.'); return self::FAILURE; } // Pre-load sets of valid artwork IDs and user IDs from the new DB $this->info('Loading new-DB artwork and user ID sets…'); $validArtworkIds = DB::table('artworks') ->whereNull('deleted_at') ->pluck('id') ->flip() // flip so we can use isset() for O(1) lookup ->all(); $validUserIds = DB::table('users') ->whereNull('deleted_at') ->pluck('id') ->flip() ->all(); $this->info(sprintf( 'Found %d artworks and %d users in new DB.', count($validArtworkIds), count($validUserIds) )); // Count legacy rows for progress bar $total = DB::connection('legacy') ->table('users_opinions') ->count(); $this->info("Legacy rows to process: {$total}"); if ($total === 0) { $this->warn('No legacy rows found. Nothing to do.'); return self::SUCCESS; } $stats = [ 'imported' => 0, 'skipped_score' => 0, 'skipped_artwork' => 0, 'skipped_user' => 0, 'skipped_duplicate'=> 0, 'updated_force' => 0, 'errors' => 0, ]; $affectedArtworkIds = []; $bar = $this->output->createProgressBar($total); $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% | imported: %imported% | skipped: %skipped%'); $bar->setMessage('0', 'imported'); $bar->setMessage('0', 'skipped'); $bar->start(); DB::connection('legacy') ->table('users_opinions') ->orderBy('opinion_id') ->chunk($chunk, function ($rows) use ( &$stats, &$affectedArtworkIds, $validArtworkIds, $validUserIds, $dryRun, $force, $bar ) { $inserts = []; $now = now(); foreach ($rows as $row) { $artworkId = (int) $row->artwork_id; $userId = (int) $row->author_id; // author_id = the voter $score = (int) $row->score; $postedAt = $row->post_date ?? $now; // --- score → medal --- $medal = self::SCORE_MAP[$score] ?? null; if ($medal === null) { $stats['skipped_score']++; $bar->advance(); continue; } // --- Artwork must exist in new DB --- if (! isset($validArtworkIds[$artworkId])) { $stats['skipped_artwork']++; $bar->advance(); continue; } // --- User must exist in new DB --- if (! isset($validUserIds[$userId])) { $stats['skipped_user']++; $bar->advance(); continue; } if (! $dryRun) { if ($force) { // Upsert: update medal if row already exists $affected = DB::table('artwork_awards') ->where('artwork_id', $artworkId) ->where('user_id', $userId) ->update([ 'medal' => $medal, 'weight' => ArtworkAward::WEIGHTS[$medal], 'updated_at' => $now, ]); if ($affected > 0) { $stats['updated_force']++; $affectedArtworkIds[$artworkId] = true; $bar->advance(); continue; } } else { // Skip if already exists if ( DB::table('artwork_awards') ->where('artwork_id', $artworkId) ->where('user_id', $userId) ->exists() ) { $stats['skipped_duplicate']++; $bar->advance(); continue; } } $inserts[] = [ 'artwork_id' => $artworkId, 'user_id' => $userId, 'medal' => $medal, 'weight' => ArtworkAward::WEIGHTS[$medal], 'created_at' => $postedAt, 'updated_at' => $postedAt, ]; $affectedArtworkIds[$artworkId] = true; } $stats['imported']++; $bar->advance(); } // Bulk insert the batch (DB::table bypasses the observer intentionally; // stats are recalculated in bulk at the end for performance) if (! $dryRun && ! empty($inserts)) { try { DB::table('artwork_awards')->insert($inserts); } catch (\Throwable $e) { // Fallback: insert one-by-one to isolate constraint violations foreach ($inserts as $row) { try { DB::table('artwork_awards')->insertOrIgnore([$row]); } catch (\Throwable) { $stats['errors']++; } } } } $skippedTotal = $stats['skipped_score'] + $stats['skipped_artwork'] + $stats['skipped_user'] + $stats['skipped_duplicate']; $bar->setMessage((string) $stats['imported'], 'imported'); $bar->setMessage((string) $skippedTotal, 'skipped'); }); $bar->finish(); $this->newLine(2); // ------------------------------------------------------------------------- // Recalculate stats for every affected artwork // ------------------------------------------------------------------------- if (! $dryRun && ! $skipStats && ! empty($affectedArtworkIds)) { $artworkCount = count($affectedArtworkIds); $this->info("Recalculating award stats for {$artworkCount} artworks…"); $statsBar = $this->output->createProgressBar($artworkCount); $statsBar->start(); foreach (array_keys($affectedArtworkIds) as $artworkId) { try { $service->recalcStats($artworkId); } catch (\Throwable $e) { $this->newLine(); $this->warn("Stats recalc failed for artwork #{$artworkId}: {$e->getMessage()}"); } $statsBar->advance(); } $statsBar->finish(); $this->newLine(2); } // ------------------------------------------------------------------------- // Summary // ------------------------------------------------------------------------- $this->table( ['Result', 'Count'], [ ['Imported (new rows)', $stats['imported']], ['Forced updates', $stats['updated_force']], ['Skipped – bad score', $stats['skipped_score']], ['Skipped – artwork gone', $stats['skipped_artwork']], ['Skipped – user gone', $stats['skipped_user']], ['Skipped – duplicate', $stats['skipped_duplicate']], ['Errors', $stats['errors']], ] ); if ($dryRun) { $this->warn('[DRY-RUN] Nothing was written. Re-run without --dry-run to apply.'); } else { $this->info('Migration complete.'); } return $stats['errors'] > 0 ? self::FAILURE : self::SUCCESS; } }