optimizations
This commit is contained in:
342
app/Console/Commands/RepairLegacyUserJoinDatesCommand.php
Normal file
342
app/Console/Commands/RepairLegacyUserJoinDatesCommand.php
Normal file
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RepairLegacyUserJoinDatesCommand extends Command
|
||||
{
|
||||
/** @var array<string, bool> */
|
||||
private array $legacyTableExistsCache = [];
|
||||
|
||||
protected $signature = 'skinbase:repair-user-join-dates
|
||||
{--chunk=500 : Number of users to process per batch}
|
||||
{--legacy-connection=legacy : Legacy database connection name}
|
||||
{--legacy-table=users : Legacy users table name}
|
||||
{--only-null : Update only users whose current created_at is null}
|
||||
{--dry-run : Preview join date updates without writing changes}';
|
||||
|
||||
protected $description = 'Backfill current users.created_at from legacy users.joinDate';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$chunk = max(1, (int) $this->option('chunk'));
|
||||
$legacyConnection = (string) $this->option('legacy-connection');
|
||||
$legacyTable = (string) $this->option('legacy-table');
|
||||
$onlyNull = (bool) $this->option('only-null');
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
if (! $this->legacyTableExists($legacyConnection, $legacyTable)) {
|
||||
$this->error("Legacy table {$legacyConnection}.{$legacyTable} does not exist or the connection is unavailable.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('[DRY RUN] No changes will be written.');
|
||||
}
|
||||
|
||||
$query = DB::table('users')->select(['id', 'created_at']);
|
||||
|
||||
if ($onlyNull) {
|
||||
$query->whereNull('created_at');
|
||||
}
|
||||
|
||||
$this->info('Scanning current users for legacy joinDate backfill.');
|
||||
|
||||
$processed = 0;
|
||||
$matched = 0;
|
||||
$updated = 0;
|
||||
$unchanged = 0;
|
||||
$skipped = 0;
|
||||
|
||||
$query
|
||||
->chunkById($chunk, function (Collection $rows) use (
|
||||
&$processed,
|
||||
&$matched,
|
||||
&$updated,
|
||||
&$unchanged,
|
||||
&$skipped,
|
||||
$legacyConnection,
|
||||
$legacyTable,
|
||||
$dryRun
|
||||
): void {
|
||||
$legacyById = $this->loadLegacyUsersForChunk($rows, $legacyConnection, $legacyTable);
|
||||
$activityById = $this->loadLegacyActivityDatesForChunk($rows, $legacyConnection);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$processed++;
|
||||
|
||||
$legacyMatch = $legacyById[(int) $row->id] ?? null;
|
||||
|
||||
if ($legacyMatch === null) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$matched++;
|
||||
|
||||
$legacyJoinDate = $this->parseLegacyJoinDate($legacyMatch->joinDate ?? null);
|
||||
$dateSource = 'joinDate';
|
||||
|
||||
if ($legacyJoinDate === null) {
|
||||
$activityFallback = $activityById[(int) $row->id] ?? null;
|
||||
$legacyJoinDate = $activityFallback['date'] ?? null;
|
||||
$dateSource = $activityFallback['source'] ?? 'activity';
|
||||
}
|
||||
|
||||
if ($legacyJoinDate === null) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentCreatedAt = $this->parseCurrentDate($row->created_at ?? null);
|
||||
if ($currentCreatedAt !== null && $currentCreatedAt->equalTo($legacyJoinDate)) {
|
||||
$unchanged++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line(sprintf(
|
||||
'[dry] Would update user id=%d created_at %s => %s (%s)',
|
||||
(int) $row->id,
|
||||
$currentCreatedAt?->toDateTimeString() ?? '<null>',
|
||||
$legacyJoinDate->toDateTimeString(),
|
||||
$dateSource
|
||||
));
|
||||
$updated++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$affected = DB::table('users')
|
||||
->where('id', (int) $row->id)
|
||||
->update([
|
||||
'created_at' => $legacyJoinDate->toDateTimeString(),
|
||||
]);
|
||||
|
||||
if ($affected > 0) {
|
||||
$updated += $affected;
|
||||
$this->line(sprintf(
|
||||
'[update] user id=%d created_at => %s (%s)',
|
||||
(int) $row->id,
|
||||
$legacyJoinDate->toDateTimeString(),
|
||||
$dateSource
|
||||
));
|
||||
}
|
||||
}
|
||||
}, 'id');
|
||||
|
||||
$this->info(sprintf(
|
||||
'Finished. processed=%d matched=%d updated=%d unchanged=%d skipped=%d',
|
||||
$processed,
|
||||
$matched,
|
||||
$updated,
|
||||
$unchanged,
|
||||
$skipped
|
||||
));
|
||||
|
||||
if ($processed === 0) {
|
||||
$this->info('No users matched the requested scope.');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function legacyTableExists(string $connection, string $table): bool
|
||||
{
|
||||
$cacheKey = strtolower($connection . ':' . $table);
|
||||
|
||||
if (array_key_exists($cacheKey, $this->legacyTableExistsCache)) {
|
||||
return $this->legacyTableExistsCache[$cacheKey];
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->legacyTableExistsCache[$cacheKey] = DB::connection($connection)->getSchemaBuilder()->hasTable($table);
|
||||
} catch (\Throwable) {
|
||||
return $this->legacyTableExistsCache[$cacheKey] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, object>
|
||||
*/
|
||||
private function loadLegacyUsersForChunk(Collection $rows, string $legacyConnection, string $legacyTable): array
|
||||
{
|
||||
$legacyById = [];
|
||||
|
||||
$ids = $rows
|
||||
->pluck('id')
|
||||
->map(static fn ($id): int => (int) $id)
|
||||
->filter(static fn (int $id): bool => $id > 0)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($ids !== []) {
|
||||
DB::connection($legacyConnection)
|
||||
->table($legacyTable)
|
||||
->select(['user_id', 'joinDate'])
|
||||
->whereIn('user_id', $ids)
|
||||
->get()
|
||||
->each(function (object $legacyRow) use (&$legacyById): void {
|
||||
$legacyById[(int) $legacyRow->user_id] = $legacyRow;
|
||||
});
|
||||
}
|
||||
|
||||
return $legacyById;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{date: Carbon, source: string}>
|
||||
*/
|
||||
private function loadLegacyActivityDatesForChunk(Collection $rows, string $legacyConnection): array
|
||||
{
|
||||
$activityById = [];
|
||||
|
||||
$ids = $rows
|
||||
->pluck('id')
|
||||
->map(static fn ($id): int => (int) $id)
|
||||
->filter(static fn (int $id): bool => $id > 0)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($ids === []) {
|
||||
return $activityById;
|
||||
}
|
||||
|
||||
if ($this->legacyTableExists($legacyConnection, 'wallz')) {
|
||||
$this->registerChunkActivityDates(
|
||||
$activityById,
|
||||
DB::connection($legacyConnection)
|
||||
->table('wallz')
|
||||
->selectRaw('user_id, MIN(datum) as first_at')
|
||||
->whereIn('user_id', $ids)
|
||||
->whereRaw("datum IS NOT NULL AND datum <> '0000-00-00 00:00:00'")
|
||||
->groupBy('user_id')
|
||||
->get(),
|
||||
'first upload'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->legacyTableExists($legacyConnection, 'forum_topics')) {
|
||||
$this->registerChunkActivityDates(
|
||||
$activityById,
|
||||
DB::connection($legacyConnection)
|
||||
->table('forum_topics')
|
||||
->selectRaw('user_id, MIN(post_date) as first_at')
|
||||
->whereIn('user_id', $ids)
|
||||
->whereRaw("post_date <> '0000-00-00 00:00:00'")
|
||||
->groupBy('user_id')
|
||||
->get(),
|
||||
'first forum topic'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->legacyTableExists($legacyConnection, 'forum_posts')) {
|
||||
$this->registerChunkActivityDates(
|
||||
$activityById,
|
||||
DB::connection($legacyConnection)
|
||||
->table('forum_posts')
|
||||
->selectRaw('user_id, MIN(post_date) as first_at')
|
||||
->whereIn('user_id', $ids)
|
||||
->whereRaw("post_date <> '0000-00-00 00:00:00'")
|
||||
->groupBy('user_id')
|
||||
->get(),
|
||||
'first forum post'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->legacyTableExists($legacyConnection, 'artworks_comments')) {
|
||||
$this->registerChunkActivityDates(
|
||||
$activityById,
|
||||
DB::connection($legacyConnection)
|
||||
->table('artworks_comments')
|
||||
->selectRaw("user_id, MIN(TIMESTAMP(`date`, COALESCE(`time`, '00:00:00'))) as first_at")
|
||||
->whereIn('user_id', $ids)
|
||||
->whereRaw("`date` IS NOT NULL AND `date` <> '0000-00-00'")
|
||||
->groupBy('user_id')
|
||||
->get(),
|
||||
'first artwork comment'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->legacyTableExists($legacyConnection, 'users_comments')) {
|
||||
$this->registerChunkActivityDates(
|
||||
$activityById,
|
||||
DB::connection($legacyConnection)
|
||||
->table('users_comments')
|
||||
->selectRaw("user_id, MIN(TIMESTAMP(`date`, COALESCE(`time`, '00:00:00'))) as first_at")
|
||||
->whereIn('user_id', $ids)
|
||||
->whereRaw("`date` IS NOT NULL AND `date` <> '0000-00-00'")
|
||||
->groupBy('user_id')
|
||||
->get(),
|
||||
'first profile comment'
|
||||
);
|
||||
}
|
||||
|
||||
return $activityById;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{date: Carbon, source: string}> $activityById
|
||||
*/
|
||||
private function registerChunkActivityDates(array &$activityById, iterable $rows, string $source): void
|
||||
{
|
||||
foreach ($rows as $row) {
|
||||
$candidate = $this->parseLegacyJoinDate($row->first_at ?? null);
|
||||
if ($candidate === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$userId = (int) ($row->user_id ?? 0);
|
||||
if ($userId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = $activityById[$userId]['date'] ?? null;
|
||||
if ($existing === null || $candidate->lt($existing)) {
|
||||
$activityById[$userId] = [
|
||||
'date' => $candidate,
|
||||
'source' => $source,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseLegacyJoinDate(mixed $value): ?Carbon
|
||||
{
|
||||
$raw = trim((string) ($value ?? ''));
|
||||
|
||||
if ($raw === '' || str_starts_with($raw, '0000-00-00')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($raw);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseCurrentDate(mixed $value): ?Carbon
|
||||
{
|
||||
if ($value instanceof Carbon) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$raw = trim((string) ($value ?? ''));
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($raw);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user