From cc6afc9ef08dc9ec54d4817c052589c3530831f8 Mon Sep 17 00:00:00 2001 From: Divine <48183131+divine@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:20:56 +0300 Subject: [PATCH] fix(odm): partial pagination limit the documents entering $facet --- .../Odm/Extension/PaginationExtension.php | 5 ++ .../Extension/PaginationExtensionTest.php | 51 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/Doctrine/Odm/Extension/PaginationExtension.php b/src/Doctrine/Odm/Extension/PaginationExtension.php index ee9f9f6099e..e6612be6934 100644 --- a/src/Doctrine/Odm/Extension/PaginationExtension.php +++ b/src/Doctrine/Odm/Extension/PaginationExtension.php @@ -67,6 +67,11 @@ public function applyToCollection(Builder $aggregationBuilder, string $resourceC */ $repository = $manager->getRepository($resourceClass); + // Limit the documents entering $facet to avoid buffering the whole result set + if (!$doesCount && $limit > 0) { + $aggregationBuilder->limit($offset + $limit); + } + $facet = $aggregationBuilder->facet(); $addFields = $aggregationBuilder->addFields(); diff --git a/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php b/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php index 557549898dc..352ccf6bcce 100644 --- a/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php +++ b/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php @@ -259,6 +259,24 @@ public function testApplyToCollectionWithMaximumItemsPerPage(): void $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', (new GetCollection())->withPaginationEnabled(true)->withPaginationClientEnabled(true)->withPaginationMaximumItemsPerPage(80), $context); } + public function testApplyToCollectionWithPartialPagination(): void + { + $pagination = new Pagination([ + 'partial' => true, + 'page_parameter_name' => '_page', + ]); + + $aggregationBuilderProphecy = $this->mockAggregationBuilder(20, 20, true); + + $context = ['filters' => ['_page' => 2, 'itemsPerPage' => 20]]; + + $extension = new PaginationExtension( + $this->managerRegistryProphecy->reveal(), + $pagination + ); + $extension->applyToCollection($aggregationBuilderProphecy->reveal(), 'Foo', (new GetCollection())->withPaginationEnabled(true)->withPaginationPartial(true)->withPaginationClientEnabled(true)->withPaginationItemsPerPage(20), $context); + } + public function testSupportsResult(): void { $pagination = new Pagination(); @@ -430,11 +448,14 @@ public function testGetResultWithExecuteOptions(): void $this->assertInstanceOf(PaginatorInterface::class, $result); } - private function mockAggregationBuilder(int $expectedOffset, int $expectedLimit): ObjectProphecy + private function mockAggregationBuilder(int $expectedOffset, int $expectedLimit, bool $partial = false): ObjectProphecy { $countProphecy = $this->prophesize(Count::class); $countAggregationBuilderProphecy = $this->prophesize(Builder::class); - $countAggregationBuilderProphecy->count('count')->shouldBeCalled()->willReturn($countProphecy->reveal()); + + if (!$partial) { + $countAggregationBuilderProphecy->count('count')->shouldBeCalled()->willReturn($countProphecy->reveal()); + } $repositoryProphecy = $this->prophesize(DocumentRepository::class); @@ -448,10 +469,17 @@ private function mockAggregationBuilder(int $expectedOffset, int $expectedLimit) if ($expectedLimit > 0) { $resultsAggregationBuilderProphecy = $this->prophesize(Builder::class); - $repositoryProphecy->createAggregationBuilder()->shouldBeCalled()->willReturn( - $resultsAggregationBuilderProphecy->reveal(), - $countAggregationBuilderProphecy->reveal() - ); + + if ($partial) { + $repositoryProphecy->createAggregationBuilder()->shouldBeCalled()->willReturn( + $resultsAggregationBuilderProphecy->reveal() + ); + } else { + $repositoryProphecy->createAggregationBuilder()->shouldBeCalled()->willReturn( + $resultsAggregationBuilderProphecy->reveal(), + $countAggregationBuilderProphecy->reveal() + ); + } $skipProphecy = $this->prophesize(Skip::class); $skipProphecy->limit($expectedLimit)->shouldBeCalled()->willReturn($skipProphecy->reveal()); @@ -467,8 +495,10 @@ private function mockAggregationBuilder(int $expectedOffset, int $expectedLimit) $addFieldsProphecy->literal([])->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); } - $facetProphecy->field('count')->shouldBeCalled()->willReturn($facetProphecy->reveal()); - $facetProphecy->pipeline($countProphecy)->shouldBeCalled()->willReturn($facetProphecy->reveal()); + if (!$partial) { + $facetProphecy->field('count')->shouldBeCalled()->willReturn($facetProphecy->reveal()); + $facetProphecy->pipeline($countProphecy)->shouldBeCalled()->willReturn($facetProphecy->reveal()); + } $addFieldsProphecy->field('__api_first_result__')->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); $addFieldsProphecy->literal($expectedOffset)->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); @@ -476,6 +506,11 @@ private function mockAggregationBuilder(int $expectedOffset, int $expectedLimit) $addFieldsProphecy->literal($expectedLimit)->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); $aggregationBuilderProphecy = $this->prophesize(Builder::class); + + if ($partial && $expectedLimit > 0) { + $aggregationBuilderProphecy->limit($expectedOffset + $expectedLimit)->shouldBeCalled(); + } + $aggregationBuilderProphecy->facet()->shouldBeCalled()->willReturn($facetProphecy->reveal()); $aggregationBuilderProphecy->addFields()->shouldBeCalled()->willReturn($addFieldsProphecy->reveal());