7 * This source file is subject to the new BSD license that is bundled
8 * with this package in the file LICENSE.txt.
9 * If you did not receive a copy of the license and are unable to
10 * obtain it through the world-wide-web, please send an email
11 * to kontakt@beberlei.de so I can send you a copy immediately.
14 namespace Doctrine\ORM\Tools\Pagination;
16 use Doctrine\ORM\Query\SqlWalker,
17 Doctrine\ORM\Query\AST\SelectStatement;
20 * Wrap the query in order to accurately count the root objects
22 * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
23 * SELECT COUNT(*) (SELECT DISTINCT <id> FROM (<original SQL>))
25 * Works with composite keys but cannot deal with queries that have multiple
26 * root entities (e.g. `SELECT f, b from Foo, Bar`)
28 * @author Sander Marechal <s.marechal@jejik.com>
30 class CountOutputWalker extends SqlWalker
33 * @var Doctrine\DBAL\Platforms\AbstractPlatform
38 * @var Doctrine\ORM\Query\ResultSetMapping
45 private $queryComponents;
48 * Constructor. Stores various parameters that are otherwise unavailable
49 * because Doctrine\ORM\Query\SqlWalker keeps everything private without
52 * @param Doctrine\ORM\Query $query
53 * @param Doctrine\ORM\Query\ParserResult $parserResult
54 * @param array $queryComponents
56 public function __construct($query, $parserResult, array $queryComponents)
58 $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
59 $this->rsm = $parserResult->getResultSetMapping();
60 $this->queryComponents = $queryComponents;
62 parent::__construct($query, $parserResult, $queryComponents);
66 * Walks down a SelectStatement AST node, wrapping it in a COUNT (SELECT DISTINCT)
68 * Note that the ORDER BY clause is not removed. Many SQL implementations (e.g. MySQL)
69 * are able to cache subqueries. By keeping the ORDER BY clause intact, the limitSubQuery
70 * that will most likely be executed next can be read from the native SQL cache.
72 * @param SelectStatement $AST
75 public function walkSelectStatement(SelectStatement $AST)
77 $sql = parent::walkSelectStatement($AST);
79 // Find out the SQL alias of the identifier column of the root entity
80 // It may be possible to make this work with multiple root entities but that
81 // would probably require issuing multiple queries or doing a UNION SELECT
82 // so for now, It's not supported.
84 // Get the root entity and alias from the AST fromClause
85 $from = $AST->fromClause->identificationVariableDeclarations;
86 if (count($from) > 1) {
87 throw new \RuntimeException("Cannot count query which selects two FROM components, cannot make distinction");
90 $rootAlias = $from[0]->rangeVariableDeclaration->aliasIdentificationVariable;
91 $rootClass = $this->queryComponents[$rootAlias]['metadata'];
92 $rootIdentifier = $rootClass->identifier;
94 // For every identifier, find out the SQL alias by combing through the ResultSetMapping
95 $sqlIdentifier = array();
96 foreach ($rootIdentifier as $property) {
97 if (isset($rootClass->fieldMappings[$property])) {
98 foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) {
99 if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
100 $sqlIdentifier[$property] = $alias;
105 if (isset($rootClass->associationMappings[$property])) {
106 $joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];
108 foreach (array_keys($this->rsm->metaMappings, $joinColumn) as $alias) {
109 if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
110 $sqlIdentifier[$property] = $alias;
116 if (count($rootIdentifier) != count($sqlIdentifier)) {
117 throw new \RuntimeException(sprintf(
118 'Not all identifier properties can be found in the ResultSetMapping: %s',
119 implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
123 // Build the counter query
124 return sprintf('SELECT %s AS dctrn_count FROM (SELECT DISTINCT %s FROM (%s) dctrn_result) dctrn_table',
125 $this->platform->getCountExpression('*'),
126 implode(', ', $sqlIdentifier),