@@ -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