collectionHandler = $collectionHandler; $this->binaryHandler = $binaryHandler; $this->batchNoise = bin2hex(random_bytes(8)); } /** * Get the batch noise marker for this transaction. */ public function getBatchNoise(): string { return $this->batchNoise; } /** * Start a new database transaction. */ public function begin(): void { DB::beginTransaction(); $this->hadErrors = false; } /** * Mark that an error occurred. */ public function markError(): void { $this->hadErrors = true; } /** * Check if errors occurred. */ public function hasErrors(): bool { return $this->hadErrors; } /** * Commit the transaction if no errors, rollback otherwise. */ public function finish(): bool { if ($this->hadErrors) { $this->rollbackAndCleanup(); return false; } try { DB::commit(); return true; } catch (\Throwable $e) { $this->rollbackAndCleanup(); if (config('app.debug') === true) { Log::error('HeaderStorageTransaction commit failed: '.$e->getMessage()); } return false; } } /** * Perform rollback and cleanup any orphaned data. */ private function rollbackAndCleanup(): void { try { DB::rollBack(); } catch (\Throwable $e) { // Already rolled back } // InnoDB rollback already reverts all rows written by this transaction. // Manual cleanup is retained only for SQLite test/in-memory scenarios. if (DB::getDriverName() === 'sqlite') { $this->cleanup(); } } /** * Cleanup rows that may have been inserted before rollback. */ private function cleanup(): void { try { $this->cleanupParts(); $this->cleanupBinaries(); $this->cleanupCollections(); } catch (\Throwable $e) { if (config('app.debug') === true) { Log::warning('Post-rollback cleanup failed: '.$e->getMessage()); } } } private function cleanupParts(): void { $ids = $this->binaryHandler->getInsertedIds(); foreach (array_chunk($ids, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) { $placeholders = implode(',', array_fill(0, \count($chunk), '?')); DB::statement("DELETE FROM parts WHERE binaries_id IN ({$placeholders})", $chunk); } } private function cleanupBinaries(): void { $ids = $this->binaryHandler->getInsertedIds(); foreach (array_chunk($ids, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) { $placeholders = implode(',', array_fill(0, \count($chunk), '?')); DB::statement("DELETE FROM binaries WHERE id IN ({$placeholders})", $chunk); } } private function cleanupCollections(): void { $insertedIds = $this->collectionHandler->getInsertedIds(); $allIds = $this->collectionHandler->getAllIds(); $hashes = $this->collectionHandler->getBatchHashes(); $ids = ! empty($insertedIds) ? $insertedIds : $allIds; if (! empty($ids)) { foreach (array_chunk($ids, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) { $placeholders = implode(',', array_fill(0, \count($chunk), '?')); DB::statement( "DELETE FROM parts WHERE binaries_id IN (SELECT id FROM binaries WHERE collections_id IN ({$placeholders}))", $chunk ); DB::statement("DELETE FROM binaries WHERE collections_id IN ({$placeholders})", $chunk); DB::statement("DELETE FROM collections WHERE id IN ({$placeholders})", $chunk); } } elseif (! empty($hashes)) { foreach (array_chunk($hashes, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) { $placeholders = implode(',', array_fill(0, \count($chunk), '?')); DB::statement( "DELETE FROM parts WHERE binaries_id IN (SELECT id FROM binaries WHERE collections_id IN (SELECT id FROM collections WHERE collectionhash IN ({$placeholders})))", $chunk ); DB::statement( "DELETE FROM binaries WHERE collections_id IN (SELECT id FROM collections WHERE collectionhash IN ({$placeholders}))", $chunk ); DB::statement("DELETE FROM collections WHERE collectionhash IN ({$placeholders})", $chunk); } } else { // Fallback by noise marker DB::statement( 'DELETE FROM parts WHERE binaries_id IN (SELECT b.id FROM binaries b WHERE b.collections_id IN (SELECT c.id FROM collections c WHERE c.noise = ?))', [$this->batchNoise] ); DB::statement( 'DELETE FROM binaries WHERE collections_id IN (SELECT id FROM collections WHERE noise = ?)', [$this->batchNoise] ); DB::statement('DELETE FROM collections WHERE noise = ?', [$this->batchNoise]); } } }