diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 04d7a82a65..5b04035574 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6492,6 +6492,12 @@ parameters: count: 1 path: src/contracts/Persistence/Content/VersionInfo.php + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 1 + path: src/contracts/Persistence/Filter/Content/Handler.php + - message: '#^Method Ibexa\\Contracts\\Core\\Persistence\\Filter\\Doctrine\\FilteringQueryBuilder\:\:buildOperatorBasedCriterionConstraint\(\) has parameter \$criterionValue with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -6522,6 +6528,12 @@ parameters: count: 1 path: src/contracts/Persistence/Filter/LazyListIterator.php + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 1 + path: src/contracts/Persistence/Filter/Location/Handler.php + - message: '#^Method Ibexa\\Contracts\\Core\\Persistence\\Handler\:\:beginTransaction\(\) has no return type specified\.$#' identifier: missingType.return @@ -6678,6 +6690,18 @@ parameters: count: 1 path: src/contracts/Repository/ContentService.php + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 1 + path: src/contracts/Repository/ContentService.php + + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\ContentService\:\:count\(\) invoked with 3 parameters, 1\-2 required\.$#' + identifier: arguments.count + count: 1 + path: src/contracts/Repository/Decorator/ContentServiceDecorator.php + - message: '#^Method Ibexa\\Contracts\\Core\\Repository\\Decorator\\ContentServiceDecorator\:\:validate\(\) has parameter \$context with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -6690,12 +6714,42 @@ parameters: count: 1 path: src/contracts/Repository/Decorator/ContentServiceDecorator.php + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 1 + path: src/contracts/Repository/Decorator/ContentServiceDecorator.php + - message: '#^Method Ibexa\\Contracts\\Core\\Repository\\Decorator\\LocationServiceDecorator\:\:loadLocationList\(\) has parameter \$locationIds with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/contracts/Repository/Decorator/LocationServiceDecorator.php + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:count\(\) invoked with 3 parameters, 1\-2 required\.$#' + identifier: arguments.count + count: 1 + path: src/contracts/Repository/Decorator/LocationServiceDecorator.php + + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:getLocationChildCount\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/contracts/Repository/Decorator/LocationServiceDecorator.php + + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:getSubtreeSize\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/contracts/Repository/Decorator/LocationServiceDecorator.php + + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 3 + path: src/contracts/Repository/Decorator/LocationServiceDecorator.php + - message: '#^Method Ibexa\\Contracts\\Core\\Repository\\Decorator\\SearchServiceDecorator\:\:suggest\(\) has no return type specified\.$#' identifier: missingType.return @@ -8280,6 +8334,12 @@ parameters: count: 1 path: src/contracts/Repository/LocationService.php + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 3 + path: src/contracts/Repository/LocationService.php + - message: '#^PHPDoc tag @param for parameter \$objectStateGroupId with type mixed is not subtype of native type int\.$#' identifier: parameter.phpDocType @@ -17478,6 +17538,12 @@ parameters: count: 1 path: src/lib/Persistence/Cache/LocationHandler.php + - + message: '#^Method Ibexa\\Contracts\\Core\\Persistence\\Content\\Location\\Handler\:\:getSubtreeSize\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Persistence/Cache/LocationHandler.php + - message: '#^Method Ibexa\\Core\\Persistence\\Cache\\LocationHandler\:\:changeMainLocation\(\) has no return type specified\.$#' identifier: missingType.return @@ -24234,6 +24300,12 @@ parameters: count: 1 path: src/lib/Repository/ContentService.php + - + message: '#^Method Ibexa\\Contracts\\Core\\Persistence\\Filter\\Content\\Handler\:\:count\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/ContentService.php + - message: '#^Method Ibexa\\Core\\Repository\\ContentService\:\:__construct\(\) has parameter \$settings with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -24294,6 +24366,12 @@ parameters: count: 1 path: src/lib/Repository/ContentService.php + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 1 + path: src/lib/Repository/ContentService.php + - message: '#^PHPDoc tag @var has invalid value \(\$content \\Ibexa\\Core\\Repository\\Values\\Content\\Content\)\: Unexpected token "\$content", expected type at offset 9 on line 1$#' identifier: phpDoc.parseError @@ -24552,12 +24630,24 @@ parameters: count: 1 path: src/lib/Repository/LocationService.php + - + message: '#^Method Ibexa\\Contracts\\Core\\Persistence\\Content\\Location\\Handler\:\:getSubtreeSize\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/LocationService.php + - message: '#^Method Ibexa\\Contracts\\Core\\Persistence\\Content\\UrlAlias\\Handler\:\:publishUrlAliasForLocation\(\) invoked with 6 parameters, 4\-5 required\.$#' identifier: arguments.count count: 1 path: src/lib/Repository/LocationService.php + - + message: '#^Method Ibexa\\Contracts\\Core\\Persistence\\Filter\\Location\\Handler\:\:count\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/LocationService.php + - message: '#^Method Ibexa\\Core\\Repository\\LocationService\:\:__construct\(\) has parameter \$settings with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -24570,6 +24660,12 @@ parameters: count: 1 path: src/lib/Repository/LocationService.php + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 2 + path: src/lib/Repository/LocationService.php + - message: '#^Property Ibexa\\Core\\Repository\\LocationService\:\:\$repository \(Ibexa\\Core\\Repository\\Repository\) does not accept Ibexa\\Contracts\\Core\\Repository\\Repository\.$#' identifier: assign.propertyType @@ -25512,6 +25608,12 @@ parameters: count: 1 path: src/lib/Repository/SettingService.php + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\ContentService\:\:count\(\) invoked with 3 parameters, 1\-2 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/SiteAccessAware/ContentService.php + - message: '#^Method Ibexa\\Core\\Repository\\SiteAccessAware\\ContentService\:\:validate\(\) has parameter \$context with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -25524,6 +25626,12 @@ parameters: count: 1 path: src/lib/Repository/SiteAccessAware/ContentService.php + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 1 + path: src/lib/Repository/SiteAccessAware/ContentService.php + - message: '#^Method Ibexa\\Core\\Repository\\SiteAccessAware\\Language\\AbstractLanguageResolver\:\:getPrioritizedLanguages\(\) has parameter \$forcedLanguages with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -25542,12 +25650,36 @@ parameters: count: 1 path: src/lib/Repository/SiteAccessAware/Language/LanguageResolver.php + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:count\(\) invoked with 3 parameters, 1\-2 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/SiteAccessAware/LocationService.php + + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:getLocationChildCount\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/SiteAccessAware/LocationService.php + + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:getSubtreeSize\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/SiteAccessAware/LocationService.php + - message: '#^Method Ibexa\\Core\\Repository\\SiteAccessAware\\LocationService\:\:loadLocationList\(\) has parameter \$locationIds with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/lib/Repository/SiteAccessAware/LocationService.php + - + message: '#^PHPDoc tag @param references unknown parameter\: \$limit$#' + identifier: parameter.notFound + count: 2 + path: src/lib/Repository/SiteAccessAware/LocationService.php + - message: '#^Property Ibexa\\Core\\Repository\\SiteAccessAware\\Repository\:\:\$notificationService \(Ibexa\\Core\\Repository\\NotificationService\) does not accept Ibexa\\Core\\Repository\\SiteAccessAware\\NotificationService\.$#' identifier: assign.propertyType @@ -41823,7 +41955,7 @@ parameters: - message: '#^Cannot access property \$id on Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Location\|null\.$#' identifier: property.nonObject - count: 11 + count: 9 path: tests/integration/Core/Repository/LocationServiceTest.php - @@ -41832,6 +41964,18 @@ parameters: count: 2 path: tests/integration/Core/Repository/LocationServiceTest.php + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:getLocationChildCount\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: tests/integration/Core/Repository/LocationServiceTest.php + + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:getSubtreeSize\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 2 + path: tests/integration/Core/Repository/LocationServiceTest.php + - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\LocationServiceTest\:\:assertAliasesBeforeCopy\(\) has no return type specified\.$#' identifier: missingType.return @@ -42030,12 +42174,6 @@ parameters: count: 1 path: tests/integration/Core/Repository/LocationServiceTest.php - - - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\LocationServiceTest\:\:testGetSubtreeSize\(\) should return Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Location but returns Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Location\|null\.$#' - identifier: return.type - count: 1 - path: tests/integration/Core/Repository/LocationServiceTest.php - - message: '#^Method Ibexa\\Tests\\Integration\\Core\\Repository\\LocationServiceTest\:\:testHideLocation\(\) has no return type specified\.$#' identifier: missingType.return @@ -42300,12 +42438,6 @@ parameters: count: 1 path: tests/integration/Core/Repository/LocationServiceTest.php - - - message: '#^Parameter \#1 \$location of method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:getSubtreeSize\(\) expects Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Location, Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Location\|null given\.$#' - identifier: argument.type - count: 2 - path: tests/integration/Core/Repository/LocationServiceTest.php - - message: '#^Parameter \#1 \$location of method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:moveSubtree\(\) expects Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Location, Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Location\|null given\.$#' identifier: argument.type @@ -67170,6 +67302,12 @@ parameters: count: 1 path: tests/lib/Persistence/Legacy/Filter/CriterionQueryBuilder/LogicalOperatorQueryBuilderQueryBuilderTest.php + - + message: '#^Parameter \#3 \$limit of method Ibexa\\Core\\Persistence\\Legacy\\Filter\\Query\\LimitedCountQueryBuilder\:\:wrap\(\) expects int\<1, max\>\|null, 0 given\.$#' + identifier: argument.type + count: 1 + path: tests/lib/Persistence/Legacy/Filter/Query/LimitedCountQueryBuilderTest.php + - message: '#^Call to an undefined method Ibexa\\Contracts\\Core\\Container\:\:get\(\)\.$#' identifier: method.notFound @@ -68970,6 +69108,12 @@ parameters: count: 1 path: tests/lib/Repository/Decorator/LanguageServiceDecoratorTest.php + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\LocationService\:\:getLocationChildCount\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: tests/lib/Repository/Decorator/LocationServiceDecoratorTest.php + - message: '#^Method Ibexa\\Tests\\Core\\Repository\\Decorator\\LocationServiceDecoratorTest\:\:testCopySubtreeDecorator\(\) has no return type specified\.$#' identifier: missingType.return diff --git a/src/contracts/Persistence/Content/Future/Repository/FutureContentService.php b/src/contracts/Persistence/Content/Future/Repository/FutureContentService.php new file mode 100644 index 0000000000..20e2a14707 --- /dev/null +++ b/src/contracts/Persistence/Content/Future/Repository/FutureContentService.php @@ -0,0 +1,23 @@ + $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'); diff --git a/src/contracts/Repository/Decorator/ContentServiceDecorator.php b/src/contracts/Repository/Decorator/ContentServiceDecorator.php index 3575c0df42..025f836ab2 100644 --- a/src/contracts/Repository/Decorator/ContentServiceDecorator.php +++ b/src/contracts/Repository/Decorator/ContentServiceDecorator.php @@ -286,9 +286,14 @@ public function find(Filter $filter, ?array $languages = null): ContentList return $this->innerService->find($filter, $languages); } - public function count(Filter $filter, ?array $languages = null): int + /** + * @param int|null $limit + */ + public function count(Filter $filter, ?array $languages = null /* ?int $limit = null */): int { - return $this->innerService->count($filter, $languages); + $limit = func_num_args() > 2 ? func_get_arg(2) : null; + + return $this->innerService->count($filter, $languages, $limit); } } diff --git a/src/contracts/Repository/Decorator/LocationServiceDecorator.php b/src/contracts/Repository/Decorator/LocationServiceDecorator.php index 5f11c4cab3..1cf35d2f16 100644 --- a/src/contracts/Repository/Decorator/LocationServiceDecorator.php +++ b/src/contracts/Repository/Decorator/LocationServiceDecorator.php @@ -82,14 +82,24 @@ public function loadParentLocationsForDraftContent( return $this->innerService->loadParentLocationsForDraftContent($versionInfo, $prioritizedLanguages); } - public function getLocationChildCount(Location $location): int + /** + * @param int|null $limit + */ + public function getLocationChildCount(Location $location /* ?int $limit = null */): int { - return $this->innerService->getLocationChildCount($location); + $limit = func_num_args() > 1 ? func_get_arg(1) : null; + + return $this->innerService->getLocationChildCount($location, $limit); } - public function getSubtreeSize(Location $location): int + /** + * @param int|null $limit + */ + public function getSubtreeSize(Location $location /* ?int $limit = null */): int { - return $this->innerService->getSubtreeSize($location); + $limit = func_num_args() > 1 ? func_get_arg(1) : null; + + return $this->innerService->getSubtreeSize($location, $limit); } public function createLocation( @@ -160,9 +170,14 @@ public function find(Filter $filter, ?array $languages = null): LocationList return $this->innerService->find($filter, $languages); } - public function count(Filter $filter, ?array $languages = null): int + /** + * @param int|null $limit + */ + public function count(Filter $filter, ?array $languages = null /* ?int $limit = null */): int { - return $this->innerService->count($filter, $languages); + $limit = func_num_args() > 2 ? func_get_arg(2) : null; + + return $this->innerService->count($filter, $languages, $limit); } } diff --git a/src/contracts/Repository/LocationService.php b/src/contracts/Repository/LocationService.php index a86c990d5b..a24401fc9a 100644 --- a/src/contracts/Repository/LocationService.php +++ b/src/contracts/Repository/LocationService.php @@ -121,17 +121,20 @@ public function loadParentLocationsForDraftContent(VersionInfo $versionInfo, ?ar * Returns the number of children which are readable by the current user of a location object. * * @param \Ibexa\Contracts\Core\Repository\Values\Content\Location $location + * @param int|null $limit If set, the count will be limited to first $limit items found. * * @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. + * + * @param int|null $limit */ - 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. @@ -274,8 +277,10 @@ public function find(Filter $filter, ?array $languages = null): LocationList; * @param array|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'); diff --git a/src/lib/Persistence/Cache/LocationHandler.php b/src/lib/Persistence/Cache/LocationHandler.php index 74a50a2039..c0f4221337 100644 --- a/src/lib/Persistence/Cache/LocationHandler.php +++ b/src/lib/Persistence/Cache/LocationHandler.php @@ -256,13 +256,18 @@ public function copySubtree($sourceId, $destinationParentId, $newOwnerId = null) return $this->persistenceHandler->locationHandler()->copySubtree($sourceId, $destinationParentId, $newOwnerId); } - public function getSubtreeSize(string $path): int + /** + * {@inheritdoc} + */ + public function getSubtreeSize(string $path /* ?int $limit = null */): int { + $limit = func_num_args() > 1 ? func_get_arg(1) : null; $this->logger->logCall(__METHOD__, [ 'path' => $path, + 'limit' => $limit, ]); - return $this->persistenceHandler->locationHandler()->getSubtreeSize($path); + return $this->persistenceHandler->locationHandler()->getSubtreeSize($path, $limit); } /** diff --git a/src/lib/Persistence/Legacy/Content/Location/Gateway.php b/src/lib/Persistence/Legacy/Content/Location/Gateway.php index 5218cad951..af6cf17990 100644 --- a/src/lib/Persistence/Legacy/Content/Location/Gateway.php +++ b/src/lib/Persistence/Legacy/Content/Location/Gateway.php @@ -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. diff --git a/src/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php b/src/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php index bfd06ecb2a..4c82e025c9 100644 --- a/src/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php +++ b/src/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabase.php @@ -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\Filter\Query\LimitedCountQueryBuilder; use Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriteriaConverter; use Ibexa\Core\Search\Legacy\Content\Common\Gateway\SortClauseConverter; use PDO; @@ -50,6 +51,9 @@ final class DoctrineDatabase extends Gateway /** @var \Ibexa\Core\Search\Legacy\Content\Common\Gateway\SortClauseConverter */ private $trashSortClauseConverter; + /** @var \Ibexa\Core\Persistence\Legacy\Filter\Query\LimitedCountQueryBuilder */ + private $limitedCountQueryBuilder; + /** * @throws \Doctrine\DBAL\DBALException */ @@ -57,13 +61,15 @@ public function __construct( Connection $connection, MaskGenerator $languageMaskGenerator, CriteriaConverter $trashCriteriaConverter, - SortClauseConverter $trashSortClauseConverter + SortClauseConverter $trashSortClauseConverter, + LimitedCountQueryBuilder $limitedCountQueryBuilder ) { $this->connection = $connection; $this->dbPlatform = $this->connection->getDatabasePlatform(); $this->languageMaskGenerator = $languageMaskGenerator; $this->trashCriteriaConverter = $trashCriteriaConverter; $this->trashSortClauseConverter = $trashSortClauseConverter; + $this->limitedCountQueryBuilder = $limitedCountQueryBuilder; } public function getBasicNodeData( @@ -260,7 +266,10 @@ public function getSubtreeChildrenDraftContentIds(int $sourceId): array return $statement->fetchFirstColumn(); } - public function getSubtreeSize(string $path): int + /** + * @phpstan-param positive-int $limit + */ + public function getSubtreeSize(string $path, ?int $limit = null): int { $query = $this->createNodeQueryBuilder([$this->dbPlatform->getCountExpression('node_id')]); $query->andWhere( @@ -272,6 +281,12 @@ public function getSubtreeSize(string $path): int ) ); + $query = $this->limitedCountQueryBuilder->wrap( + $query, + 't.node_id', + $limit + ); + return (int) $query->execute()->fetchOne(); } diff --git a/src/lib/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php b/src/lib/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php index e2d3c5db23..011cee9d52 100644 --- a/src/lib/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php +++ b/src/lib/Persistence/Legacy/Content/Location/Gateway/ExceptionConversion.php @@ -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); } diff --git a/src/lib/Persistence/Legacy/Content/Location/Handler.php b/src/lib/Persistence/Legacy/Content/Location/Handler.php index 8ce6a52493..a0926a2459 100644 --- a/src/lib/Persistence/Legacy/Content/Location/Handler.php +++ b/src/lib/Persistence/Legacy/Content/Location/Handler.php @@ -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); } /** diff --git a/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php b/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php index 18a2424afc..8abc1c10cd 100644 --- a/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php +++ b/src/lib/Persistence/Legacy/Filter/Gateway/Content/Doctrine/DoctrineGateway.php @@ -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\Filter\Query\LimitedCountQueryBuilder; use function iterator_to_array; use function sprintf; use Traversable; @@ -68,14 +69,19 @@ final class DoctrineGateway implements Gateway /** @var \Ibexa\Contracts\Core\Persistence\Filter\SortClauseVisitor */ private $sortClauseVisitor; + /** @var \Ibexa\Core\Persistence\Legacy\Filter\Query\LimitedCountQueryBuilder */ + private $limitedCountQueryBuilder; + public function __construct( Connection $connection, CriterionVisitor $criterionVisitor, - SortClauseVisitor $sortClauseVisitor + SortClauseVisitor $sortClauseVisitor, + LimitedCountQueryBuilder $limitedCountQueryBuilder ) { $this->connection = $connection; $this->criterionVisitor = $criterionVisitor; $this->sortClauseVisitor = $sortClauseVisitor; + $this->limitedCountQueryBuilder = $limitedCountQueryBuilder; } private function getDatabasePlatform(): AbstractPlatform @@ -87,13 +93,22 @@ private function getDatabasePlatform(): AbstractPlatform } } - public function count(FilteringCriterion $criterion): int + /** + * @phpstan-param positive-int $limit + */ + public function count(FilteringCriterion $criterion, ?int $limit = null): int { $query = $this->buildQuery( [$this->getDatabasePlatform()->getCountExpression('DISTINCT content.id')], $criterion ); + $query = $this->limitedCountQueryBuilder->wrap( + $query, + 'content.id', + $limit + ); + return (int)$query->execute()->fetch(FetchMode::COLUMN); } diff --git a/src/lib/Persistence/Legacy/Filter/Gateway/Gateway.php b/src/lib/Persistence/Legacy/Filter/Gateway/Gateway.php index 993b015f8a..415aa91651 100644 --- a/src/lib/Persistence/Legacy/Filter/Gateway/Gateway.php +++ b/src/lib/Persistence/Legacy/Filter/Gateway/Gateway.php @@ -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, diff --git a/src/lib/Persistence/Legacy/Filter/Gateway/Location/Doctrine/DoctrineGateway.php b/src/lib/Persistence/Legacy/Filter/Gateway/Location/Doctrine/DoctrineGateway.php index 6f0b9d74d6..e1cb392a8e 100644 --- a/src/lib/Persistence/Legacy/Filter/Gateway/Location/Doctrine/DoctrineGateway.php +++ b/src/lib/Persistence/Legacy/Filter/Gateway/Location/Doctrine/DoctrineGateway.php @@ -20,6 +20,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\Filter\Query\LimitedCountQueryBuilder; /** * @internal for internal use by Legacy Storage @@ -35,14 +36,19 @@ final class DoctrineGateway implements Gateway /** @var \Ibexa\Contracts\Core\Persistence\Filter\SortClauseVisitor */ private $sortClauseVisitor; + /** @var \Ibexa\Core\Persistence\Legacy\Filter\Query\LimitedCountQueryBuilder */ + private $limitedCountQueryBuilder; + public function __construct( Connection $connection, CriterionVisitor $criterionVisitor, - SortClauseVisitor $sortClauseVisitor + SortClauseVisitor $sortClauseVisitor, + LimitedCountQueryBuilder $limitedCountQueryBuilder ) { $this->connection = $connection; $this->criterionVisitor = $criterionVisitor; $this->sortClauseVisitor = $sortClauseVisitor; + $this->limitedCountQueryBuilder = $limitedCountQueryBuilder; } private function getDatabasePlatform(): AbstractPlatform @@ -54,12 +60,21 @@ private function getDatabasePlatform(): AbstractPlatform } } - public function count(FilteringCriterion $criterion): int + /** + * @phpstan-param positive-int $limit + */ + public function count(FilteringCriterion $criterion, ?int $limit = null): int { $query = $this->buildQuery($criterion); $query->select($this->getDatabasePlatform()->getCountExpression('DISTINCT location.node_id')); + $query = $this->limitedCountQueryBuilder->wrap( + $query, + 'location.node_id', + $limit + ); + return (int)$query->execute()->fetch(FetchMode::COLUMN); } diff --git a/src/lib/Persistence/Legacy/Filter/Handler/ContentFilteringHandler.php b/src/lib/Persistence/Legacy/Filter/Handler/ContentFilteringHandler.php index 7676b18a1c..4d81ccf922 100644 --- a/src/lib/Persistence/Legacy/Filter/Handler/ContentFilteringHandler.php +++ b/src/lib/Persistence/Legacy/Filter/Handler/ContentFilteringHandler.php @@ -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); } } diff --git a/src/lib/Persistence/Legacy/Filter/Handler/LocationFilteringHandler.php b/src/lib/Persistence/Legacy/Filter/Handler/LocationFilteringHandler.php index a5bdacb382..a99c2e5f97 100644 --- a/src/lib/Persistence/Legacy/Filter/Handler/LocationFilteringHandler.php +++ b/src/lib/Persistence/Legacy/Filter/Handler/LocationFilteringHandler.php @@ -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); } } diff --git a/src/lib/Persistence/Legacy/Filter/Query/LimitedCountQueryBuilder.php b/src/lib/Persistence/Legacy/Filter/Query/LimitedCountQueryBuilder.php new file mode 100644 index 0000000000..28e24f164a --- /dev/null +++ b/src/lib/Persistence/Legacy/Filter/Query/LimitedCountQueryBuilder.php @@ -0,0 +1,71 @@ +connection = $connection; + } + + /** + * Takes a QueryBuilder and wraps it in a count query with a limit if a limit is provided. + * 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;. + * + * @phpstan-param positive-int $limit + * + * @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException + * @throws \Doctrine\DBAL\Exception + */ + public function wrap( + QueryBuilder $queryBuilder, + string $countableField, + ?int $limit = null + ): QueryBuilder { + if ($limit === null) { + return $queryBuilder; + } + + if ($limit <= 0) { + throw new InvalidArgumentException('$limit', 'Limit must be greater than 0'); + } + + $querySql = $queryBuilder + ->select($countableField) + ->setMaxResults($limit) + ->getSQL(); + + $countQuery = $this->connection->createQueryBuilder(); + + return $countQuery + ->select( + 'COUNT(*)' + ) + ->from('(' . $querySql . ')', 'csub') + ->setParameters($queryBuilder->getParameters(), $queryBuilder->getParameterTypes()); + } +} diff --git a/src/lib/Repository/ContentService.php b/src/lib/Repository/ContentService.php index 16cd434e61..cc26ab1a71 100644 --- a/src/lib/Repository/ContentService.php +++ b/src/lib/Repository/ContentService.php @@ -2713,8 +2713,12 @@ public function find(Filter $filter, ?array $languages = null): ContentList return new ContentList($contentItemsIterator->getTotalCount(), $contentItems); } - public function count(Filter $filter, ?array $languages = null): int + /** + * @param int|null $limit + */ + public function count(Filter $filter, ?array $languages = null /*?int $limit = null */): int { + $limit = func_num_args() > 2 ? func_get_arg(2) : null; $filter = clone $filter; if (!empty($languages)) { $filter->andWithCriterion(new LanguageCode($languages)); @@ -2733,7 +2737,7 @@ public function count(Filter $filter, ?array $languages = null): int $filter->andWithCriterion($permissionCriterion); } - return $this->contentFilteringHandler->count($filter); + return $this->contentFilteringHandler->count($filter, $limit); } } diff --git a/src/lib/Repository/LocationService.php b/src/lib/Repository/LocationService.php index 6c71114051..37dfe82afe 100644 --- a/src/lib/Repository/LocationService.php +++ b/src/lib/Repository/LocationService.php @@ -372,17 +372,24 @@ public function loadParentLocationsForDraftContent(VersionInfo $versionInfo, ?ar /** * Returns the number of children which are readable by the current user of a Location object. */ - public function getLocationChildCount(APILocation $location): int + public function getLocationChildCount(APILocation $location /*?int $limit = null */): int { + $limit = func_num_args() > 1 ? func_get_arg(1) : null; $filter = $this->buildLocationChildrenFilter($location); - return $this->count($filter); + return $this->count($filter, null, $limit); } - public function getSubtreeSize(APILocation $location): int + /** + * @param int|null $limit + */ + public function getSubtreeSize(APILocation $location /* ?int $limit = null */): int { + $limit = func_num_args() > 1 ? func_get_arg(1) : null; + return $this->persistenceHandler->locationHandler()->getSubtreeSize( - $location->getPathString() + $location->getPathString(), + $limit ); } @@ -942,8 +949,12 @@ public function find(Filter $filter, ?array $languages = null): LocationList ); } - public function count(Filter $filter, ?array $languages = null): int + /** + * @param int|null $limit + */ + public function count(Filter $filter, ?array $languages = null /* ?int $limit = null */): int { + $limit = func_num_args() > 2 ? func_get_arg(2) : null; $filter = clone $filter; if (!empty($languages)) { $filter->andWithCriterion(new LanguageCode($languages)); @@ -962,7 +973,7 @@ public function count(Filter $filter, ?array $languages = null): int $filter->andWithCriterion($permissionCriterion); } - return $this->locationFilteringHandler->count($filter); + return $this->locationFilteringHandler->count($filter, $limit); } /** diff --git a/src/lib/Repository/SiteAccessAware/ContentService.php b/src/lib/Repository/SiteAccessAware/ContentService.php index f931f59b1f..b2f024fd6e 100644 --- a/src/lib/Repository/SiteAccessAware/ContentService.php +++ b/src/lib/Repository/SiteAccessAware/ContentService.php @@ -297,11 +297,17 @@ public function find(Filter $filter, ?array $languages = null): ContentList ); } - public function count(Filter $filter, ?array $languages = null): int + /** + * @param int|null $limit + */ + public function count(Filter $filter, ?array $languages = null /*?int $limit = null */): int { + $limit = func_num_args() > 2 ? func_get_arg(2) : null; + return $this->service->count( $filter, - $this->languageResolver->getPrioritizedLanguages($languages) + $this->languageResolver->getPrioritizedLanguages($languages), + $limit ); } } diff --git a/src/lib/Repository/SiteAccessAware/LocationService.php b/src/lib/Repository/SiteAccessAware/LocationService.php index 43e6feb7c9..3f57f5daf3 100644 --- a/src/lib/Repository/SiteAccessAware/LocationService.php +++ b/src/lib/Repository/SiteAccessAware/LocationService.php @@ -104,14 +104,21 @@ public function loadParentLocationsForDraftContent(VersionInfo $versionInfo, ?ar ); } - public function getLocationChildCount(Location $location): int + public function getLocationChildCount(Location $location /* ?int $limit = null */): int { - return $this->service->getLocationChildCount($location); + $limit = func_num_args() > 1 ? func_get_arg(1) : null; + + return $this->service->getLocationChildCount($location, $limit); } - public function getSubtreeSize(Location $location): int + /** + * @param int|null $limit + */ + public function getSubtreeSize(Location $location /* ?int $limit = null */): int { - return $this->service->getSubtreeSize($location); + $limit = func_num_args() > 1 ? func_get_arg(1) : null; + + return $this->service->getSubtreeSize($location, $limit); } public function createLocation(ContentInfo $contentInfo, LocationCreateStruct $locationCreateStruct): Location @@ -192,11 +199,17 @@ public function find(Filter $filter, ?array $languages = null): LocationList ); } - public function count(Filter $filter, ?array $languages = null): int + /** + * @param int|null $limit + */ + public function count(Filter $filter, ?array $languages = null /* ?int $limit = null */): int { + $limit = func_num_args() > 2 ? func_get_arg(2) : null; + return $this->service->count( $filter, - $this->languageResolver->getPrioritizedLanguages($languages) + $this->languageResolver->getPrioritizedLanguages($languages), + $limit ); } } diff --git a/src/lib/Resources/settings/repository/inner.yml b/src/lib/Resources/settings/repository/inner.yml index 8b5e99afcf..852b6078d2 100644 --- a/src/lib/Resources/settings/repository/inner.yml +++ b/src/lib/Resources/settings/repository/inner.yml @@ -45,6 +45,8 @@ services: class: Ibexa\Core\Repository\ContentService factory: ['@Ibexa\Core\Repository\Repository', getContentService] lazy: true + tags: + - { name: 'proxy', interface: 'Ibexa\Contracts\Core\Future\Repository\FutureContentService' } Ibexa\Core\Repository\ContentTypeService: class: Ibexa\Core\Repository\ContentTypeService @@ -102,6 +104,8 @@ services: class: Ibexa\Core\Repository\LocationService factory: ['@Ibexa\Core\Repository\Repository', getLocationService] lazy: true + tags: + - { name: 'proxy', interface: 'Ibexa\Contracts\Core\Future\Repository\FutureLocationService' } Ibexa\Core\Repository\LanguageService: class: Ibexa\Core\Repository\LanguageService diff --git a/src/lib/Resources/settings/storage_engines/legacy/filter.yaml b/src/lib/Resources/settings/storage_engines/legacy/filter.yaml index 4ac9426423..54022002cb 100644 --- a/src/lib/Resources/settings/storage_engines/legacy/filter.yaml +++ b/src/lib/Resources/settings/storage_engines/legacy/filter.yaml @@ -56,3 +56,7 @@ services: arguments: $gateway: '@Ibexa\Core\Persistence\Legacy\Filter\Gateway\Location\Doctrine\DoctrineGateway' $locationMapper: '@Ibexa\Core\Persistence\Legacy\Content\Location\Mapper' + + Ibexa\Core\Persistence\Legacy\Filter\Query\LimitedCountQueryBuilder: + arguments: + $connection: '@ibexa.persistence.connection' \ No newline at end of file diff --git a/src/lib/Resources/settings/storage_engines/legacy/location.yml b/src/lib/Resources/settings/storage_engines/legacy/location.yml index 3b0ba0cde5..ee3a7a813a 100644 --- a/src/lib/Resources/settings/storage_engines/legacy/location.yml +++ b/src/lib/Resources/settings/storage_engines/legacy/location.yml @@ -6,6 +6,7 @@ services: - '@Ibexa\Core\Persistence\Legacy\Content\Language\MaskGenerator' - '@ibexa.core.trash.search.legacy.gateway.criteria_converter' - '@ibexa.core.trash.search.legacy.gateway.sort_clause_converter' + - '@Ibexa\Core\Persistence\Legacy\Filter\Query\LimitedCountQueryBuilder' Ibexa\Core\Persistence\Legacy\Content\Location\Gateway\ExceptionConversion: class: Ibexa\Core\Persistence\Legacy\Content\Location\Gateway\ExceptionConversion diff --git a/tests/integration/Core/Repository/LocationServiceTest.php b/tests/integration/Core/Repository/LocationServiceTest.php index 350347bda4..c1d3f28a8c 100644 --- a/tests/integration/Core/Repository/LocationServiceTest.php +++ b/tests/integration/Core/Repository/LocationServiceTest.php @@ -1114,6 +1114,26 @@ public function testGetLocationChildCount() ); } + /** + * Test for the getLocationChildCount() method with a limitation on the number of children. + * + * @covers \Ibexa\Contracts\Core\Repository\LocationService::getLocationChildCount() + * @depends testLoadLocation + */ + public function testGetLocationChildCountWithLimitation(): void + { + // $locationId is the ID of an existing location + $locationService = $this->getRepository()->getLocationService(); + $location = $locationService->loadLocation($this->generateId('location', 5)); + $this->assertSame( + 2, + $locationService->getLocationChildCount( + $location, + 2 + ) + ); + } + /** * Test for the loadLocationChildren() method. * @@ -3546,6 +3566,9 @@ public function testGetSubtreeSize(): Location $folder = $this->createFolder(['eng-GB' => 'Parent Folder'], 2); $location = $folder->getVersionInfo()->getContentInfo()->getMainLocation(); + self::assertNotNull($location); + + // phpstan-ignore-next-line self::assertSame(1, $locationService->getSubtreeSize($location)); $this->createFolder(['eng-GB' => 'Child 1'], $location->id); @@ -3556,6 +3579,47 @@ public function testGetSubtreeSize(): Location return $location; } + public function testGetSubtreeSizeWithLimit(): Location + { + $repository = $this->getRepository(); + $locationService = $repository->getLocationService(); + + $folder = $this->createFolder(['eng-GB' => 'Parent Folder'], 2); + $location = $folder->getVersionInfo()->getContentInfo()->getMainLocation(); + self::assertNotNull($location); + + for ($i = 1; $i <= 10; ++$i) { + $this->createFolder(['eng-GB' => 'Child ' . $i], $location->id); + } + + self::assertSame(3, $locationService->getSubtreeSize($location, 3)); + + return $location; + } + + public function testGetSubtreeSizeWithInvalidLimitThrowsExpectedError(): Location + { + $repository = $this->getRepository(); + $locationService = $repository->getLocationService(); + + $folder = $this->createFolder(['eng-GB' => 'Parent Folder'], 2); + $location = $folder->getVersionInfo()->getContentInfo()->getMainLocation(); + self::assertNotNull($location); + + self::assertSame(1, $locationService->getSubtreeSize($location)); + + for ($i = 1; $i <= 10; ++$i) { + $this->createFolder(['eng-GB' => 'Child ' . $i], $location->id); + } + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/Limit must be greater than 0/'); + + self::assertSame(3, $locationService->getSubtreeSize($location, -42)); + + return $location; + } + /** * Loads properties from all locations in the $location's subtree. * diff --git a/tests/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabaseTest.php b/tests/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabaseTest.php index 789530d83d..62c6a10100 100644 --- a/tests/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabaseTest.php +++ b/tests/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabaseTest.php @@ -28,7 +28,8 @@ protected function getLocationGateway() $this->getDatabaseConnection(), $this->getLanguageMaskGenerator(), $this->getTrashCriteriaConverterDependency(), - $this->getTrashSortClauseConverterDependency() + $this->getTrashSortClauseConverterDependency(), + $this->getLimitedCountQueryBuilderDependency() ); } diff --git a/tests/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabaseTrashTest.php b/tests/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabaseTrashTest.php index 322dfacd79..bfd587ed87 100644 --- a/tests/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabaseTrashTest.php +++ b/tests/lib/Persistence/Legacy/Content/Location/Gateway/DoctrineDatabaseTrashTest.php @@ -24,7 +24,8 @@ protected function getLocationGateway() $this->getDatabaseConnection(), $this->getLanguageMaskGenerator(), $this->getTrashCriteriaConverterDependency(), - $this->getTrashSortClauseConverterDependency() + $this->getTrashSortClauseConverterDependency(), + $this->getLimitedCountQueryBuilderDependency(), ); } diff --git a/tests/lib/Persistence/Legacy/Content/UrlAlias/UrlAliasHandlerTest.php b/tests/lib/Persistence/Legacy/Content/UrlAlias/UrlAliasHandlerTest.php index 13569aefd1..0c1df662f6 100644 --- a/tests/lib/Persistence/Legacy/Content/UrlAlias/UrlAliasHandlerTest.php +++ b/tests/lib/Persistence/Legacy/Content/UrlAlias/UrlAliasHandlerTest.php @@ -5431,7 +5431,8 @@ protected function getLocationGateway() $this->getDatabaseConnection(), $this->getLanguageMaskGenerator(), $this->getTrashCriteriaConverterDependency(), - $this->getTrashSortClauseConverterDependency() + $this->getTrashSortClauseConverterDependency(), + $this->getLimitedCountQueryBuilderDependency() ); } diff --git a/tests/lib/Persistence/Legacy/Filter/Query/LimitedCountQueryBuilderTest.php b/tests/lib/Persistence/Legacy/Filter/Query/LimitedCountQueryBuilderTest.php new file mode 100644 index 0000000000..0ae64145db --- /dev/null +++ b/tests/lib/Persistence/Legacy/Filter/Query/LimitedCountQueryBuilderTest.php @@ -0,0 +1,73 @@ +limitedCountQueryBuilder = new LimitedCountQueryBuilder($this->getDatabaseConnection()); + } + + /** + * @covers \Ibexa\Core\Persistence\Legacy\Filter\Query\LimitedCountQueryBuilder::wrap + */ + public function testWrapThrowsExceptionOnZeroLimit(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/Limit must be greater than 0/'); + + $qb = $this->getDatabaseConnection()->createQueryBuilder(); + + $this->limitedCountQueryBuilder->wrap($qb, 'someField', 0); + } + + /** + * @covers \Ibexa\Core\Persistence\Legacy\Filter\Query\LimitedCountQueryBuilder::wrap + */ + public function testWrapDoesNotChangeQueryBuilderIfLimitIsNull(): void + { + $qb = $this->getDatabaseConnection()->createQueryBuilder(); + $qb->select('DISTINCT someField') + ->from('someTable') + ->where('someCondition = :condition') + ->setParameter('condition', 'value'); + + $wrappedQueryBuilder = $this->limitedCountQueryBuilder->wrap($qb, 'someField', null); + + // The original query should remain unchanged + $this->assertEquals($qb->getSQL(), $wrappedQueryBuilder->getSQL()); + } + + /** + * @covers \Ibexa\Core\Persistence\Legacy\Filter\Query\LimitedCountQueryBuilder::wrap + */ + public function testWrapWrapsQueryBuilderCorrectly(): void + { + $qb = $this->getDatabaseConnection()->createQueryBuilder(); + $qb->select('DISTINCT someField') + ->from('someTable') + ->where('someCondition = :condition') + ->setParameter('condition', 'value'); + + $wrappedQueryBuilder = $this->limitedCountQueryBuilder->wrap($qb, 'someField', 10); + + $expectedSql = 'SELECT COUNT(*) FROM (SELECT someField FROM someTable WHERE someCondition = :condition LIMIT 10) csub'; + $this->assertEquals($expectedSql, $wrappedQueryBuilder->getSQL()); + } +} diff --git a/tests/lib/Persistence/Legacy/TestCase.php b/tests/lib/Persistence/Legacy/TestCase.php index 2edd0079da..70c7ff6e16 100644 --- a/tests/lib/Persistence/Legacy/TestCase.php +++ b/tests/lib/Persistence/Legacy/TestCase.php @@ -16,6 +16,7 @@ use Ibexa\Contracts\Core\Test\Persistence\Fixture\FixtureImporter; use Ibexa\Contracts\Core\Test\Persistence\Fixture\YamlFixture; use Ibexa\Contracts\Core\Test\Repository\SetupFactory\Legacy; +use Ibexa\Core\Persistence\Legacy\Filter\Query\LimitedCountQueryBuilder; use Ibexa\Core\Persistence\Legacy\SharedGateway; use Ibexa\Core\Search\Legacy\Content; use Ibexa\Core\Search\Legacy\Content\Common\Gateway\CriteriaConverter; @@ -346,6 +347,11 @@ protected function getTrashSortClauseConverterDependency(): SortClauseConverter ] ); } + + protected function getLimitedCountQueryBuilderDependency(): LimitedCountQueryBuilder + { + return new LimitedCountQueryBuilder($this->getDatabaseConnection()); + } } class_alias(TestCase::class, 'eZ\Publish\Core\Persistence\Legacy\Tests\TestCase'); diff --git a/tests/lib/Repository/Decorator/LocationServiceDecoratorTest.php b/tests/lib/Repository/Decorator/LocationServiceDecoratorTest.php index 2e579230e3..b11940719f 100644 --- a/tests/lib/Repository/Decorator/LocationServiceDecoratorTest.php +++ b/tests/lib/Repository/Decorator/LocationServiceDecoratorTest.php @@ -151,7 +151,7 @@ public function testGetLocationChildCountDecorator() $serviceMock = $this->createServiceMock(); $decoratedService = $this->createDecorator($serviceMock); - $parameters = [$this->createMock(Location::class)]; + $parameters = [$this->createMock(Location::class), 8]; $serviceMock->expects($this->once())->method('getLocationChildCount')->with(...$parameters);