Skip to content

Commit

Permalink
Support single group-by statement in ResultProvider:findCount (#2)
Browse files Browse the repository at this point in the history
This enables to properly get count of grouped statements.

Co-authored-by: Valentinas Bartusevičius <v.bartusevicius@paysera.com>
  • Loading branch information
vbartusevicius and Valentinas Bartusevičius authored Mar 16, 2020
1 parent 39b8c17 commit 64acd26
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .php_cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ include __DIR__ . '/vendor/autoload.php';
return Paysera\PhpCsFixerConfig\Config\PayseraConventionsConfig::create()
->setDefaultFinder(['src', 'tests'], [])
->setRiskyRules([
'Paysera/php_basic_comment_php_doc_necessity' => false,
'Paysera/php_basic_comment_php_doc_contents' => false,
'Paysera/php_basic_code_style_splitting_in_several_lines' => false,
'Paysera/php_basic_code_style_chained_method_calls' => false,
])
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 1.0.0
### Fixed
- support single `group-by` statement in `ResultProvider:findCount` - this enables to properly get count of grouped statements.
29 changes: 29 additions & 0 deletions src/Exception/InvalidGroupByException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);

namespace Paysera\Pagination\Exception;

class InvalidGroupByException extends PaginationException
{
/**
* @var string
*/
private $groupBy;

public function __construct(string $groupBy)
{
parent::__construct(sprintf(
'Calculating total-count only supported with single group-by, instead provided: "%s"',
$groupBy
));
$this->groupBy = $groupBy;
}

/**
* @return string
*/
public function getGroupBy(): string
{
return $this->groupBy;
}
}
57 changes: 57 additions & 0 deletions src/Service/Doctrine/ResultProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\Query\Expr\Orx;
use Doctrine\ORM\Query\Expr\GroupBy;
use Doctrine\ORM\Query\Expr\Comparison;
use Doctrine\ORM\QueryBuilder;
use Paysera\Pagination\Entity\Doctrine\AnalysedQuery;
use Paysera\Pagination\Entity\OrderingConfiguration;
use Paysera\Pagination\Entity\Doctrine\ConfiguredQuery;
use Paysera\Pagination\Entity\Pager;
use Paysera\Pagination\Entity\Result;
use Paysera\Pagination\Exception\InvalidGroupByException;
use Paysera\Pagination\Service\CursorBuilderInterface;

class ResultProvider
Expand Down Expand Up @@ -317,7 +319,62 @@ private function calculateTotalCount(Pager $filter, int $resultCount)
private function findCount(AnalysedQuery $analysedQuery): int
{
$countQueryBuilder = $analysedQuery->cloneQueryBuilder();
$groupByColumn = $this->getSingleValidGroupByColumn($countQueryBuilder);

if ($groupByColumn !== null) {
return $this->findCountWithGroupBy($groupByColumn, $analysedQuery);
}

$countQueryBuilder->select(sprintf('count(%s)', $analysedQuery->getRootAlias()));
return (int)$countQueryBuilder->getQuery()->getSingleScalarResult();
}

private function findCountWithGroupBy(string $groupByColumn, AnalysedQuery $analysedQuery): int
{
$countQueryBuilder = $analysedQuery->cloneQueryBuilder();
$countQueryBuilder
->resetDQLPart('groupBy')
->select(sprintf('count(distinct %s)', $groupByColumn))
;

$nullQueryBuilder = $analysedQuery->cloneQueryBuilder()
->resetDQLPart('groupBy')
->select($analysedQuery->getRootAlias())
->setMaxResults(1)
->andWhere($groupByColumn . ' is null')
;

$nonNullCount = (int)$countQueryBuilder->getQuery()->getSingleScalarResult();
$nullExists = count($nullQueryBuilder->getQuery()->getScalarResult());

return $nonNullCount + $nullExists;
}

/**
* @param QueryBuilder $queryBuilder
* @return string|null
*/
private function getSingleValidGroupByColumn(QueryBuilder $queryBuilder)
{
/** @var GroupBy[] $groupByParts */
$groupByParts = $queryBuilder->getDQLPart('groupBy');

if (count($groupByParts) === 0) {
return null;
}

if (count($groupByParts) > 1) {
$groupNames = array_map(
function (GroupBy $groupBy) { return $groupBy->getParts()[0]; },
$groupByParts
);
throw new InvalidGroupByException(implode(', ', $groupNames));
}

if (count($groupByParts) === 1 && count($groupByParts[0]->getParts()) > 1) {
throw new InvalidGroupByException(implode(', ', $groupByParts[0]->getParts()));
}

return $groupByParts[0]->getParts()[0];
}
}
26 changes: 26 additions & 0 deletions tests/Functional/Fixtures/ParentTestEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ class ParentTestEntity
*/
private $name;

/**
* @var string|null
*
* @Column(type="string", nullable=true)
*/
private $groupKey;

/**
* @var ChildTestEntity[]|Collection
*
Expand Down Expand Up @@ -98,4 +105,23 @@ public function setChildren($children)

return $this;
}

/**
* @return string|null
*/
public function getGroupKey()
{
return $this->groupKey;
}

/**
* @param string|null $groupKey
*
* @return $this
*/
public function setGroupKey(string $groupKey): self
{
$this->groupKey = $groupKey;
return $this;
}
}
68 changes: 64 additions & 4 deletions tests/Functional/Service/Doctrine/ResultProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Doctrine\ORM\QueryBuilder;
use Paysera\Pagination\Entity\OrderingConfiguration;
use Doctrine\ORM\EntityManager;
use Paysera\Pagination\Exception\InvalidGroupByException;
use Paysera\Pagination\Service\Doctrine\QueryAnalyser;
use Paysera\Pagination\Entity\Doctrine\ConfiguredQuery;
use Paysera\Pagination\Entity\OrderingPair;
Expand Down Expand Up @@ -36,10 +37,13 @@ protected function setUp()
);
}

private function createTestData(EntityManager $entityManager)
private function createTestData(EntityManager $entityManager, int $groupEvery = 10)
{
for ($parentIndex = 0; $parentIndex < 30; $parentIndex++) {
$parent = (new ParentTestEntity())->setName(sprintf('P%s', $parentIndex));
if ($parentIndex % $groupEvery === 0) {
$parent->setGroupKey(sprintf('group_%s', $parentIndex));
}
$entityManager->persist($parent);
}

Expand Down Expand Up @@ -603,23 +607,79 @@ public function testGetTotalCountForQuery($expectedResult, QueryBuilder $queryBu
$this->assertSame($expectedResult, $result);
}

public function getResultProviderForGetTotalCountForQuery()
public function testGetTotalCountForQueryThrowsExceptionWithMoreThanOneGroupByArgument()
{
$entityManager = $this->createTestEntityManager();
$this->createTestData($entityManager);
$queryBuilder = $entityManager->createQueryBuilder()
->select('p')
->from('PaginationTest:ParentTestEntity', 'p')
->groupBy('p.groupKey', 'p.name')
;

$configuredQuery = (new ConfiguredQuery($queryBuilder))->setTotalCountNeeded(false);

$this->expectException(InvalidGroupByException::class);
$this->resultProvider->getTotalCountForQuery($configuredQuery);
}

public function testGetTotalCountForQueryThrowsExceptionWithMoreThanOneGroupByExpression()
{
$entityManager = $this->createTestEntityManager();
$this->createTestData($entityManager);
$queryBuilder = $entityManager->createQueryBuilder()
->select('p')
->from('PaginationTest:ParentTestEntity', 'p')
->groupBy('p.groupKey')
->addGroupBy('p.name')
;

$configuredQuery = (new ConfiguredQuery($queryBuilder))->setTotalCountNeeded(false);

$this->expectException(InvalidGroupByException::class);
$this->resultProvider->getTotalCountForQuery($configuredQuery);
}

public function testGetTotalCountForQueryGetsCorrectCountWhenNoNullsAreInResult()
{
$entityManager = $this->createTestEntityManager();
$this->createTestData($entityManager, 1);
$queryBuilder = $entityManager->createQueryBuilder()
->select('p')
->from('PaginationTest:ParentTestEntity', 'p')
->groupBy('p.groupKey')
;

$configuredQuery = (new ConfiguredQuery($queryBuilder))->setTotalCountNeeded(false);
$this->assertSame(30, $this->resultProvider->getTotalCountForQuery($configuredQuery));
}

public function getResultProviderForGetTotalCountForQuery()
{
$entityManager = $this->createTestEntityManager();
$this->createTestData($entityManager);
$queryBuilder1 = $entityManager->createQueryBuilder()
->select('p')
->from('PaginationTest:ParentTestEntity', 'p')
;
$queryBuilder2 = $entityManager->createQueryBuilder()
->select('p')
->from('PaginationTest:ParentTestEntity', 'p')
->groupBy('p.groupKey')
;

return [
[
30,
$queryBuilder,
$queryBuilder1,
],
[
11,
(clone $queryBuilder)->andWhere('p.name LIKE :name')->setParameter('name', 'P2%'),
(clone $queryBuilder1)->andWhere('p.name LIKE :name')->setParameter('name', 'P2%'),
],
[
4,
$queryBuilder2,
],
];
}
Expand Down

0 comments on commit 64acd26

Please sign in to comment.