Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion src/contracts/Persistence/Content/Location/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public function loadParentLocationsForDraftContent($contentId);
*/
public function copySubtree($sourceId, $destinationParentId);

public function getSubtreeSize(string $path): int;
public function getSubtreeSize(string $path, ?int $limit = null): int;

/**
* Moves location identified by $sourceId into new parent identified by $destinationParentId.
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/Persistence/Filter/Content/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface Handler
*/
public function find(Filter $filter): iterable;

public function count(Filter $filter): int;
public function count(Filter $filter, ?int $limit = null): int;
}

class_alias(Handler::class, 'eZ\Publish\SPI\Persistence\Filter\Content\Handler');
2 changes: 1 addition & 1 deletion src/contracts/Persistence/Filter/Location/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface Handler
*/
public function find(Filter $filter): iterable;

public function count(Filter $filter): int;
public function count(Filter $filter, ?int $limit = null): int;
}

class_alias(Handler::class, 'eZ\Publish\SPI\Persistence\Filter\Location\Handler');
4 changes: 3 additions & 1 deletion src/contracts/Repository/ContentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,10 @@ public function find(Filter $filter, ?array $languages = null): ContentList;
* @param array<int, string> $languages A list of language codes to be added as additional constraints.
* If skipped, by default, unless SiteAccessAware layer has been disabled, languages set
* for a SiteAccess in a current context will be used.
* @param int|null $limit If set, the count will be limited to first $limit items found.
* In some cases it can significantly speed up a count operation for more complex filters.
*/
public function count(Filter $filter, ?array $languages = null): int;
public function count(Filter $filter, ?array $languages = null, ?int $limit = null): int;
}

class_alias(ContentService::class, 'eZ\Publish\API\Repository\ContentService');
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,9 @@ public function find(Filter $filter, ?array $languages = null): ContentList
return $this->innerService->find($filter, $languages);
}

public function count(Filter $filter, ?array $languages = null): int
public function count(Filter $filter, ?array $languages = null, ?int $limit = null): int
{
return $this->innerService->count($filter, $languages);
return $this->innerService->count($filter, $languages, $limit);
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/contracts/Repository/Decorator/LocationServiceDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,14 @@ public function loadParentLocationsForDraftContent(
return $this->innerService->loadParentLocationsForDraftContent($versionInfo, $prioritizedLanguages);
}

public function getLocationChildCount(Location $location): int
public function getLocationChildCount(Location $location, ?int $limit = null): int
{
return $this->innerService->getLocationChildCount($location);
return $this->innerService->getLocationChildCount($location, $limit);
}

public function getSubtreeSize(Location $location): int
public function getSubtreeSize(Location $location, ?int $limit = null): int
{
return $this->innerService->getSubtreeSize($location);
return $this->innerService->getSubtreeSize($location, $limit);
}

public function createLocation(
Expand Down Expand Up @@ -160,9 +160,9 @@ public function find(Filter $filter, ?array $languages = null): LocationList
return $this->innerService->find($filter, $languages);
}

public function count(Filter $filter, ?array $languages = null): int
public function count(Filter $filter, ?array $languages = null, ?int $limit = null): int
{
return $this->innerService->count($filter, $languages);
return $this->innerService->count($filter, $languages, $limit);
}
}

Expand Down
8 changes: 5 additions & 3 deletions src/contracts/Repository/LocationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,14 @@ public function loadParentLocationsForDraftContent(VersionInfo $versionInfo, ?ar
*
* @return int
*/
public function getLocationChildCount(Location $location): int;
public function getLocationChildCount(Location $location, ?int $limit = null): int;

/**
* Return the subtree size of a given location.
*
* Warning! This method is not permission aware by design.
*/
public function getSubtreeSize(Location $location): int;
public function getSubtreeSize(Location $location, ?int $limit = null): int;

/**
* Creates the new $location in the content repository for the given content.
Expand Down Expand Up @@ -274,8 +274,10 @@ public function find(Filter $filter, ?array $languages = null): LocationList;
* @param array<int, string>|null $languages a list of language codes to be added as additional constraints.
* If skipped, by default, unless SiteAccessAware layer has been disabled, languages set
* for a SiteAccess in a current context will be used.
* @param int|null $limit If set, the count will be limited to first $limit items found.
* In some cases it can significantly speed up a count operation for more complex filters.
*/
public function count(Filter $filter, ?array $languages = null): int;
public function count(Filter $filter, ?array $languages = null, ?int $limit = null): int;
}

class_alias(LocationService::class, 'eZ\Publish\API\Repository\LocationService');
4 changes: 2 additions & 2 deletions src/lib/Persistence/Cache/LocationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,13 @@ public function copySubtree($sourceId, $destinationParentId, $newOwnerId = null)
return $this->persistenceHandler->locationHandler()->copySubtree($sourceId, $destinationParentId, $newOwnerId);
}

public function getSubtreeSize(string $path): int
public function getSubtreeSize(string $path, ?int $limit = null): int
{
$this->logger->logCall(__METHOD__, [
'path' => $path,
]);

return $this->persistenceHandler->locationHandler()->getSubtreeSize($path);
return $this->persistenceHandler->locationHandler()->getSubtreeSize($path, $limit);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/lib/Persistence/Legacy/Content/Location/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ abstract public function getSubtreeContent(int $sourceId, bool $onlyIds = false)
*/
abstract public function getSubtreeChildrenDraftContentIds(int $sourceId): array;

abstract public function getSubtreeSize(string $path): int;
abstract public function getSubtreeSize(string $path, ?int $limit = null): int;

/**
* Returns data for the first level children of the location identified by given $locationId.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Ibexa\Core\Persistence\Legacy\Content\Gateway as ContentGateway;
use Ibexa\Core\Persistence\Legacy\Content\Language\MaskGenerator;
use Ibexa\Core\Persistence\Legacy\Content\Location\Gateway;
use Ibexa\Core\Persistence\Legacy\Traits\Doctrine\LimitedCountQueryTrait;
use Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriteriaConverter;
use Ibexa\Core\Search\Legacy\Content\Common\Gateway\SortClauseConverter;
use PDO;
Expand All @@ -35,6 +36,8 @@
*/
final class DoctrineDatabase extends Gateway
{
use LimitedCountQueryTrait;

/** @var \Doctrine\DBAL\Connection */
private $connection;

Expand Down Expand Up @@ -260,7 +263,7 @@ public function getSubtreeChildrenDraftContentIds(int $sourceId): array
return $statement->fetchFirstColumn();
}

public function getSubtreeSize(string $path): int
public function getSubtreeSize(string $path, ?int $limit = null): int
{
$query = $this->createNodeQueryBuilder([$this->dbPlatform->getCountExpression('node_id')]);
$query->andWhere(
Expand All @@ -272,6 +275,12 @@ public function getSubtreeSize(string $path): int
)
);

$query = $this->wrapCountQuery(
$query,
't.node_id',
$limit
);

return (int) $query->execute()->fetchOne();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ public function getSubtreeChildrenDraftContentIds(int $sourceId): array
}
}

public function getSubtreeSize(string $path): int
public function getSubtreeSize(string $path, ?int $limit = null): int
{
try {
return $this->innerGateway->getSubtreeSize($path);
return $this->innerGateway->getSubtreeSize($path, $limit);
} catch (DBALException | PDOException $e) {
throw DatabaseException::wrap($e);
}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/Persistence/Legacy/Content/Location/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,9 +332,9 @@ public function copySubtree($sourceId, $destinationParentId, $newOwnerId = null)
return $copiedSubtreeRootLocation;
}

public function getSubtreeSize(string $path): int
public function getSubtreeSize(string $path, ?int $limit = null): int
{
return $this->locationGateway->getSubtreeSize($path);
return $this->locationGateway->getSubtreeSize($path, $limit);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Ibexa\Core\Persistence\Legacy\Content\Gateway as ContentGateway;
use Ibexa\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway;
use Ibexa\Core\Persistence\Legacy\Filter\Gateway\Gateway;
use Ibexa\Core\Persistence\Legacy\Traits\Doctrine\LimitedCountQueryTrait;
use function iterator_to_array;
use function sprintf;
use Traversable;
Expand All @@ -31,6 +32,8 @@
*/
final class DoctrineGateway implements Gateway
{
use LimitedCountQueryTrait;

public const COLUMN_MAP = [
// Content Info
'content_id' => 'content.id',
Expand Down Expand Up @@ -87,13 +90,19 @@ private function getDatabasePlatform(): AbstractPlatform
}
}

public function count(FilteringCriterion $criterion): int
public function count(FilteringCriterion $criterion, ?int $limit = null): int
{
$query = $this->buildQuery(
[$this->getDatabasePlatform()->getCountExpression('DISTINCT content.id')],
$criterion
);

$query = $this->wrapCountQuery(
$query,
'content.id',
$limit
);

return (int)$query->execute()->fetch(FetchMode::COLUMN);
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/Persistence/Legacy/Filter/Gateway/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
interface Gateway
{
/**
* Return number of matched rows for the given Criteria (a total count w/o pagination constraints).
* Return number of matched rows for the given Criteria (a total count w/o pagination constraints, Unless a limit is passed).
*/
public function count(FilteringCriterion $criterion): int;
public function count(FilteringCriterion $criterion, ?int $limit = null): int;

/**
* Return iterator for raw Repository data for the given Query result filtered by the given Criteria,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@
use Ibexa\Core\Persistence\Legacy\Content\Gateway as ContentGateway;
use Ibexa\Core\Persistence\Legacy\Content\Location\Gateway as LocationGateway;
use Ibexa\Core\Persistence\Legacy\Filter\Gateway\Gateway;
use Ibexa\Core\Persistence\Legacy\Traits\Doctrine\LimitedCountQueryTrait;

/**
* @internal for internal use by Legacy Storage
*/
final class DoctrineGateway implements Gateway
{
use LimitedCountQueryTrait;

/** @var \Doctrine\DBAL\Connection */
private $connection;

Expand Down Expand Up @@ -54,12 +57,18 @@ private function getDatabasePlatform(): AbstractPlatform
}
}

public function count(FilteringCriterion $criterion): int
public function count(FilteringCriterion $criterion, ?int $limit = null): int
{
$query = $this->buildQuery($criterion);

$query->select($this->getDatabasePlatform()->getCountExpression('DISTINCT location.node_id'));

$query = $this->wrapCountQuery(
$query,
'location.node_id',
$limit
);

return (int)$query->execute()->fetch(FetchMode::COLUMN);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ function (array $row): ContentItem {
return $list;
}

public function count(Filter $filter): int
public function count(Filter $filter, ?int $limit = null): int
{
return $this->gateway->count($filter->getCriterion());
return $this->gateway->count($filter->getCriterion(), $limit);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ function (array $row): LocationWithContentInfo {
return $list;
}

public function count(Filter $filter): int
public function count(Filter $filter, ?int $limit = null): int
{
return $this->gateway->count($filter->getCriterion());
return $this->gateway->count($filter->getCriterion(), $limit);
}
}

Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a fan of traits under certain circumstances. I somewhat prefer object composition approach unless there are valid reasons.

In this case, trait is used to prevent code duplication only, and it requires presence of connection property (which is an implicit assumption not visible outside of trait).

Since the method added is using protected visibility, it is immediately available to descendants. While not a big deal, I would personally prefer it to use private visibility (if left as-is, that is).

Additionally, since it is a trait, unit tests are more difficult to provide.

Overall, I'd suggest making it it's own class, and injecting it into relevant gateways via constructor. To facilitate usage of the correct Connection object, it could be passed as part of the method arguments.

WDYT?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit awkward given it is multiple inheritance of a shared bit of functionality. Though do agree it should probably be refactored into it's own thing. Will take a look at refactoring into something that can be unit tested and refactor. The connection was misued here as it can be pulled from the passed query builder so there is no implicit requirement on there being a connection present as seen with a few lines below where $queryBuilder->getConnection()->getDatabasePlatform()->getCountExpression('*') is used. But based on everything else that is a mute point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, right, now I've noticed it.

By the by, note that Platform::getCountExpression() is deprecated and removed in DBAL v3. We are now expected to use "COUNT(*)" directly, without asking platform for their variant. I assume that is because COUNT() is ANSI SQL, and there were never any platform differences to begin with.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that makes sense I am currently aiming to keep things as closeley aligned with the way ibexa currently generates it's queries internally. It is likely Platform::getCountExpression is better placed to be updated globally in a separate ticket considering that is what is already in use now and this is not a wide sweeping change targeting specifically that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is updated in 5.x. I'd only ask to not use it here, as it will make merge up simpler :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh that makes sense! Sorry saw all of the other references to it and got a bit confused. Updated as outlined here https://github.com/doctrine/dbal/blob/4.2.x/UPGRADE.md#deprecated-redundant-abstractplatform-methods

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Core\Persistence\Legacy\Traits\Doctrine;

use Doctrine\DBAL\Query\QueryBuilder;

/**
* Limited Count count trait. Used to allow for proper limiting of count queries
* when using Doctrine DBAL QueryBuilder.
*/
trait LimitedCountQueryTrait
{
/**
* Takes a QueryBuilder and wraps it in a count query.
* This performs the following transformation to the passed query
* SELECT DISTINCT COUNT(DISTINCT someField) FROM XXX WHERE YYY;
* To
* SELECT COUNT(*) FROM (SELECT DISTINCT someField FROM XXX WHERE YYY LIMIT N) AS csub;.
*
* @param \Doctrine\DBAL\Query\QueryBuilder $queryBuilder
* @param string $countableField
* @param mixed $limit
*
* @return \Doctrine\DBAL\Query\QueryBuilder
*/
protected function wrapCountQuery(
QueryBuilder $queryBuilder,
string $countableField,
?int $limit,
): QueryBuilder {
$useLimit = $limit !== null && $limit > 0;

if (!$useLimit) {
return $queryBuilder;
}

$querySql = $queryBuilder->select($countableField)
->setMaxResults($limit)
->getSQL();

$countQuery = $this->connection->createQueryBuilder();

return $countQuery
->select(
$queryBuilder->getConnection()->getDatabasePlatform()->getCountExpression('*')
)
->from('(' . $querySql . ')', 'csub')
->setParameters($queryBuilder->getParameters(), $queryBuilder->getParameterTypes());
}
}
4 changes: 2 additions & 2 deletions src/lib/Repository/ContentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2713,7 +2713,7 @@ public function find(Filter $filter, ?array $languages = null): ContentList
return new ContentList($contentItemsIterator->getTotalCount(), $contentItems);
}

public function count(Filter $filter, ?array $languages = null): int
public function count(Filter $filter, ?array $languages = null, ?int $limit = null): int
{
$filter = clone $filter;
if (!empty($languages)) {
Expand All @@ -2733,7 +2733,7 @@ public function count(Filter $filter, ?array $languages = null): int
$filter->andWithCriterion($permissionCriterion);
}

return $this->contentFilteringHandler->count($filter);
return $this->contentFilteringHandler->count($filter, $limit);
}
}

Expand Down
Loading
Loading