271 lines
9.3 KiB
PHP
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,
|
|
];
|
|
}
|
|
} |