diff --git a/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php b/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php new file mode 100644 index 000000000..47df1192f --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php @@ -0,0 +1,120 @@ + FROM ()) + * + * Works with composite keys but cannot deal with queries that have multiple + * root entities (e.g. `SELECT f, b from Foo, Bar`) + * + * @author Sander Marechal + */ +class CountOutputWalker extends SqlWalker +{ + /** + * @var Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $platform; + + /** + * @var Doctrine\ORM\Query\ResultSetMapping + */ + private $rsm; + + /** + * @var array + */ + private $queryComponents; + + /** + * Constructor. Stores various parameters that are otherwise unavailable + * because Doctrine\ORM\Query\SqlWalker keeps everything private without + * accessors. + * + * @param Doctrine\ORM\Query $query + * @param Doctrine\ORM\Query\ParserResult $parserResult + * @param array $queryComponents + */ + public function __construct($query, $parserResult, array $queryComponents) + { + $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); + $this->rsm = $parserResult->getResultSetMapping(); + $this->queryComponents = $queryComponents; + + parent::__construct($query, $parserResult, $queryComponents); + } + + /** + * Walks down a SelectStatement AST node, wrapping it in a COUNT (SELECT DISTINCT) + * + * Note that the ORDER BY clause is not removed. Many SQL implementations (e.g. MySQL) + * are able to cache subqueries. By keeping the ORDER BY clause intact, the limitSubQuery + * that will most likely be executed next can be read from the native SQL cache. + * + * @param SelectStatement $AST + * @return string + */ + public function walkSelectStatement(SelectStatement $AST) + { + $sql = parent::walkSelectStatement($AST); + + // Find out the SQL alias of the identifier column of the root entity + // It may be possible to make this work with multiple root entities but that + // would probably require issuing multiple queries or doing a UNION SELECT + // so for now, It's not supported. + + // Get the root entity and alias from the AST fromClause + $from = $AST->fromClause->identificationVariableDeclarations; + if (count($from) > 1) { + throw new \RuntimeException("Cannot count query which selects two FROM components, cannot make distinction"); + } + + $rootClass = $from[0]->rangeVariableDeclaration->abstractSchemaName; + $rootAlias = $from[0]->rangeVariableDeclaration->aliasIdentificationVariable; + + // Get the identity properties from the metadata + $rootIdentifier = $this->queryComponents[$rootAlias]['metadata']->identifier; + + // For every identifier, find out the SQL alias by combing through the ResultSetMapping + $sqlIdentifier = array(); + foreach ($rootIdentifier as $property) { + foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + + if (count($rootIdentifier) != count($sqlIdentifier)) { + throw new \RuntimeException(sprintf( + 'Not all identifier properties can be found in the ResultSetMapping: %s', + implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))) + )); + } + + // Build the counter query + return sprintf('SELECT %s AS _dctrn_count FROM (SELECT DISTINCT %s FROM (%s) AS _dctrn_result) AS _dctrn_table', + $this->platform->getCountExpression('*'), + implode(', ', $sqlIdentifier), + $sql + ); + } +} diff --git a/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php b/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php index 10df1c3e1..2f80f7f63 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php +++ b/lib/Doctrine/ORM/Tools/Pagination/CountWalker.php @@ -43,6 +43,10 @@ class CountWalker extends TreeWalkerAdapter */ public function walkSelectStatement(SelectStatement $AST) { + if ($AST->havingClause) { + throw new \RuntimeException('Cannot count query that uses a HAVING clause. Use the output walkers for pagination'); + } + $rootComponents = array(); foreach ($this->_getQueryComponents() AS $dqlAlias => $qComp) { $isParent = array_key_exists('parent', $qComp) diff --git a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php new file mode 100644 index 000000000..e703ea0b8 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php @@ -0,0 +1,144 @@ + FROM () LIMIT x OFFSET y + * + * Works with composite keys but cannot deal with queries that have multiple + * root entities (e.g. `SELECT f, b from Foo, Bar`) + * + * @author Sander Marechal + */ +class LimitSubqueryOutputWalker extends SqlWalker +{ + /** + * @var Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $platform; + + /** + * @var Doctrine\ORM\Query\ResultSetMapping + */ + private $rsm; + + /** + * @var array + */ + private $queryComponents; + + /** + * @var int + */ + private $firstResult; + + /** + * @var int + */ + private $maxResults; + + /** + * Constructor. Stores various parameters that are otherwise unavailable + * because Doctrine\ORM\Query\SqlWalker keeps everything private without + * accessors. + * + * @param Doctrine\ORM\Query $query + * @param Doctrine\ORM\Query\ParserResult $parserResult + * @param array $queryComponents + */ + public function __construct($query, $parserResult, array $queryComponents) + { + $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform(); + $this->rsm = $parserResult->getResultSetMapping(); + $this->queryComponents = $queryComponents; + + // Reset limit and offset + $this->firstResult = $query->getFirstResult(); + $this->maxResults = $query->getMaxResults(); + $query->setFirstResult(null)->setMaxResults(null); + + parent::__construct($query, $parserResult, $queryComponents); + } + + /** + * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT + * + * @param SelectStatement $AST + * @return string + */ + public function walkSelectStatement(SelectStatement $AST) + { + $sql = parent::walkSelectStatement($AST); + + // Find out the SQL alias of the identifier column of the root entity + // It may be possible to make this work with multiple root entities but that + // would probably require issuing multiple queries or doing a UNION SELECT + // so for now, It's not supported. + + // Get the root entity and alias from the AST fromClause + $from = $AST->fromClause->identificationVariableDeclarations; + if (count($from) !== 1) { + throw new \RuntimeException("Cannot count query which selects two FROM components, cannot make distinction"); + } + + $rootClass = $from[0]->rangeVariableDeclaration->abstractSchemaName; + $rootAlias = $from[0]->rangeVariableDeclaration->aliasIdentificationVariable; + + // Get the identity properties from the metadata + $metadata = $this->queryComponents[$rootAlias]['metadata']; + $rootIdentifier = $metadata->identifier; + + // For every identifier, find out the SQL alias by combing through the ResultSetMapping + $sqlIdentifier = array(); + foreach ($rootIdentifier as $property) { + foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) { + if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) { + $sqlIdentifier[$property] = $alias; + } + } + } + + if (count($rootIdentifier) != count($sqlIdentifier)) { + throw new \RuntimeException(sprintf( + 'Not all identifier properties can be found in the ResultSetMapping: %s', + implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier))) + )); + } + + // Build the counter query + $sql = sprintf('SELECT DISTINCT %s FROM (%s) AS _dctrn_result', + implode(', ', $sqlIdentifier), $sql); + + // Apply the limit and offset + $sql = $this->platform->modifyLimitQuery( + $sql, $this->maxResults, $this->firstResult + ); + + // Add the columns to the ResultSetMapping. It's not really nice but + // it works. Preferably I'd clear the RSM or simply create a new one + // but that is not possible from inside the output walker, so we dirty + // up the one we have. + foreach ($sqlIdentifier as $property => $alias) { + $this->rsm->addScalarResult($alias, $property); + } + + return $sql; + } +} diff --git a/lib/Doctrine/ORM/Tools/Pagination/Paginator.php b/lib/Doctrine/ORM/Tools/Pagination/Paginator.php index 760d7de13..566ecdf12 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/Paginator.php +++ b/lib/Doctrine/ORM/Tools/Pagination/Paginator.php @@ -19,14 +19,10 @@ namespace Doctrine\ORM\Tools\Pagination; -use Doctrine\ORM\QueryBuilder; -use Doctrine\ORM\Query; -use Doctrine\ORM\NoResultException; -use Doctrine\ORM\Tools\Pagination\WhereInWalker; -use Doctrine\ORM\Tools\Pagination\CountWalker; -use Countable; -use IteratorAggregate; -use ArrayIterator; +use Doctrine\ORM\QueryBuilder, + Doctrine\ORM\Query, + Doctrine\ORM\Query\ResultSetMapping, + Doctrine\ORM\NoResultException; /** * Paginator @@ -49,6 +45,11 @@ class Paginator implements \Countable, \IteratorAggregate */ private $fetchJoinCollection; + /** + * @var bool|null + */ + private $useOutputWalkers; + /** * @var int */ @@ -90,6 +91,28 @@ class Paginator implements \Countable, \IteratorAggregate return $this->fetchJoinCollection; } + /** + * Returns whether the paginator will use an output walker + * + * @return bool|null + */ + public function getUseOutputWalkers() + { + return $this->useOutputWalkers; + } + + /** + * Set whether the paginator will use an output walker + * + * @param bool|null $useOutputWalkers + * @return $this + */ + public function setUseOutputWalkers($useOutputWalkers) + { + $this->useOutputWalkers = $useOutputWalkers; + return $this; + } + /** * {@inheritdoc} */ @@ -103,7 +126,16 @@ class Paginator implements \Countable, \IteratorAggregate $countQuery->setHint(CountWalker::HINT_DISTINCT, true); } - $countQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\CountWalker')); + if ($this->useOutputWalker($countQuery)) { + $rsm = new ResultSetMapping(); + $rsm->addScalarResult('_dctrn_count', 'count'); + + $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Tools\Pagination\CountOutputWalker'); + $countQuery->setResultSetMapping($rsm); + } else { + $countQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\CountWalker')); + } + $countQuery->setFirstResult(null)->setMaxResults(null); try { @@ -127,9 +159,14 @@ class Paginator implements \Countable, \IteratorAggregate if ($this->fetchJoinCollection) { $subQuery = $this->cloneQuery($this->query); - $subQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker')) - ->setFirstResult($offset) - ->setMaxResults($length); + + if ($this->useOutputWalker($subQuery)) { + $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Tools\Pagination\LimitSubqueryOutputWalker'); + } else { + $subQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker')); + } + + $subQuery->setFirstResult($offset)->setMaxResults($length); $ids = array_map('current', $subQuery->getScalarResult()); @@ -176,5 +213,21 @@ class Paginator implements \Countable, \IteratorAggregate return $cloneQuery; } + + /** + * Determine whether to use an output walker for the query + * + * @param Query $query The query. + * + * @return bool + */ + private function useOutputWalker(Query $query) + { + if ($this->useOutputWalkers === null) { + return (Boolean) $query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER) == false; + } + + return $this->useOutputWalkers; + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/PaginationTest.php b/tests/Doctrine/Tests/ORM/Functional/PaginationTest.php index 990353519..c30ecb2a1 100644 --- a/tests/Doctrine/Tests/ORM/Functional/PaginationTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/PaginationTest.php @@ -24,64 +24,108 @@ class PaginationTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->populate(); } - public function testCountSimpleWithoutJoin() + /** + * @dataProvider useOutputWalkers + */ + public function testCountSimpleWithoutJoin($useOutputWalkers) { $dql = "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u"; $query = $this->_em->createQuery($dql); $paginator = new Paginator($query); - $this->assertEquals(3, count($paginator)); + $paginator->setUseOutputWalkers($useOutputWalkers); + $this->assertCount(3, $paginator); } - public function testCountWithFetchJoin() + /** + * @dataProvider useOutputWalkers + */ + public function testCountWithFetchJoin($useOutputWalkers) { $dql = "SELECT u,g FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.groups g"; $query = $this->_em->createQuery($dql); $paginator = new Paginator($query); - $this->assertEquals(3, count($paginator)); + $paginator->setUseOutputWalkers($useOutputWalkers); + $this->assertCount(3, $paginator); } - public function testIterateSimpleWithoutJoinFetchJoinHandlingOff() + public function testCountComplexWithOutputWalker() + { + $dql = "SELECT g, COUNT(u.id) AS userCount FROM Doctrine\Tests\Models\CMS\CmsGroup g LEFT JOIN g.users u GROUP BY g.id HAVING userCount > 0"; + $query = $this->_em->createQuery($dql); + + $paginator = new Paginator($query); + $paginator->setUseOutputWalkers(true); + $this->assertCount(9, $paginator); + } + + /** + * @dataProvider useOutputWalkers + */ + public function testIterateSimpleWithoutJoinFetchJoinHandlingOff($useOutputWalkers) { $dql = "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u"; $query = $this->_em->createQuery($dql); $paginator = new Paginator($query, false); - - $data = array(); - foreach ($paginator as $user) { - $data[] = $user; - } - $this->assertEquals(3, count($data)); + $paginator->setUseOutputWalkers($useOutputWalkers); + $this->assertCount(3, $paginator->getIterator()); } - public function testIterateSimpleWithoutJoinFetchJoinHandlingOn() + /** + * @dataProvider useOutputWalkers + */ + public function testIterateSimpleWithoutJoinFetchJoinHandlingOn($useOutputWalkers) { $dql = "SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u"; $query = $this->_em->createQuery($dql); $paginator = new Paginator($query, true); - - $data = array(); - foreach ($paginator as $user) { - $data[] = $user; - } - $this->assertEquals(3, count($data)); + $paginator->setUseOutputWalkers($useOutputWalkers); + $this->assertCount(3, $paginator->getIterator()); } - public function testIterateWithFetchJoin() + /** + * @dataProvider useOutputWalkers + */ + public function testIterateWithFetchJoin($useOutputWalkers) { $dql = "SELECT u,g FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.groups g"; $query = $this->_em->createQuery($dql); $paginator = new Paginator($query, true); + $paginator->setUseOutputWalkers($useOutputWalkers); + $this->assertCount(3, $paginator->getIterator()); + } - $data = array(); - foreach ($paginator as $user) { - $data[] = $user; - } - $this->assertEquals(3, count($data)); + public function testIterateComplexWithOutputWalker() + { + $dql = "SELECT g, COUNT(u.id) AS userCount FROM Doctrine\Tests\Models\CMS\CmsGroup g LEFT JOIN g.users u GROUP BY g.id HAVING userCount > 0"; + $query = $this->_em->createQuery($dql); + + $paginator = new Paginator($query); + $paginator->setUseOutputWalkers(true); + $this->assertCount(9, $paginator->getIterator()); + } + + public function testDetectOutputWalker() + { + // This query works using the output walkers but causes an exception using the TreeWalker + $dql = "SELECT g, COUNT(u.id) AS userCount FROM Doctrine\Tests\Models\CMS\CmsGroup g LEFT JOIN g.users u GROUP BY g.id HAVING userCount > 0"; + $query = $this->_em->createQuery($dql); + + // If the Paginator detects the custom output walker it should fall back to using the + // Tree walkers for pagination, which leads to an exception. If the query works, the output walkers were used + $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Query\SqlWalker'); + $paginator = new Paginator($query); + + $this->setExpectedException( + 'RuntimeException', + 'Cannot count query that uses a HAVING clause. Use the output walkers for pagination' + ); + + count($paginator); } public function populate() @@ -102,4 +146,12 @@ class PaginationTest extends \Doctrine\Tests\OrmFunctionalTestCase } $this->_em->flush(); } + + public function useOutputWalkers() + { + return array( + array(true), + array(false), + ); + } } diff --git a/tests/Doctrine/Tests/ORM/Tools/Pagination/CountOutputWalkerTest.php b/tests/Doctrine/Tests/ORM/Tools/Pagination/CountOutputWalkerTest.php new file mode 100644 index 000000000..550abaafb --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Pagination/CountOutputWalkerTest.php @@ -0,0 +1,45 @@ +entityManager->createQuery( + 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN p.category c JOIN p.author a'); + $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Tools\Pagination\CountOutputWalker'); + $query->setFirstResult(null)->setMaxResults(null); + + $this->assertEquals( + "SELECT COUNT(*) AS _dctrn_count FROM (SELECT DISTINCT id0 FROM (SELECT b0_.id AS id0, c1_.id AS id1, a2_.id AS id2, a2_.name AS name3, b0_.author_id AS author_id4, b0_.category_id AS category_id5 FROM BlogPost b0_ INNER JOIN Category c1_ ON b0_.category_id = c1_.id INNER JOIN Author a2_ ON b0_.author_id = a2_.id) AS _dctrn_result) AS _dctrn_table", $query->getSql() + ); + } + + public function testCountQuery_MixedResultsWithName() + { + $query = $this->entityManager->createQuery( + 'SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a'); + $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Tools\Pagination\CountOutputWalker'); + $query->setFirstResult(null)->setMaxResults(null); + + $this->assertEquals( + "SELECT COUNT(*) AS _dctrn_count FROM (SELECT DISTINCT id0 FROM (SELECT a0_.id AS id0, a0_.name AS name1, sum(a0_.name) AS sclr2 FROM Author a0_) AS _dctrn_result) AS _dctrn_table", $query->getSql() + ); + } + + public function testCountQuery_Having() + { + $query = $this->entityManager->createQuery( + 'SELECT g, u, count(u.id) AS userCount FROM Doctrine\Tests\ORM\Tools\Pagination\Group g LEFT JOIN g.users u GROUP BY g.id HAVING userCount > 0'); + $query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Tools\Pagination\CountOutputWalker'); + $query->setFirstResult(null)->setMaxResults(null); + + $this->assertEquals( + "SELECT COUNT(*) AS _dctrn_count FROM (SELECT DISTINCT id1 FROM (SELECT count(u0_.id) AS sclr0, g1_.id AS id1, u0_.id AS id2 FROM groups g1_ LEFT JOIN user_group u2_ ON g1_.id = u2_.group_id LEFT JOIN User u0_ ON u0_.id = u2_.user_id GROUP BY g1_.id HAVING sclr0 > 0) AS _dctrn_result) AS _dctrn_table", $query->getSql() + ); + } +} + diff --git a/tests/Doctrine/Tests/ORM/Tools/Pagination/CountWalkerTest.php b/tests/Doctrine/Tests/ORM/Tools/Pagination/CountWalkerTest.php index 816339df0..405b63a1f 100644 --- a/tests/Doctrine/Tests/ORM/Tools/Pagination/CountWalkerTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/Pagination/CountWalkerTest.php @@ -13,66 +13,82 @@ class CountWalkerTest extends PaginationTestCase public function testCountQuery() { $query = $this->entityManager->createQuery( - 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN p.category c JOIN p.author a'); + 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN p.category c JOIN p.author a'); $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\CountWalker')); $query->setHint(CountWalker::HINT_DISTINCT, true); $query->setFirstResult(null)->setMaxResults(null); $this->assertEquals( - "SELECT count(DISTINCT b0_.id) AS sclr0 FROM BlogPost b0_ INNER JOIN Category c1_ ON b0_.category_id = c1_.id INNER JOIN Author a2_ ON b0_.author_id = a2_.id", $query->getSql() + "SELECT count(DISTINCT b0_.id) AS sclr0 FROM BlogPost b0_ INNER JOIN Category c1_ ON b0_.category_id = c1_.id INNER JOIN Author a2_ ON b0_.author_id = a2_.id", $query->getSql() ); } public function testCountQuery_MixedResultsWithName() { $query = $this->entityManager->createQuery( - 'SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a'); + 'SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a'); $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\CountWalker')); $query->setHint(CountWalker::HINT_DISTINCT, true); $query->setFirstResult(null)->setMaxResults(null); $this->assertEquals( - "SELECT count(DISTINCT a0_.id) AS sclr0 FROM Author a0_", $query->getSql() + "SELECT count(DISTINCT a0_.id) AS sclr0 FROM Author a0_", $query->getSql() ); } public function testCountQuery_KeepsGroupBy() { $query = $this->entityManager->createQuery( - 'SELECT b FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost b GROUP BY b.id'); + 'SELECT b FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost b GROUP BY b.id'); $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\CountWalker')); $query->setHint(CountWalker::HINT_DISTINCT, true); $query->setFirstResult(null)->setMaxResults(null); $this->assertEquals( - "SELECT count(DISTINCT b0_.id) AS sclr0 FROM BlogPost b0_ GROUP BY b0_.id", $query->getSql() + "SELECT count(DISTINCT b0_.id) AS sclr0 FROM BlogPost b0_ GROUP BY b0_.id", $query->getSql() ); } public function testCountQuery_RemovesOrderBy() { $query = $this->entityManager->createQuery( - 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN p.category c JOIN p.author a ORDER BY a.name'); + 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN p.category c JOIN p.author a ORDER BY a.name'); $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\CountWalker')); $query->setHint(CountWalker::HINT_DISTINCT, true); $query->setFirstResult(null)->setMaxResults(null); $this->assertEquals( - "SELECT count(DISTINCT b0_.id) AS sclr0 FROM BlogPost b0_ INNER JOIN Category c1_ ON b0_.category_id = c1_.id INNER JOIN Author a2_ ON b0_.author_id = a2_.id", $query->getSql() + "SELECT count(DISTINCT b0_.id) AS sclr0 FROM BlogPost b0_ INNER JOIN Category c1_ ON b0_.category_id = c1_.id INNER JOIN Author a2_ ON b0_.author_id = a2_.id", $query->getSql() ); } public function testCountQuery_RemovesLimits() { $query = $this->entityManager->createQuery( - 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN p.category c JOIN p.author a'); + 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN p.category c JOIN p.author a'); $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\CountWalker')); $query->setHint(CountWalker::HINT_DISTINCT, true); $query->setFirstResult(null)->setMaxResults(null); $this->assertEquals( - "SELECT count(DISTINCT b0_.id) AS sclr0 FROM BlogPost b0_ INNER JOIN Category c1_ ON b0_.category_id = c1_.id INNER JOIN Author a2_ ON b0_.author_id = a2_.id", $query->getSql() + "SELECT count(DISTINCT b0_.id) AS sclr0 FROM BlogPost b0_ INNER JOIN Category c1_ ON b0_.category_id = c1_.id INNER JOIN Author a2_ ON b0_.author_id = a2_.id", $query->getSql() ); } + + public function testCountQuery_HavingException() + { + $query = $this->entityManager->createQuery( + "SELECT g, COUNT(u.id) AS userCount FROM Doctrine\Tests\Models\CMS\CmsGroup g LEFT JOIN g.users u GROUP BY g.id HAVING userCount > 0" + ); + $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\CountWalker')); + $query->setFirstResult(null)->setMaxResults(null); + + $this->setExpectedException( + 'RuntimeException', + 'Cannot count query that uses a HAVING clause. Use the output walkers for pagination' + ); + + $query->getSql(); + } } diff --git a/tests/Doctrine/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php b/tests/Doctrine/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php new file mode 100644 index 000000000..712fb5f62 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Pagination/LimitSubqueryOutputWalkerTest.php @@ -0,0 +1,33 @@ +entityManager->createQuery( + 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a'); + $limitQuery = clone $query; + $limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Tools\Pagination\LimitSubqueryOutputWalker'); + + $this->assertEquals( + "SELECT DISTINCT id0 FROM (SELECT m0_.id AS id0, c1_.id AS id1, a2_.id AS id2, a2_.name AS name3, m0_.author_id AS author_id4, m0_.category_id AS category_id5 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id) AS _dctrn_result", $limitQuery->getSql() + ); + } + + public function testCountQuery_MixedResultsWithName() + { + $query = $this->entityManager->createQuery( + 'SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a'); + $limitQuery = clone $query; + $limitQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Tools\Pagination\LimitSubqueryOutputWalker'); + + $this->assertEquals( + "SELECT DISTINCT id0 FROM (SELECT a0_.id AS id0, a0_.name AS name1, sum(a0_.name) AS sclr2 FROM Author a0_) AS _dctrn_result", $limitQuery->getSql() + ); + } +} + diff --git a/tests/Doctrine/Tests/ORM/Tools/Pagination/LimitSubqueryWalkerTest.php b/tests/Doctrine/Tests/ORM/Tools/Pagination/LimitSubqueryWalkerTest.php index f166dddb3..2fd6046dd 100644 --- a/tests/Doctrine/Tests/ORM/Tools/Pagination/LimitSubqueryWalkerTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/Pagination/LimitSubqueryWalkerTest.php @@ -12,24 +12,24 @@ class LimitSubqueryWalkerTest extends PaginationTestCase public function testLimitSubquery() { $query = $this->entityManager->createQuery( - 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a'); + 'SELECT p, c, a FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN p.category c JOIN p.author a'); $limitQuery = clone $query; $limitQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker')); $this->assertEquals( - "SELECT DISTINCT m0_.id AS id0 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id", $limitQuery->getSql() + "SELECT DISTINCT m0_.id AS id0 FROM MyBlogPost m0_ INNER JOIN Category c1_ ON m0_.category_id = c1_.id INNER JOIN Author a2_ ON m0_.author_id = a2_.id", $limitQuery->getSql() ); } public function testCountQuery_MixedResultsWithName() { $query = $this->entityManager->createQuery( - 'SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a'); + 'SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a'); $limitQuery = clone $query; $limitQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker')); $this->assertEquals( - "SELECT DISTINCT a0_.id AS id0, sum(a0_.name) AS sclr1 FROM Author a0_", $limitQuery->getSql() + "SELECT DISTINCT a0_.id AS id0, sum(a0_.name) AS sclr1 FROM Author a0_", $limitQuery->getSql() ); } } diff --git a/tests/Doctrine/Tests/ORM/Tools/Pagination/WhereInWalkerTest.php b/tests/Doctrine/Tests/ORM/Tools/Pagination/WhereInWalkerTest.php index 330e64ed1..b813c625b 100644 --- a/tests/Doctrine/Tests/ORM/Tools/Pagination/WhereInWalkerTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/Pagination/WhereInWalkerTest.php @@ -13,98 +13,98 @@ class WhereInWalkerTest extends PaginationTestCase public function testWhereInQuery_NoWhere() { $query = $this->entityManager->createQuery( - 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g' + 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g' ); $whereInQuery = clone $query; $whereInQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\WhereInWalker')); $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, 10); $this->assertEquals( - "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() + "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() ); } public function testCountQuery_MixedResultsWithName() { $query = $this->entityManager->createQuery( - 'SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a' + 'SELECT a, sum(a.name) as foo FROM Doctrine\Tests\ORM\Tools\Pagination\Author a' ); $whereInQuery = clone $query; $whereInQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\WhereInWalker')); $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, 10); $this->assertEquals( - "SELECT a0_.id AS id0, a0_.name AS name1, sum(a0_.name) AS sclr2 FROM Author a0_ WHERE a0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() + "SELECT a0_.id AS id0, a0_.name AS name1, sum(a0_.name) AS sclr2 FROM Author a0_ WHERE a0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() ); } public function testWhereInQuery_SingleWhere() { $query = $this->entityManager->createQuery( - 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE 1 = 1' + 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE 1 = 1' ); $whereInQuery = clone $query; $whereInQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\WhereInWalker')); $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, 10); $this->assertEquals( - "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE 1 = 1 AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() + "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE 1 = 1 AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() ); } public function testWhereInQuery_MultipleWhereWithAnd() { $query = $this->entityManager->createQuery( - 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE 1 = 1 AND 2 = 2' + 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE 1 = 1 AND 2 = 2' ); $whereInQuery = clone $query; $whereInQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\WhereInWalker')); $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, 10); $this->assertEquals( - "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE 1 = 1 AND 2 = 2 AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() + "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE 1 = 1 AND 2 = 2 AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() ); } public function testWhereInQuery_MultipleWhereWithOr() { $query = $this->entityManager->createQuery( - 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE 1 = 1 OR 2 = 2' + 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE 1 = 1 OR 2 = 2' ); $whereInQuery = clone $query; $whereInQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\WhereInWalker')); $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, 10); $this->assertEquals( - "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE (1 = 1 OR 2 = 2) AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() + "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE (1 = 1 OR 2 = 2) AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() ); } public function testWhereInQuery_MultipleWhereWithMixed_1() { $query = $this->entityManager->createQuery( - 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE (1 = 1 OR 2 = 2) AND 3 = 3' + 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE (1 = 1 OR 2 = 2) AND 3 = 3' ); $whereInQuery = clone $query; $whereInQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\WhereInWalker')); $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, 10); $this->assertEquals( - "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE (1 = 1 OR 2 = 2) AND 3 = 3 AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() + "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE (1 = 1 OR 2 = 2) AND 3 = 3 AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() ); } public function testWhereInQuery_MultipleWhereWithMixed_2() { $query = $this->entityManager->createQuery( - 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE 1 = 1 AND 2 = 2 OR 3 = 3' + 'SELECT u, g FROM Doctrine\Tests\ORM\Tools\Pagination\User u JOIN u.groups g WHERE 1 = 1 AND 2 = 2 OR 3 = 3' ); $whereInQuery = clone $query; $whereInQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\WhereInWalker')); $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, 10); $this->assertEquals( - "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE (1 = 1 AND 2 = 2 OR 3 = 3) AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() + "SELECT u0_.id AS id0, g1_.id AS id1 FROM User u0_ INNER JOIN user_group u2_ ON u0_.id = u2_.user_id INNER JOIN groups g1_ ON g1_.id = u2_.group_id WHERE (1 = 1 AND 2 = 2 OR 3 = 3) AND u0_.id IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", $whereInQuery->getSql() ); }