Skip to content

Commit

Permalink
Merge pull request #3480 from nextcloud/backport/3477/stable-6
Browse files Browse the repository at this point in the history
[stable-6] Count user's votes by subquery
  • Loading branch information
come-nc committed May 14, 2024
2 parents 5d8c2c5 + 81c0fa2 commit dd20c6b
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 79 deletions.
54 changes: 26 additions & 28 deletions lib/Db/Poll.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -105,7 +112,6 @@ class Poll extends EntityWithUser implements JsonSerializable {

private IURLGenerator $urlGenerator;
protected UserMapper $userMapper;
private VoteMapper $voteMapper;

// schema columns
public $id = null;
Expand All @@ -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');
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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(),
],
];
}
Expand Down Expand Up @@ -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 {
Expand Down
81 changes: 57 additions & 24 deletions lib/Db/PollMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand All @@ -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)),
)
);
}
Expand All @@ -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;
}

}
28 changes: 1 addition & 27 deletions lib/Db/VoteMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array-key, Vote>
*/
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<array-key, Vote>
*/
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())
Expand Down Expand Up @@ -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)),
)
);

Expand Down

0 comments on commit dd20c6b

Please sign in to comment.