Files
SkinbaseNova/app/Console/Commands/AuditLegacyArtworkUserIdsCommand.php
2026-04-18 17:02:56 +02:00

271 lines
9.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
final class AuditLegacyArtworkUserIdsCommand extends Command
{
protected $signature = 'artworks:audit-legacy-user-ids
{--chunk=1000 : Number of legacy artwork rows to process per batch}
{--show=100 : Maximum number of discrepancies to print}
{--artwork-id= : Only compare one artwork id}
{--legacy-connection=legacy : Legacy database connection name}
{--legacy-table=wallz : Legacy artworks table name}
{--new-table=artworks : Current artworks table name}
{--json : Output the summary and discrepancies as JSON}';
protected $description = 'Compare legacy wallz.user_id values against artworks.user_id using shared artwork ids';
public function handle(): int
{
$chunkSize = max(1, (int) $this->option('chunk'));
$show = max(0, (int) $this->option('show'));
$legacyConnection = (string) $this->option('legacy-connection');
$legacyTable = (string) $this->option('legacy-table');
$newTable = (string) $this->option('new-table');
$artworkId = $this->option('artwork-id') !== null ? (int) $this->option('artwork-id') : null;
$json = (bool) $this->option('json');
if ($artworkId !== null && $artworkId <= 0) {
$this->error('The --artwork-id option must be a positive integer.');
return self::FAILURE;
}
if (! $this->tableExists($legacyConnection, $legacyTable)) {
$this->error("Legacy table {$legacyConnection}.{$legacyTable} does not exist or the connection is unavailable.");
return self::FAILURE;
}
if (! $this->tableExists(null, $newTable)) {
$this->error("Current table {$newTable} does not exist.");
return self::FAILURE;
}
if (! $this->columnExists($legacyConnection, $legacyTable, 'user_id') || ! $this->columnExists($legacyConnection, $legacyTable, 'id')) {
$this->error("Legacy table {$legacyConnection}.{$legacyTable} must contain id and user_id columns.");
return self::FAILURE;
}
if (! $this->columnExists(null, $newTable, 'user_id') || ! $this->columnExists(null, $newTable, 'id')) {
$this->error("Current table {$newTable} must contain id and user_id columns.");
return self::FAILURE;
}
$legacyCountQuery = DB::connection($legacyConnection)->table($legacyTable);
if ($artworkId !== null) {
$legacyCountQuery->where('id', $artworkId);
}
$total = (int) $legacyCountQuery->count();
if ($total === 0) {
$message = $artworkId === null
? "No rows found in {$legacyConnection}.{$legacyTable}."
: "Legacy artwork #{$artworkId} was not found in {$legacyConnection}.{$legacyTable}.";
$this->warn($message);
return $artworkId === null ? self::SUCCESS : self::FAILURE;
}
$this->info(sprintf(
'Comparing %d legacy %s.%s row(s) against %s in chunks of %d...',
$total,
$legacyConnection,
$legacyTable,
$newTable,
$chunkSize,
));
$summary = [
'checked' => 0,
'matched' => 0,
'mismatched' => 0,
'missing_in_new' => 0,
];
$discrepancies = [];
$legacyQuery = DB::connection($legacyConnection)
->table($legacyTable)
->select(['id', 'user_id'])
->orderBy('id');
if ($artworkId !== null) {
$legacyQuery->where('id', $artworkId);
}
$legacyQuery->chunkById($chunkSize, function ($rows) use (&$summary, &$discrepancies, $show, $newTable): void {
$artworkIds = $rows->pluck('id')->map(static fn (mixed $id): int => (int) $id)->all();
$newRows = DB::table($newTable)
->select(['id', 'user_id', 'title'])
->whereIn('id', $artworkIds)
->get()
->keyBy(static fn (object $row): int => (int) $row->id);
foreach ($rows as $row) {
$summary['checked']++;
$legacyUserId = $this->normalizeNullableInt($row->user_id ?? null);
$currentRow = $newRows->get((int) $row->id);
if ($currentRow === null) {
$summary['missing_in_new']++;
$this->rememberDiscrepancy(
$discrepancies,
$show,
(int) $row->id,
$legacyUserId,
null,
null,
'missing_in_new',
);
continue;
}
$newUserId = $this->normalizeNullableInt($currentRow->user_id ?? null);
if ($legacyUserId === $newUserId) {
$summary['matched']++;
continue;
}
$summary['mismatched']++;
$this->rememberDiscrepancy(
$discrepancies,
$show,
(int) $row->id,
$legacyUserId,
$newUserId,
(string) ($currentRow->title ?? ''),
'user_id_mismatch',
);
}
if ($this->output->isVerbose()) {
$this->line(sprintf(
' audited=%d matched=%d mismatched=%d missing_in_new=%d',
$summary['checked'],
$summary['matched'],
$summary['mismatched'],
$summary['missing_in_new'],
));
}
}, 'id');
if ($json) {
$this->line(json_encode([
'summary' => $summary,
'discrepancies' => $discrepancies,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
return ($summary['mismatched'] === 0 && $summary['missing_in_new'] === 0)
? self::SUCCESS
: self::FAILURE;
}
$this->table(
['Checked', 'Matched', 'Mismatched', 'Missing In New'],
[[
$summary['checked'],
$summary['matched'],
$summary['mismatched'],
$summary['missing_in_new'],
]],
);
if ($discrepancies !== []) {
$this->newLine();
$this->warn(sprintf(
'Showing %d discrepancy row(s)%s.',
count($discrepancies),
($summary['mismatched'] + $summary['missing_in_new']) > count($discrepancies)
? sprintf(' out of %d total', $summary['mismatched'] + $summary['missing_in_new'])
: '',
));
$this->table(
['Artwork ID', 'Legacy user_id', 'New user_id', 'Status', 'Title'],
array_map(static fn (array $row): array => [
$row['artwork_id'],
$row['legacy_user_id'],
$row['new_user_id'],
$row['status'],
$row['title'],
], $discrepancies),
);
} else {
$this->info('No user_id mismatches were found.');
}
return ($summary['mismatched'] === 0 && $summary['missing_in_new'] === 0)
? self::SUCCESS
: self::FAILURE;
}
private function tableExists(?string $connection, string $table): bool
{
try {
return $connection === null
? DB::getSchemaBuilder()->hasTable($table)
: DB::connection($connection)->getSchemaBuilder()->hasTable($table);
} catch (\Throwable) {
return false;
}
}
private function columnExists(?string $connection, string $table, string $column): bool
{
try {
return $connection === null
? DB::getSchemaBuilder()->hasColumn($table, $column)
: DB::connection($connection)->getSchemaBuilder()->hasColumn($table, $column);
} catch (\Throwable) {
return false;
}
}
private function normalizeNullableInt(mixed $value): ?int
{
if ($value === null || $value === '') {
return null;
}
return (int) $value;
}
/**
* @param array<int, array{artwork_id:int, legacy_user_id:string, new_user_id:string, title:string, status:string}> $discrepancies
*/
private function rememberDiscrepancy(
array &$discrepancies,
int $show,
int $artworkId,
?int $legacyUserId,
?int $newUserId,
?string $title,
string $status,
): void {
if (count($discrepancies) >= $show) {
return;
}
$discrepancies[] = [
'artwork_id' => $artworkId,
'legacy_user_id' => $legacyUserId === null ? '[null]' : (string) $legacyUserId,
'new_user_id' => $newUserId === null ? '[missing]' : (string) $newUserId,
'title' => $title ?? '',
'status' => $status,
];
}
}