diff --git a/app/Services/Binaries/BinaryHandler.php b/app/Services/Binaries/BinaryHandler.php index 9771556a0..8ca1dd9f6 100644 --- a/app/Services/Binaries/BinaryHandler.php +++ b/app/Services/Binaries/BinaryHandler.php @@ -116,10 +116,15 @@ final class BinaryHandler int $fileNumber, int $partSize ): int { - $affected = DB::affectingStatement( - 'INSERT OR IGNORE INTO binaries (binaryhash, name, collections_id, totalparts, currentparts, filenumber, partsize) VALUES (?, ?, ?, ?, 1, ?, ?)', - [$hash, $name, $collectionId, $totalParts, $fileNumber, $partSize] - ); + $affected = DB::table('binaries')->insertOrIgnore([ + 'binaryhash' => $hash, + 'name' => $name, + 'collections_id' => $collectionId, + 'totalparts' => $totalParts, + 'currentparts' => 1, + 'filenumber' => $fileNumber, + 'partsize' => $partSize, + ]); if ($affected > 0 && ($lastId = (int) DB::connection()->getPdo()->lastInsertId()) > 0) { $this->insertedBinaryIds[$lastId] = true; @@ -214,33 +219,21 @@ final class BinaryHandler private function flushUpdatesMysql(array $updates, int $chunkSize): bool { foreach (array_chunk($updates, $chunkSize) as $chunk) { - // Build a CASE statement for batch UPDATE instead of INSERT...ON DUPLICATE KEY - // This avoids FK constraint violations when collections have been deleted - $ids = []; - $partsizeCases = []; - $currentpartsCases = []; + $selects = []; $bindings = []; foreach ($chunk as $row) { - $ids[] = $row['id']; - $partsizeCases[] = 'WHEN id = ? THEN partsize + ?'; - $currentpartsCases[] = 'WHEN id = ? THEN currentparts + ?'; + $selects[] = 'SELECT ? AS id, ? AS partsize, ? AS currentparts'; $bindings[] = $row['id']; $bindings[] = $row['partsize']; - } - - foreach ($chunk as $row) { - $bindings[] = $row['id']; $bindings[] = $row['currentparts']; } - $idPlaceholders = implode(',', array_fill(0, count($ids), '?')); - $bindings = array_merge($bindings, $ids); - - $sql = 'UPDATE binaries SET ' - .'partsize = CASE '.implode(' ', $partsizeCases).' ELSE partsize END, ' - .'currentparts = CASE '.implode(' ', $currentpartsCases).' ELSE currentparts END ' - .'WHERE id IN ('.$idPlaceholders.')'; + $sql = 'UPDATE binaries b INNER JOIN (' + .implode(' UNION ALL ', $selects) + .') u ON u.id = b.id ' + .'SET b.partsize = b.partsize + u.partsize, ' + .'b.currentparts = b.currentparts + u.currentparts'; DB::statement($sql, $bindings); } diff --git a/tests/Feature/BinariesStorageInternalsTest.php b/tests/Feature/BinariesStorageInternalsTest.php index ed4a7fe7c..da189c7a5 100644 --- a/tests/Feature/BinariesStorageInternalsTest.php +++ b/tests/Feature/BinariesStorageInternalsTest.php @@ -68,6 +68,34 @@ class BinariesStorageInternalsTest extends TestCase $this->assertSame(1, DB::table('parts')->count()); } + public function test_binary_handler_flushes_cached_article_aggregate_updates(): void + { + DB::statement('CREATE TABLE binaries ( + id INTEGER PRIMARY KEY, + binaryhash BLOB, + name VARCHAR(255), + collections_id INT, + totalparts INT, + currentparts INT, + filenumber INT, + partsize INT, + UNIQUE(binaryhash, collections_id) + )'); + + $handler = new BinaryHandler; + $first = $this->parsedHeader(251, 1, 'Aggregate.Release', 100); + $second = $this->parsedHeader(252, 2, 'Aggregate.Release', 50); + + $binaryId = $handler->getOrCreateBinary($first, 1, 1, 0); + $this->assertNotNull($binaryId); + $this->assertSame($binaryId, $handler->getOrCreateBinary($second, 1, 1, 0)); + $this->assertTrue($handler->flushUpdates()); + + $binary = DB::table('binaries')->where('id', $binaryId)->first(); + $this->assertSame(2, (int) $binary->currentparts); + $this->assertSame(150, (int) $binary->partsize); + } + public function test_sqlite_rollback_cleanup_keeps_unrelated_parts_with_same_article_number(): void { DB::statement('CREATE TABLE collections (id INTEGER PRIMARY KEY, collectionhash VARCHAR(40), noise VARCHAR(64))'); @@ -102,7 +130,10 @@ class BinariesStorageInternalsTest extends TestCase $collectionHandler = new CollectionHandler(new class extends CollectionsCleaningService { - public function __construct() {} + public function __construct() + { + parent::__construct(); + } public function collectionsCleaner(string $subject, string $groupName = ''): array { @@ -160,13 +191,13 @@ class BinariesStorageInternalsTest extends TestCase subject VARCHAR(255), fromname VARCHAR(255), date DATETIME NULL, - xref TEXT DEFAULT "", + xref TEXT DEFAULT \'\', groups_id INT, totalfiles INT, collectionhash VARCHAR(40) UNIQUE, collection_regexes_id INT, dateadded DATETIME NULL, - noise VARCHAR(64) DEFAULT "" + noise VARCHAR(64) DEFAULT \'\' )'); DB::statement('CREATE TABLE binaries (