From 51e734d5ba5da4e1fb39adccbabf7fb5ffd29cc6 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 27 Jul 2026 17:22:41 +0200 Subject: [PATCH] fix: don't rely on constraint for filecache_extended "upsert" when in transaction Signed-off-by: Robin Appelman --- lib/private/Files/Cache/Cache.php | 108 ++++++++++++++++++------------ 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 9039c2317394a..7db9ac1e281cd 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -322,17 +322,7 @@ public function insert($file, array $data) { if ($builder->executeStatement()) { $fileId = $builder->getLastInsertId(); - if (count($extensionValues)) { - $query = $this->getQueryBuilder(); - $query->insert('filecache_extended'); - $query->hintShardKey('storage', $storageId); - - $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)); - foreach ($extensionValues as $column => $value) { - $query->setValue($column, $query->createNamedParameter($value)); - } - $query->executeStatement(); - } + $this->setExtensionValues($fileId, $extensionValues, true); $event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId); $this->eventDispatcher->dispatch(CacheEntryInsertedEvent::class, $event); @@ -400,47 +390,81 @@ public function update($id, array $data) { $query->executeStatement(); } - if (count($extensionValues)) { - try { - $query = $this->getQueryBuilder(); - $query->insert('filecache_extended'); - $query->hintShardKey('storage', $this->getNumericStorageId()); + $this->setExtensionValues($id, $extensionValues, false); - $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)); - foreach ($extensionValues as $column => $value) { - $query->setValue($column, $query->createNamedParameter($value)); - } + $path = $this->getPathById($id); + // path can still be null if the file doesn't exist + if ($path !== null) { + $event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId()); + $this->eventDispatcher->dispatchTyped($event); + } + } - $query->executeStatement(); + private function hasExtensionValues(int $id) { + $query = $this->getQueryBuilder(); + $query->select('fileid') + ->from('filecache_extended') + ->whereFileId($id); + + return $query->executeQuery()->fetchOne() !== false; + } + + private function setExtensionValues(int $id, array $extensionValues, bool $newFile) { + if (!$extensionValues) { + return; + } + + // a failed insert in a transaction aborts the transactions on some platforms, so we can't rely on + // that behavior to to an "upsert" + // for new files, we can safely assume that there won't be a conflict, since the fileid is new + if (!$newFile && $this->connection->inTransaction()) { + if ($this->hasExtensionValues($id)) { + $this->updateExtensionValues($id, $extensionValues); + } else { + $this->insertExtensionValues($id, $extensionValues); + } + } else { + try { + $this->insertExtensionValues($id, $extensionValues); } catch (Exception $e) { if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { throw $e; } - $query = $this->getQueryBuilder(); - $query->update('filecache_extended') - ->whereFileId($id) - ->hintShardKey('storage', $this->getNumericStorageId()) - ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) { - return $query->expr()->orX( - $query->expr()->neq($key, $query->createNamedParameter($value)), - $query->expr()->isNull($key) - ); - }, array_keys($extensionValues), array_values($extensionValues)))); - - foreach ($extensionValues as $key => $value) { - $query->set($key, $query->createNamedParameter($value)); - } - - $query->executeStatement(); + $this->updateExtensionValues($id, $extensionValues); } } + } - $path = $this->getPathById($id); - // path can still be null if the file doesn't exist - if ($path !== null) { - $event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId()); - $this->eventDispatcher->dispatchTyped($event); + private function insertExtensionValues(int $id, array $extensionValues) { + $query = $this->getQueryBuilder(); + $query->insert('filecache_extended'); + $query->hintShardKey('storage', $this->getNumericStorageId()); + + $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)); + foreach ($extensionValues as $column => $value) { + $query->setValue($column, $query->createNamedParameter($value)); } + + $query->executeStatement(); + } + + private function updateExtensionValues(int $id, array $extensionValues) { + $query = $this->getQueryBuilder(); + $query->update('filecache_extended') + ->whereFileId($id) + ->hintShardKey('storage', $this->getNumericStorageId()) + ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) { + return $query->expr()->orX( + $query->expr()->neq($key, $query->createNamedParameter($value)), + $query->expr()->isNull($key) + ); + }, array_keys($extensionValues), array_values($extensionValues)))); + + foreach ($extensionValues as $key => $value) { + $query->set($key, $query->createNamedParameter($value)); + } + + $query->executeStatement(); } /**