Skip to content

Commit 0f8e0cd

Browse files
committed
fix(BookmarkMapper): Fix click count methods to use recursive CTE
Signed-off-by: Marcel Klehr <mklehr@gmx.net>
1 parent 4db81d8 commit 0f8e0cd

1 file changed

Lines changed: 39 additions & 40 deletions

File tree

lib/Db/BookmarkMapper.php

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -612,30 +612,30 @@ public function countArchived(string $userId): int {
612612
* @throws Exception
613613
*/
614614
public function countAllClicks(string $userId): int {
615-
$qb = $this->db->getQueryBuilder();
616-
$qb->selectAlias($qb->func()->sum('b.clickcount'), 'count');
617-
$qb
618-
->from('bookmarks', 'b')
619-
->innerJoin('b', 'bookmarks_tree', 'tr', 'b.id = tr.id AND tr.type = ' . $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK) . ' AND tr.soft_deleted_at is NULL')
620-
->where($qb->expr()->eq('b.user_id', $qb->createPositionalParameter($userId)))
621-
->andWhere($qb->expr()->neq('b.clickcount', $qb->createPositionalParameter(0, IQueryBuilder::PARAM_INT)));
622-
$result = $qb->executeQuery();
623-
$userOwnerClickCount = $result->fetch(PDO::FETCH_COLUMN);
624-
$result->closeCursor();
615+
// Sum clicks across the recursive folder_tree CTE so that bookmarks nested in subfolders of
616+
// shared folders are included. Because the CTE yields one row per (bookmark, reachable
617+
// parent_folder), we first deduplicate to one row per bookmark and then sum, so a bookmark
618+
// reachable through several folders doesn't have its clicks counted more than once.
619+
// Hand-rolled queries against the raw bookmarks_tree table missed the nested case entirely.
620+
$rootFolder = $this->folderMapper->findRootFolder($userId);
621+
[$cte, $params, $paramTypes] = $this->generateCTE($rootFolder->getId(), false);
625622

626623
$qb = $this->db->getQueryBuilder();
627-
$qb->selectAlias($qb->func()->sum('b.clickcount'), 'count');
628-
$qb
629-
->from('bookmarks', 'b')
630-
->innerJoin('b', 'bookmarks_tree', 'tr', 'b.id = tr.id AND tr.type = ' . $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK) . ' AND tr.soft_deleted_at is NULL')
631-
->innerJoin('tr', 'bookmarks_shared_folders', 'sf', $qb->expr()->eq('tr.parent_folder', 'sf.folder_id'))
632-
->where($qb->expr()->eq('sf.user_id', $qb->createPositionalParameter($userId)))
633-
->andWhere($qb->expr()->neq('b.clickcount', $qb->createPositionalParameter(0, IQueryBuilder::PARAM_INT)));
634-
$result = $qb->executeQuery();
635-
$foreignClickCount = $result->fetch(PDO::FETCH_COLUMN);
624+
$qb->automaticTablePrefix(false);
625+
$qb->selectDistinct(['b.id', 'b.clickcount'])
626+
->from('*PREFIX*bookmarks', 'b')
627+
->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')
628+
->where($qb->expr()->neq('b.clickcount', $qb->createPositionalParameter(0, IQueryBuilder::PARAM_INT)));
629+
630+
$finalQuery = $cte . ' SELECT COALESCE(SUM(sub.clickcount), 0) FROM (' . $qb->getSQL() . ') sub';
631+
$params = array_merge($params, $qb->getParameters());
632+
$paramTypes = array_merge($paramTypes, $qb->getParameterTypes());
633+
634+
$result = $this->db->executeQuery($finalQuery, $params, $paramTypes);
635+
$count = (int)$result->fetchOne();
636636
$result->closeCursor();
637637

638-
return $userOwnerClickCount + $foreignClickCount;
638+
return $count;
639639
}
640640

641641
/**
@@ -644,30 +644,29 @@ public function countAllClicks(string $userId): int {
644644
* @throws Exception
645645
*/
646646
public function countWithClicks(string $userId): int {
647-
$qb = $this->db->getQueryBuilder();
648-
$qb->selectAlias($qb->func()->count('b.id'), 'count');
649-
$qb
650-
->from('bookmarks', 'b')
651-
->innerJoin('b', 'bookmarks_tree', 'tr', 'b.id = tr.id AND tr.type = ' . $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK) . ' AND tr.soft_deleted_at is NULL')
652-
->where($qb->expr()->eq('b.user_id', $qb->createPositionalParameter($userId)))
653-
->andWhere($qb->expr()->neq('b.clickcount', $qb->createPositionalParameter(0, IQueryBuilder::PARAM_INT)));
654-
$result = $qb->executeQuery();
655-
$userOwnerWithClicksCount = $result->fetch(PDO::FETCH_COLUMN);
656-
$result->closeCursor();
647+
// Count clicked bookmarks against the recursive folder_tree CTE so that bookmarks nested in
648+
// subfolders of shared folders are included, and count each bookmark once (COUNT DISTINCT).
649+
// Hand-rolled queries against the raw bookmarks_tree table miss everything that only becomes
650+
// visible through the recursive expansion, which made this method under-count.
651+
$rootFolder = $this->folderMapper->findRootFolder($userId);
652+
[$cte, $params, $paramTypes] = $this->generateCTE($rootFolder->getId(), false);
657653

658654
$qb = $this->db->getQueryBuilder();
659-
$qb->selectAlias($qb->func()->count('b.id'), 'count');
660-
$qb
661-
->from('bookmarks', 'b')
662-
->innerJoin('b', 'bookmarks_tree', 'tr', 'b.id = tr.id AND tr.type = ' . $qb->createPositionalParameter(TreeMapper::TYPE_BOOKMARK) . ' AND tr.soft_deleted_at is NULL')
663-
->innerJoin('tr', 'bookmarks_shared_folders', 'sf', $qb->expr()->eq('tr.parent_folder', 'sf.folder_id'))
664-
->where($qb->expr()->eq('sf.user_id', $qb->createPositionalParameter($userId)))
665-
->andWhere($qb->expr()->neq('b.clickcount', $qb->createPositionalParameter(0, IQueryBuilder::PARAM_INT)));
666-
$result = $qb->executeQuery();
667-
$foreignWithClicksCount = $result->fetch(PDO::FETCH_COLUMN);
655+
$qb->automaticTablePrefix(false);
656+
$qb->select($qb->createFunction('COUNT(DISTINCT b.id)'))
657+
->from('*PREFIX*bookmarks', 'b')
658+
->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')
659+
->where($qb->expr()->neq('b.clickcount', $qb->createPositionalParameter(0, IQueryBuilder::PARAM_INT)));
660+
661+
$finalQuery = $cte . ' ' . $qb->getSQL();
662+
$params = array_merge($params, $qb->getParameters());
663+
$paramTypes = array_merge($paramTypes, $qb->getParameterTypes());
664+
665+
$result = $this->db->executeQuery($finalQuery, $params, $paramTypes);
666+
$count = (int)$result->fetchOne();
668667
$result->closeCursor();
669668

670-
return $userOwnerWithClicksCount + $foreignWithClicksCount;
669+
return $count;
671670
}
672671

673672
/**

0 commit comments

Comments
 (0)