diff --git a/lib/Db/Poll.php b/lib/Db/Poll.php index fb9f56277..7cb01e5fa 100644 --- a/lib/Db/Poll.php +++ b/lib/Db/Poll.php @@ -78,8 +78,15 @@ * @method void setLastInteraction(int $value) * @method string getMiscSettings() * @method void setMiscSettings(string $value) + * + * Magic functions for joined columns * @method int getMinDate() * @method int getMaxDate() + * + * Magic functions for subqueried columns + * @method int getCurrentUserCountOrphanedVotes() + * @method int getCurrentUserCountVotes() + * @method int getCurrentUserCountVotesYes() */ class Poll extends EntityWithUser implements JsonSerializable { @@ -105,7 +112,6 @@ class Poll extends EntityWithUser implements JsonSerializable { private IURLGenerator $urlGenerator; protected UserMapper $userMapper; - private VoteMapper $voteMapper; // schema columns public $id = null; @@ -132,12 +138,15 @@ class Poll extends EntityWithUser implements JsonSerializable { protected ?string $miscSettings = ''; // joined columns - protected bool $hasOrphanedVotes = false; + protected ?int $isCurrentUserLocked = 0; protected int $maxDate = 0; protected int $minDate = 0; - protected int $currentUserVotes = 0; protected string $userRole = "none"; - protected ?int $isCurrentUserLocked = 0; + + // subqueried columns + protected int $currentUserCountOrphanedVotes = 0; + protected int $currentUserCountVotes = 0; + protected int $currentUserCountVotesYes = 0; public function __construct() { $this->addType('created', 'int'); @@ -153,12 +162,19 @@ public function __construct() { $this->addType('hideBookedUp', 'int'); $this->addType('useNo', 'int'); $this->addType('lastInteraction', 'int'); + + // joined columns + $this->addType('isCurrentUserLocked', 'int'); $this->addType('maxDate', 'int'); $this->addType('minDate', 'int'); - $this->addType('currentUserVotes', 'int'); + + // subqueried columns + $this->addType('currentUserCountVotes', 'int'); + $this->addType('currentUserCountVotesYes', 'int'); + $this->addType('currentUserCountOrphanedVotes', 'int'); + $this->urlGenerator = Container::queryClass(IURLGenerator::class); $this->userMapper = Container::queryClass(UserMapper::class); - $this->voteMapper = Container::queryClass(VoteMapper::class); } /** @@ -191,10 +207,10 @@ public function jsonSerialize(): array { 'voteLimit' => $this->getVoteLimit(), 'lastInteraction' => $this->getLastInteraction(), 'summary' => [ - 'orphanedVotes' => $this->getCurrentUserOrphanedVotes(), - 'yesByCurrentUser' => $this->getCurrentUserYesVotes(), - 'countVotes' => $this->getCurrentUserCountVotes(), 'userRole' => $this->getUserRole(), + 'orphanedVotes' => $this->getCurrentUserCountOrphanedVotes(), + 'yesByCurrentUser' => $this->getCurrentUserCountVotesYes(), + 'countVotes' => $this->getCurrentUserCountVotes(), ], ]; } @@ -342,26 +358,8 @@ public function getRelevantThresholdNet(): int { ); } - public function getCurrentUserCountVotes(): int { - return $this->currentUserVotes; - } - public function getIsCurrentUserLocked(): bool { - return (bool) $this->isCurrentUserLocked; - } - - /** - * @psalm-return int<0, max> - */ - public function getCurrentUserOrphanedVotes(): int { - return count($this->voteMapper->findOrphanedByPollandUser($this->id, $this->userMapper->getCurrentUserCached()->getId())); - } - - /** - * @psalm-return int<0, max> - */ - public function getCurrentUserYesVotes(): int { - return count($this->voteMapper->getYesVotesByParticipant($this->getPollId(), $this->userMapper->getCurrentUserCached()->getId())); + return boolval($this->isCurrentUserLocked); } public function getDeadline(): int { diff --git a/lib/Db/PollMapper.php b/lib/Db/PollMapper.php index 12726301f..7ca975135 100644 --- a/lib/Db/PollMapper.php +++ b/lib/Db/PollMapper.php @@ -27,6 +27,7 @@ namespace OCA\Polls\Db; use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IParameter; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\Search\ISearchQuery; @@ -174,21 +175,24 @@ protected function buildQuery(): IQueryBuilder { $qb = $this->db->getQueryBuilder(); $qb->select(self::TABLE . '.*') - // TODO: check if this is necessary, in case of empty table to avoid possibly nulled columns - // ->groupBy(self::TABLE . '.id') - ->from($this->getTableName(), self::TABLE); + ->from($this->getTableName(), self::TABLE) + ->groupBy(self::TABLE . '.id'); + + $paramUser = $qb->createNamedParameter($currentUserId, IQueryBuilder::PARAM_STR); + $paramAnswerYes = $qb->createNamedParameter(Vote::VOTE_YES, IQueryBuilder::PARAM_STR); + + $qb->selectAlias($qb->createFunction('(' . $this->subQueryVotesCount(self::TABLE, $paramUser)->getSQL() . ')'), 'current_user_count_votes'); + $qb->selectAlias($qb->createFunction('(' . $this->subQueryVotesCount(self::TABLE, $paramUser, $paramAnswerYes)->getSQL() . ')'), 'current_user_count_votes_yes'); + $qb->selectAlias($qb->createFunction('(' . $this->subQueryOrphanedVotesCount(self::TABLE, $paramUser)->getSQL() . ')'), 'current_user_count_orphaned_votes'); + $this->joinOptionsForMaxDate($qb, self::TABLE); - $this->joinCurrentUserVotes($qb, self::TABLE, $currentUserId); $this->joinUserRole($qb, self::TABLE, $currentUserId); - $qb->groupBy(self::TABLE . '.id'); + return $qb; } /** - * Joins options to evaluate min and max option date for date polls - * if text poll or no options are set, - * the min value is the current time, - * the max value is null + * Joins shares to evaluate user role */ protected function joinUserRole(IQueryBuilder &$qb, string $fromAlias, string $currentUserId): void { $joinAlias = 'shares'; @@ -201,6 +205,7 @@ protected function joinUserRole(IQueryBuilder &$qb, string $fromAlias, string $c $qb->expr()->andX( $qb->expr()->eq($fromAlias . '.id', $joinAlias . '.poll_id'), $qb->expr()->eq($joinAlias . '.user_id', $qb->createNamedParameter($currentUserId, IQueryBuilder::PARAM_STR)), + $qb->expr()->eq($joinAlias . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT)), ) ); } @@ -226,26 +231,54 @@ protected function joinOptionsForMaxDate(IQueryBuilder &$qb, string $fromAlias): ); } + + /** - * Joins options to evaluate min and max option date for date polls - * if text poll or no options are set, - * the min value is the current time, - * the max value is null + * Subquery for votes count */ - protected function joinCurrentUserVotes(IQueryBuilder &$qb, string $fromAlias, $currentUserId): void { - $joinAlias = 'user_vote'; - // force value into a MIN function to avoid grouping errors - $qb->selectAlias($qb->func()->count($joinAlias . '.vote_answer'), 'current_user_votes'); + protected function subQueryVotesCount(string $fromAlias, IParameter $currentUserId, ?IParameter $answerFilter = null): IQueryBuilder { + $subAlias = 'user_vote_sub'; - $qb->leftJoin( - $fromAlias, - Vote::TABLE, - $joinAlias, - $qb->expr()->andX( - $qb->expr()->eq($joinAlias . '.poll_id', $fromAlias . '.id'), - $qb->expr()->eq($joinAlias . '.user_id', $qb->createNamedParameter($currentUserId, IQueryBuilder::PARAM_STR)), + $subQuery = $this->db->getQueryBuilder(); + $subQuery->select($subQuery->func()->count($subAlias . '.vote_answer')) + ->from(Vote::TABLE, $subAlias) + ->where($subQuery->expr()->eq($subAlias . '.poll_id', $fromAlias . '.id')) + ->andWhere($subQuery->expr()->eq($subAlias . '.user_id', $currentUserId)); + + // filter by answer + if ($answerFilter) { + $subQuery->andWhere($subQuery->expr()->eq($subAlias . '.vote_answer', $answerFilter)); + } + + return $subQuery; + } + + /** + * Subquery for count of orphaned votes + */ + protected function subQueryOrphanedVotesCount(string $fromAlias, IParameter $currentUserId): IQueryBuilder { + $subAlias = 'user_vote_sub'; + $subJoinAlias = 'vote_options_join'; + + // use subQueryVotesCount as base query + $subQuery = $this->subQueryVotesCount($fromAlias, $currentUserId); + + // superseed select, group result by voteId and add an additional condition + $subQuery->select($subQuery->func()->count($subAlias . '.vote_answer')) + ->andWhere($subQuery->expr()->isNull($subJoinAlias . '.id')); + + // join options to restrict query to votes with actually undeleted options + $subQuery->leftJoin( + $subAlias, + Option::TABLE, + $subJoinAlias, + $subQuery->expr()->andX( + $subQuery->expr()->eq($subJoinAlias . '.poll_id', $subAlias . '.poll_id'), + $subQuery->expr()->eq($subJoinAlias . '.poll_option_text', $subAlias . '.vote_option_text'), + $subQuery->expr()->eq($subJoinAlias . '.deleted', $subQuery->expr()->literal(0, IQueryBuilder::PARAM_INT)), ) ); + return $subQuery; } } diff --git a/lib/Db/VoteMapper.php b/lib/Db/VoteMapper.php index 85119df66..442b0685a 100644 --- a/lib/Db/VoteMapper.php +++ b/lib/Db/VoteMapper.php @@ -134,32 +134,6 @@ public function deleteByPollAndUserId(int $pollId, string $userId): void { $qb->executeStatement(); } - /** - * @throws \OCP\AppFramework\Db\DoesNotExistException if not found - * @return Vote[] - * @psalm-return array - */ - public function getYesVotesByParticipant(int $pollId, string $userId): array { - $qb = $this->buildQuery(); - $qb->andWhere($qb->expr()->eq(self::TABLE . '.poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq(self::TABLE . '.user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))) - ->andWhere($qb->expr()->eq(self::TABLE . '.vote_answer', $qb->createNamedParameter(Vote::VOTE_YES, IQueryBuilder::PARAM_STR))); - return $this->findEntities($qb); - } - - /** - * @throws \OCP\AppFramework\Db\DoesNotExistException if not found - * @return Vote[] - * @psalm-return array - */ - public function getYesVotesByOption(int $pollId, string $pollOptionText): array { - $qb = $this->buildQuery(); - $qb->andWhere($qb->expr()->eq(self::TABLE . '.poll_id', $qb->createNamedParameter($pollId, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq(self::TABLE . '.vote_option_text', $qb->createNamedParameter($pollOptionText, IQueryBuilder::PARAM_STR))) - ->andWhere($qb->expr()->eq(self::TABLE . '.vote_answer', $qb->createNamedParameter(Vote::VOTE_YES, IQueryBuilder::PARAM_STR))); - return $this->findEntities($qb); - } - public function renameUserId(string $userId, string $replacementName): void { $query = $this->db->getQueryBuilder(); $query->update($this->getTableName()) @@ -253,7 +227,7 @@ protected function joinOption(IQueryBuilder &$qb, string $fromAlias): string { $qb->expr()->andX( $qb->expr()->eq($joinAlias . '.poll_id', $fromAlias . '.poll_id'), $qb->expr()->eq($joinAlias . '.poll_option_text', $fromAlias . '.vote_option_text'), - $qb->expr()->eq($joinAlias . '.deleted', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq($joinAlias . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT)), ) );