option('dry-run'); $chunk = max(1, (int) $this->option('chunk')); $skipEmpty = (bool) $this->option('skip-empty'); if ($dryRun) { $this->warn('[DRY-RUN] No data will be written.'); } // Verify legacy connection 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('artworks_comments')) { $this->error('Legacy table `artworks_comments` not found.'); return self::FAILURE; } if (! DB::getSchemaBuilder()->hasColumn('artwork_comments', 'legacy_id')) { $this->error('Column `legacy_id` missing from `artwork_comments`. Run: php artisan migrate'); return self::FAILURE; } // Pre-load valid artwork IDs and user IDs from new DB for O(1) lookup $this->info('Loading new-DB artwork and user ID sets…'); $validArtworkIds = DB::table('artworks') ->whereNull('deleted_at') ->pluck('id') ->flip() ->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) )); // Already-imported legacy IDs (to resume safely) $this->info('Loading already-imported legacy_ids…'); $alreadyImported = DB::table('artwork_comments') ->whereNotNull('legacy_id') ->pluck('legacy_id') ->flip() ->all(); $this->info(sprintf('%d comments already imported (will be skipped).', count($alreadyImported))); $total = DB::connection('legacy')->table('artworks_comments')->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_duplicate' => 0, 'skipped_artwork' => 0, 'skipped_user' => 0, 'skipped_empty' => 0, 'errors' => 0, ]; $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('artworks_comments') ->orderBy('comment_id') ->chunk($chunk, function ($rows) use ( &$stats, &$alreadyImported, $validArtworkIds, $validUserIds, $dryRun, $skipEmpty, $bar ) { $inserts = []; $now = now(); foreach ($rows as $row) { $legacyId = (int) $row->comment_id; $artworkId = (int) $row->artwork_id; $userId = (int) $row->user_id; $content = trim((string) ($row->description ?? '')); // --- Already imported --- if (isset($alreadyImported[$legacyId])) { $stats['skipped_duplicate']++; $bar->advance(); continue; } // --- Content --- if ($skipEmpty && $content === '') { $stats['skipped_empty']++; $bar->advance(); continue; } // Replace empty content with a placeholder so NOT NULL is satisfied if ($content === '') { $content = '[no content]'; } // --- Artwork must exist --- if (! isset($validArtworkIds[$artworkId])) { $stats['skipped_artwork']++; $bar->advance(); continue; } // --- User must exist --- if (! isset($validUserIds[$userId])) { $stats['skipped_user']++; $bar->advance(); continue; } // --- Build timestamp from separate date + time columns --- $createdAt = $this->buildTimestamp($row->date, $row->time, $now); if (! $dryRun) { $inserts[] = [ 'legacy_id' => $legacyId, 'artwork_id' => $artworkId, 'user_id' => $userId, 'content' => $content, 'is_approved' => 1, 'created_at' => $createdAt, 'updated_at' => $createdAt, 'deleted_at' => null, ]; $alreadyImported[$legacyId] = true; } $stats['imported']++; $bar->advance(); } if (! $dryRun && ! empty($inserts)) { try { DB::table('artwork_comments')->insert($inserts); } catch (\Throwable $e) { // Fallback: row-by-row with ignore on unique violations foreach ($inserts as $row) { try { DB::table('artwork_comments')->insertOrIgnore([$row]); } catch (\Throwable) { $stats['errors']++; } } } } $skippedTotal = $stats['skipped_duplicate'] + $stats['skipped_artwork'] + $stats['skipped_user'] + $stats['skipped_empty']; $bar->setMessage((string) $stats['imported'], 'imported'); $bar->setMessage((string) $skippedTotal, 'skipped'); }); $bar->finish(); $this->newLine(2); // ------------------------------------------------------------------------- // Summary // ------------------------------------------------------------------------- $this->table( ['Result', 'Count'], [ ['Imported', $stats['imported']], ['Skipped – already imported', $stats['skipped_duplicate']], ['Skipped – artwork gone', $stats['skipped_artwork']], ['Skipped – user gone', $stats['skipped_user']], ['Skipped – empty content', $stats['skipped_empty']], ['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; } /** * Combine a legacy `date` (DATE) and `time` (TIME) column into a single datetime string. * Falls back to $fallback when both are null. */ private function buildTimestamp(mixed $date, mixed $time, \Illuminate\Support\Carbon $fallback): string { if (! $date) { return $fallback->toDateTimeString(); } $datePart = substr((string) $date, 0, 10); // '2000-09-13' $timePart = $time ? substr((string) $time, 0, 8) : '00:00:00'; // '09:34:27' // Sanity-check: MySQL TIME can be negative or > 24h for intervals — clamp to midnight if (! preg_match('/^\d{2}:\d{2}:\d{2}$/', $timePart) || $timePart < '00:00:00') { $timePart = '00:00:00'; } return $datePart . ' ' . $timePart; } }