Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions lib/Db/AttachmentMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,34 @@ public function findByData(int $cardId, string $data): Attachment {
return $this->findEntity($qb);
}

/**
* Returns a map of cardId => attachment count for the given card IDs in a single query.
*
* @param int[] $cardIds
* @return array<int, int>
* @throws \OCP\DB\Exception
*/
public function findCountByCardIds(array $cardIds): array {
if (empty($cardIds)) {
return [];
}
$qb = $this->db->getQueryBuilder();
$qb->select('card_id')
->selectAlias($qb->func()->count('id'), 'attachment_count')
->from($this->getTableName())
->where($qb->expr()->in('card_id', $qb->createNamedParameter($cardIds, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
->groupBy('card_id');

$counts = [];
$cursor = $qb->executeQuery();
while ($row = $cursor->fetch()) {
$counts[(int)$row['card_id']] = (int)$row['attachment_count'];
}
$cursor->closeCursor();
return $counts;
}

/**
* @return Entity[]
* @throws \OCP\DB\Exception
Expand Down
10 changes: 6 additions & 4 deletions lib/Db/BoardMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,14 @@ public function findAllForUser(string $userId, ?int $since = null, bool $include
return $board->getId();
}, $allBoards));

// Pre-group ACLs by board ID
$aclsByBoard = [];
foreach ($acls as $acl) {
$aclsByBoard[$acl->getBoardId()][] = $acl;
}
/* @var Board $entry */
foreach ($allBoards as $entry) {
$boardAcls = array_values(array_filter($acls, function ($acl) use ($entry) {
return $acl->getBoardId() === $entry->getId();
}));
$entry->setAcl($boardAcls);
$entry->setAcl($aclsByBoard[$entry->getId()] ?? []);
}

foreach ($allBoards as $board) {
Expand Down
30 changes: 28 additions & 2 deletions lib/Db/CardMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ public function findCalendarEntries(int $boardId, ?int $limit = null, $offset =
return $this->findEntities($qb);
}

public function findAllArchived($stackId, $limit = null, $offset = null) {
public function findAllArchived(int $stackId, ?int $limit = null, ?int $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_cards')
Expand All @@ -250,7 +250,33 @@ public function findAllArchived($stackId, $limit = null, $offset = null) {
return $this->findEntities($qb);
}

public function findAllByStack($stackId, $limit = null, $offset = null) {
/**
* Batch-fetch all archived cards for multiple stacks in a single query.
*
* @param int[] $stackIds
* @return array<int, Card[]> Map of stackId => Card[]
* @throws \OCP\DB\Exception
*/
public function findAllArchivedForStacks(array $stackIds): array {
if (empty($stackIds)) {
return [];
}
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_cards')
->where($qb->expr()->in('stack_id', $qb->createNamedParameter($stackIds, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)))
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
->orderBy('last_modified');

$cards = array_fill_keys($stackIds, []);
foreach ($this->findEntities($qb) as $card) {
$cards[$card->getStackId()][] = $card;
}
return $cards;
}

public function findAllByStack(int $stackId, ?int $limit = null, ?int $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_cards')
Expand Down
24 changes: 24 additions & 0 deletions lib/Db/LabelMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ public function findAll(int $boardId, ?int $limit = null, int $offset = 0): arra
return $this->findEntities($qb);
}

/**
* Check if a label with the given title already exists on the board.
* Pass $excludeId to skip a specific label (used during updates).
*
* @throws \OCP\DB\Exception
*/
public function existsByBoardIdAndTitle(int $boardId, string $title, ?int $excludeId = null): bool {
$qb = $this->db->getQueryBuilder();
$qb->select('id')
->from($this->getTableName())
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('title', $qb->createNamedParameter($title, IQueryBuilder::PARAM_STR)))
->setMaxResults(1);

if ($excludeId !== null) {
$qb->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($excludeId, IQueryBuilder::PARAM_INT)));
}

$cursor = $qb->executeQuery();
$exists = $cursor->fetch() !== false;
$cursor->closeCursor();
return $exists;
}

public function delete(Entity $entity): Entity {
// delete assigned labels
$this->deleteLabelAssignments($entity->getId());
Expand Down
37 changes: 37 additions & 0 deletions lib/Db/StackMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,43 @@ public function find(int $id): Stack {
return $this->stackCache[(string)$id];
}

/**
* Fetch multiple stacks by their IDs in a single query.
*
* @param int[] $ids
* @return array<int, Stack> Map of stackId => Stack
* @throws \OCP\DB\Exception
*/
public function findByIds(array $ids): array {
if (empty($ids)) {
return [];
}

$stacks = [];
$uncachedIds = [];
foreach ($ids as $id) {
if (isset($this->stackCache[(string)$id])) {
$stacks[$id] = $this->stackCache[(string)$id];
} else {
$uncachedIds[] = $id;
}
}

if (!empty($uncachedIds)) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->in('id', $qb->createNamedParameter($uncachedIds, IQueryBuilder::PARAM_INT_ARRAY)));

foreach ($this->findEntities($qb) as $stack) {
$this->stackCache[(string)$stack->getId()] = $stack;
$stacks[$stack->getId()] = $stack;
}
}

return $stacks;
}

/**
* @throws \OCP\DB\Exception
*/
Expand Down
56 changes: 46 additions & 10 deletions lib/Service/AttachmentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,21 +137,57 @@ public function findAll(int $cardId, bool $withDeleted = false): array {
* @throws \OCP\DB\Exception
*/
public function count(int $cardId): int {
$count = $this->attachmentCacheHelper->getAttachmentCount($cardId);
if ($count === null) {
$count = count($this->attachmentMapper->findAll($cardId));

foreach (array_keys($this->services) as $attachmentType) {
$service = $this->getService($attachmentType);
if ($service instanceof ICustomAttachmentService) {
$count += $service->getAttachmentCount($cardId);
return ($this->countForCards([$cardId]))[$cardId] ?? 0;
}

/**
* Returns a map of cardId => attachment count for the given card IDs using batch queries.
*
* @param int[] $cardIds
* @return array<int, int>
* @throws \OCP\DB\Exception
*/
public function countForCards(array $cardIds): array {
if (empty($cardIds)) {
return [];
}

$counts = [];
$uncachedIds = [];
foreach ($cardIds as $cardId) {
$cached = $this->attachmentCacheHelper->getAttachmentCount($cardId);
if ($cached !== null) {
$counts[$cardId] = $cached;
} else {
$uncachedIds[] = $cardId;
$counts[$cardId] = 0;
}
}

if (empty($uncachedIds)) {
return $counts;
}

// Batch query for deck_file attachments stored in the deck_attachment table
foreach ($this->attachmentMapper->findCountByCardIds($uncachedIds) as $cardId => $count) {
$counts[$cardId] = $count;
}

// Add counts from custom attachment services (e.g. files shared via FilesAppService)
foreach (array_keys($this->services) as $attachmentType) {
$service = $this->getService($attachmentType);
if ($service instanceof ICustomAttachmentService) {
foreach ($service->getAttachmentCountForCards($uncachedIds) as $cardId => $count) {
$counts[$cardId] = ($counts[$cardId] ?? 0) + $count;
}
}
}

$this->attachmentCacheHelper->setAttachmentCount($cardId, $count);
foreach ($uncachedIds as $cardId) {
$this->attachmentCacheHelper->setAttachmentCount($cardId, $counts[$cardId]);
}

return $count;
return $counts;
}

/**
Expand Down
45 changes: 31 additions & 14 deletions lib/Service/BoardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -721,13 +721,14 @@ private function cloneCards(Board $board, Board $newBoard, bool $withAssignments
usort($stacks, $stackSorter);
usort($newStacks, $stackSorter);

$stackIds = array_map(fn (Stack $stack) => $stack->getId(), $stacks);
$activeCardsByStack = $this->cardMapper->findAllForStacks($stackIds);
$archivedCardsByStack = $this->cardMapper->findAllArchivedForStacks($stackIds);

$i = 0;
foreach ($stacks as $stack) {
$cards = $this->cardMapper->findAll($stack->getId());
$archivedCards = $this->cardMapper->findAllArchived($stack->getId());

/** @var Card[] $cards */
$cards = array_merge($cards, $archivedCards);
$cards = array_merge($activeCardsByStack[$stack->getId()] ?? [], $archivedCardsByStack[$stack->getId()] ?? []);

foreach ($cards as $card) {
$targetStackId = $moveCardsToLeftStack ? $newStacks[0]->getId() : $newStacks[$i]->getId();
Expand Down Expand Up @@ -830,22 +831,38 @@ private function clearBoardFromCache(Board $board): void {

private function enrichWithCards(Board $board): void {
$stacks = $this->stackMapper->findAll($board->getId());
if (\count($stacks) === 0) {
return;
}

$stackIds = array_map(fn (Stack $stack) => $stack->getId(), $stacks);

// Fetch all active cards for all stacks in one query
$cardsByStack = $this->cardMapper->findAllForStacks($stackIds);

$allCards = array_merge(...array_values(array_filter($cardsByStack)));
$allCardIds = array_map(fn (Card $card) => $card->getId(), $allCards);

// Batch-fetch labels and assigned users for all cards
$labelsByCard = [];
foreach ($this->labelMapper->findAssignedLabelsForCards($allCardIds) as $label) {
$labelsByCard[$label->getCardId()][] = $label;
}
$usersByCard = [];
foreach ($this->assignedUsersMapper->findIn($allCardIds) as $assignment) {
$usersByCard[$assignment->getCardId()][] = $assignment;
}

foreach ($stacks as $stack) {
$cards = $this->cardMapper->findAllByStack($stack->getId());
$fullCards = [];
foreach ($cards as $card) {
$fullCard = $this->cardMapper->find($card->getId());
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
$fullCard->setAssignedUsers($assignedUsers);
array_push($fullCards, $fullCard);
foreach ($cardsByStack[$stack->getId()] ?? [] as $card) {
$card->setLabels($labelsByCard[$card->getId()] ?? []);
$card->setAssignedUsers($usersByCard[$card->getId()] ?? []);
$fullCards[] = $card;
}
$stack->setCards($fullCards);
}

if (\count($stacks) === 0) {
return;
}

$board->setStacks($stacks);
}
}
35 changes: 22 additions & 13 deletions lib/Service/CardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@
use OCA\Deck\Activity\ChangeSet;
use OCA\Deck\BadRequestException;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\AssignmentMapper;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\Card;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\ChangeHelper;
use OCA\Deck\Db\Label;
use OCA\Deck\Db\LabelMapper;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\Event\CardCreatedEvent;
Expand Down Expand Up @@ -71,12 +69,19 @@ public function __construct(
public function enrichCards(array $cards): array {
$user = $this->userManager->get($this->userId);

$cardIds = array_map(function (Card $card) use ($user): int {
$allCardIds = array_map(fn (Card $card) => $card->getId(), $cards);
$attachmentCounts = $this->attachmentService->countForCards($allCardIds);

// Pre-fetch all stacks for this batch in one query and index by ID
$stackIds = array_unique(array_map(fn (Card $card) => $card->getStackId(), $cards));
$stacksById = $this->stackMapper->findByIds($stackIds);

$cardIds = array_map(function (Card $card) use ($user, $attachmentCounts, $stacksById): int {
// Everything done in here might be heavy as it is executed for every card
$cardId = $card->getId();
$this->cardMapper->mapOwner($card);

$card->setAttachmentCount($this->attachmentService->count($cardId));
$card->setAttachmentCount($attachmentCounts[$cardId] ?? 0);

// TODO We should find a better way just to get the comment count so we can save 1-3 queries per card here
$countComments = $this->commentsManager->getNumberOfCommentsForObject('deckCard', (string)$card->getId());
Expand All @@ -85,7 +90,7 @@ public function enrichCards(array $cards): array {
$card->setCommentsUnread($countUnreadComments);
$card->setCommentsCount($countComments);

$stack = $this->stackMapper->find($card->getStackId());
$stack = $stacksById[$card->getStackId()] ?? $this->stackMapper->find($card->getStackId());
$board = $this->boardService->find($stack->getBoardId(), false);
$card->setRelatedStack($stack);
$card->setRelatedBoard($board);
Expand All @@ -96,15 +101,19 @@ public function enrichCards(array $cards): array {
$assignedLabels = $this->labelMapper->findAssignedLabelsForCards($cardIds);
$assignedUsers = $this->assignedUsersMapper->findIn($cardIds);

// Pre-group labels and users by card ID
$labelsByCard = [];
foreach ($assignedLabels as $label) {
$labelsByCard[$label->getCardId()][] = $label;
}
$usersByCard = [];
foreach ($assignedUsers as $assignment) {
$usersByCard[$assignment->getCardId()][] = $assignment;
}

foreach ($cards as $card) {
$cardLabels = array_values(array_filter($assignedLabels, function (Label $label) use ($card) {
return $label->getCardId() === $card->getId();
}));
$cardAssignedUsers = array_values(array_filter($assignedUsers, function (Assignment $assignment) use ($card) {
return $assignment->getCardId() === $card->getId();
}));
$card->setLabels($cardLabels);
$card->setAssignedUsers($cardAssignedUsers);
$card->setLabels($labelsByCard[$card->getId()] ?? []);
$card->setAssignedUsers($usersByCard[$card->getId()] ?? []);
}

return array_map(
Expand Down
Loading
Loading