Skip to content

Commit bb81f5a

Browse files
authored
Merge pull request #2468 from nextcloud/fix/long-page-load
perf(WebViewController): Improve countDuplicated performance
2 parents 9310ddf + c415f67 commit bb81f5a

2 files changed

Lines changed: 63 additions & 15 deletions

File tree

lib/Db/BookmarkMapper.php

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,9 @@ private function _filterDuplicated(IQueryBuilder $qb, QueryParameters $params) {
491491
->from('folder_tree', 'trdup')
492492
->where($subQuery->expr()->eq('b.id', 'trdup.item_id'))
493493
->andWhere($subQuery->expr()->neq('trdup.parent_folder', 'tree.parent_folder'))
494-
->andWhere($subQuery->expr()->eq('trdup.type', $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK)));
494+
->andWhere($subQuery->expr()->eq('trdup.type', $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK)))
495+
// Only live copies make a bookmark a duplicate; a trashed copy in another folder doesn't count.
496+
->andWhere($subQuery->expr()->isNull('trdup.soft_deleted_at'));
495497
$qb->andWhere($qb->createFunction('EXISTS(' . $subQuery->getSQL() . ')'));
496498
}
497499
}
@@ -710,25 +712,19 @@ public function countUnavailable(string $userId): int {
710712
* @throws Exception
711713
*/
712714
public function countDuplicated(string $userId): int {
713-
// Count duplicates the exact same way the "Duplicated" list is computed in findAll():
714-
// against the recursive folder_tree CTE (which covers nested folders and bookmarks inside
715-
// shared folders/subfolders) and using the same _filterDuplicated() predicate. Hand-rolled
716-
// queries against the raw bookmarks_tree table miss everything that only becomes visible
717-
// through the recursive expansion, which made this method under-count.
718715
$rootFolder = $this->folderMapper->findRootFolder($userId);
719716
[$cte, $params, $paramTypes] = $this->generateCTE($rootFolder->getId(), false);
720717

721718
$qb = $this->db->getQueryBuilder();
722719
$qb->automaticTablePrefix(false);
723-
$qb->select($qb->createFunction('COUNT(DISTINCT b.id)'))
724-
->from('*PREFIX*bookmarks', 'b')
725-
->innerJoin('b', 'folder_tree', 'tree', 'tree.item_id = b.id AND tree.type = ' . $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK) . ' AND tree.soft_deleted_at is NULL');
726-
727-
$queryParams = new QueryParameters();
728-
$queryParams->setDuplicated(true);
729-
$this->_filterDuplicated($qb, $queryParams);
730-
731-
$finalQuery = $cte . ' ' . $qb->getSQL();
720+
$qb->select('tree.item_id')
721+
->from('folder_tree', 'tree')
722+
->where($qb->expr()->eq('tree.type', $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK)))
723+
->andWhere($qb->expr()->isNull('tree.soft_deleted_at'))
724+
->groupBy('tree.item_id')
725+
->having('COUNT(DISTINCT tree.parent_folder) > 1');
726+
727+
$finalQuery = $cte . ' SELECT COUNT(*) FROM (' . $qb->getSQL() . ') dup';
732728
$params = array_merge($params, $qb->getParameters());
733729
$paramTypes = array_merge($paramTypes, $qb->getParameterTypes());
734730

tests/BookmarkMapperTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,58 @@ public function testCountDuplicatedInSubfolderOfSharedFolder() {
227227
$this->assertCount(1, $duplicatedList);
228228
}
229229

230+
/**
231+
* A bookmark that lives in two folders but whose second copy has been trashed is
232+
* NOT a duplicate: only live copies count. countDuplicated() and the "Duplicated"
233+
* list (findAll + setDuplicated) must both ignore the trashed copy and agree.
234+
*
235+
* @throws AlreadyExistsError
236+
* @throws UserLimitExceededError
237+
* @throws UrlParseError
238+
* @throws DoesNotExistException
239+
* @throws MultipleObjectsReturnedException
240+
*/
241+
public function testCountDuplicatedIgnoresTrashedCopies() {
242+
$rootFolderId = $this->folderMapper->findRootFolder($this->userId)->getId();
243+
244+
$folderA = new Db\Folder();
245+
$folderA->setTitle('dup-a');
246+
$folderA->setUserId($this->userId);
247+
$this->folderMapper->insert($folderA);
248+
$this->treeMapper->move(Db\TreeMapper::TYPE_FOLDER, $folderA->getId(), $rootFolderId);
249+
250+
$folderB = new Db\Folder();
251+
$folderB->setTitle('dup-b');
252+
$folderB->setUserId($this->userId);
253+
$this->folderMapper->insert($folderB);
254+
$this->treeMapper->move(Db\TreeMapper::TYPE_FOLDER, $folderB->getId(), $rootFolderId);
255+
256+
// One bookmark placed in both folders -> duplicated while both copies are live.
257+
$bookmark = Db\Bookmark::fromArray([
258+
'userId' => $this->userId,
259+
'url' => 'https://example.org/duplicated-then-trashed',
260+
'title' => 'Trashed duplicate',
261+
'description' => '',
262+
]);
263+
$bookmark = $this->bookmarkMapper->insertOrUpdate($bookmark);
264+
$this->treeMapper->addToFolders(
265+
Db\TreeMapper::TYPE_BOOKMARK,
266+
$bookmark->getId(),
267+
[$folderA->getId(), $folderB->getId()],
268+
);
269+
270+
$this->assertSame(1, $this->bookmarkMapper->countDuplicated($this->userId));
271+
272+
// Trash the copy in folder B -> only one live copy remains -> not a duplicate.
273+
$this->treeMapper->softDeleteEntry(Db\TreeMapper::TYPE_BOOKMARK, $bookmark->getId(), $folderB->getId());
274+
275+
$this->assertSame(0, $this->bookmarkMapper->countDuplicated($this->userId));
276+
277+
$params = new \OCA\Bookmarks\QueryParameters();
278+
$duplicatedList = $this->bookmarkMapper->findAll($this->userId, $params->setDuplicated(true));
279+
$this->assertCount(0, $duplicatedList);
280+
}
281+
230282
/**
231283
* Regression test: countUnavailable() must count an unavailable bookmark living
232284
* in a subfolder of a shared folder, for the sharee. The old implementation only

0 commit comments

Comments
 (0)