Completed
Pull Request — master (#6354)
by COLE
10:36
created

LimitSubqueryOutputWalker::preserveSqlOrdering()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 22
c 0
b 0
f 0
ccs 9
cts 9
cp 1
rs 9.2
cc 2
eloc 10
nc 2
nop 4
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Tools\Pagination;
21
22
use Doctrine\DBAL\Platforms\DB2Platform;
23
use Doctrine\DBAL\Platforms\OraclePlatform;
24
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
25
use Doctrine\DBAL\Platforms\SQLAnywherePlatform;
26
use Doctrine\DBAL\Platforms\SQLServerPlatform;
27
use Doctrine\ORM\Query\AST\OrderByClause;
28
use Doctrine\ORM\Query\AST\PartialObjectExpression;
29
use Doctrine\ORM\Query\AST\SelectExpression;
30
use Doctrine\ORM\Query\SqlWalker;
31
use Doctrine\ORM\Query\AST\SelectStatement;
32
33
/**
34
 * Wraps the query in order to select root entity IDs for pagination.
35
 *
36
 * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
37
 * SELECT DISTINCT <id> FROM (<original SQL>) LIMIT x OFFSET y
38
 *
39
 * Works with composite keys but cannot deal with queries that have multiple
40
 * root entities (e.g. `SELECT f, b from Foo, Bar`)
41
 *
42
 * @author Sander Marechal <[email protected]>
43
 */
44
class LimitSubqueryOutputWalker extends SqlWalker
45
{
46
    /**
47
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
48
     */
49
    private $platform;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
50
51
    /**
52
     * @var \Doctrine\ORM\Query\ResultSetMapping
53
     */
54
    private $rsm;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
55
56
    /**
57
     * @var array
58
     */
59
    private $queryComponents;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
60
61
    /**
62
     * @var int
63
     */
64
    private $firstResult;
65
66
    /**
67
     * @var int
68
     */
69
    private $maxResults;
70
71
    /**
72
     * @var \Doctrine\ORM\EntityManager
73
     */
74
    private $em;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
75
76
    /**
77
     * The quote strategy.
78
     *
79
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
80
     */
81
    private $quoteStrategy;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
82
83
    /**
84
     * @var array
85
     */
86
    private $orderByPathExpressions = [];
87
88
    /**
89
     * @var bool We don't want to add path expressions from sub-selects into the select clause of the containing query.
90
     *           This state flag simply keeps track on whether we are walking on a subquery or not
91
     */
92
    private $inSubSelect = false;
93
94
    /**
95
     * Constructor.
96
     *
97
     * Stores various parameters that are otherwise unavailable
98
     * because Doctrine\ORM\Query\SqlWalker keeps everything private without
99
     * accessors.
100
     *
101
     * @param \Doctrine\ORM\Query              $query
102
     * @param \Doctrine\ORM\Query\ParserResult $parserResult
103
     * @param array                            $queryComponents
104
     */
105 26
    public function __construct($query, $parserResult, array $queryComponents)
106
    {
107 26
        $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
108 26
        $this->rsm = $parserResult->getResultSetMapping();
109 26
        $this->queryComponents = $queryComponents;
110
111
        // Reset limit and offset
112 26
        $this->firstResult = $query->getFirstResult();
113 26
        $this->maxResults = $query->getMaxResults();
114 26
        $query->setFirstResult(null)->setMaxResults(null);
115
116 26
        $this->em               = $query->getEntityManager();
117 26
        $this->quoteStrategy    = $this->em->getConfiguration()->getQuoteStrategy();
118
119 26
        parent::__construct($query, $parserResult, $queryComponents);
120 26
    }
121
122
    /**
123
     * Check if the platform supports the ROW_NUMBER window function.
124
     *
125
     * @return bool
126
     */
127 26
    private function platformSupportsRowNumber()
128
    {
129 26
        return $this->platform instanceof PostgreSqlPlatform
130 19
            || $this->platform instanceof SQLServerPlatform
131 19
            || $this->platform instanceof OraclePlatform
132 13
            || $this->platform instanceof SQLAnywherePlatform
133 13
            || $this->platform instanceof DB2Platform
134 13
            || (method_exists($this->platform, 'supportsRowNumberFunction')
135 26
                && $this->platform->supportsRowNumberFunction());
0 ignored issues
show
Bug introduced by
The method supportsRowNumberFunction() does not seem to exist on object<Doctrine\DBAL\Platforms\AbstractPlatform>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
136
    }
137
138
    /**
139
     * Rebuilds a select statement's order by clause for use in a
140
     * ROW_NUMBER() OVER() expression.
141
     *
142
     * @param SelectStatement $AST
143
     */
144 11
    private function rebuildOrderByForRowNumber(SelectStatement $AST)
145
    {
146 11
        $orderByClause = $AST->orderByClause;
147 11
        $selectAliasToExpressionMap = [];
148
        // Get any aliases that are available for select expressions.
149 11
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
150 11
            $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
151
        }
152
153
        // Rebuild string orderby expressions to use the select expression they're referencing
154 11
        foreach ($orderByClause->orderByItems as $orderByItem) {
155 11
            if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) {
156 11
                $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression];
157
            }
158
        }
159 11
        $func = new RowNumberOverFunction('dctrn_rownum');
160 11
        $func->orderByClause = $AST->orderByClause;
161 11
        $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true);
162
163
        // No need for an order by clause, we'll order by rownum in the outer query.
164 11
        $AST->orderByClause = null;
165 11
    }
166
167
    /**
168
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
169
     *
170
     * @param SelectStatement $AST
171
     *
172
     * @return string
173
     *
174
     * @throws \RuntimeException
175
     */
176 26
    public function walkSelectStatement(SelectStatement $AST)
177
    {
178 26
        if ($this->platformSupportsRowNumber()) {
179 13
            return $this->walkSelectStatementWithRowNumber($AST);
180
        }
181 13
        return $this->walkSelectStatementWithoutRowNumber($AST);
182
    }
183
184
    /**
185
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
186
     * This method is for use with platforms which support ROW_NUMBER.
187
     *
188
     * @param SelectStatement $AST
189
     *
190
     * @return string
191
     *
192
     * @throws \RuntimeException
193
     */
194 13
    public function walkSelectStatementWithRowNumber(SelectStatement $AST)
195
    {
196 13
        $hasOrderBy = false;
197 13
        $outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
198 13
        $orderGroupBy = '';
199 13
        if ($AST->orderByClause instanceof OrderByClause) {
200 11
            $hasOrderBy = true;
201 11
            $this->rebuildOrderByForRowNumber($AST);
202
        }
203
204 13
        $innerSql = $this->getInnerSQL($AST);
205
206 13
        $sqlIdentifier = $this->getSQLIdentifier($AST);
207
208 13
        if ($hasOrderBy) {
209 11
            $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier);
210 11
            $sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum';
211
        }
212
213
        // Build the counter query
214 13
        $sql = sprintf(
215 13
            'SELECT DISTINCT %s FROM (%s) dctrn_result',
216 13
            implode(', ', $sqlIdentifier),
217
            $innerSql
218
        );
219
220 13
        if ($hasOrderBy) {
221 11
            $sql .= $orderGroupBy . $outerOrderBy;
222
        }
223
224
        // Apply the limit and offset.
225 13
        $sql = $this->platform->modifyLimitQuery(
226
            $sql,
227 13
            $this->maxResults,
228 13
            $this->firstResult
229
        );
230
231
        // Add the columns to the ResultSetMapping. It's not really nice but
232
        // it works. Preferably I'd clear the RSM or simply create a new one
233
        // but that is not possible from inside the output walker, so we dirty
234
        // up the one we have.
235 13
        foreach ($sqlIdentifier as $property => $alias) {
236 13
            $this->rsm->addScalarResult($alias, $property);
237
        }
238
239 13
        return $sql;
240
    }
241
242
    /**
243
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
244
     * This method is for platforms which DO NOT support ROW_NUMBER.
245
     *
246
     * @param SelectStatement $AST
247
     * @param bool $addMissingItemsFromOrderByToSelect
248
     *
249
     * @return string
250
     *
251
     * @throws \RuntimeException
252
     */
253 13
    public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
254
    {
255
        // We don't want to call this recursively!
256 13
        if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
257
            // In the case of ordering a query by columns from joined tables, we
258
            // must add those columns to the select clause of the query BEFORE
259
            // the SQL is generated.
260 10
            $this->addMissingItemsFromOrderByToSelect($AST);
261
        }
262
263
        // Remove order by clause from the inner query
264
        // It will be re-appended in the outer select generated by this method
265 13
        $orderByClause = $AST->orderByClause;
266 13
        $AST->orderByClause = null;
267
268 13
        $innerSql = $this->getInnerSQL($AST);
269
270 13
        $sqlIdentifier = $this->getSQLIdentifier($AST);
271
272
        // Build the counter query
273 13
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result',
274 13
            implode(', ', $sqlIdentifier), $innerSql);
275
276
        // http://www.doctrine-project.org/jira/browse/DDC-1958
277 13
        $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
0 ignored issues
show
Bug introduced by
It seems like $orderByClause defined by $AST->orderByClause on line 265 can be null; however, Doctrine\ORM\Tools\Pagin...::preserveSqlOrdering() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
278
279
        // Apply the limit and offset.
280 13
        $sql = $this->platform->modifyLimitQuery(
281 13
            $sql, $this->maxResults, $this->firstResult
282
        );
283
284
        // Add the columns to the ResultSetMapping. It's not really nice but
285
        // it works. Preferably I'd clear the RSM or simply create a new one
286
        // but that is not possible from inside the output walker, so we dirty
287
        // up the one we have.
288 13
        foreach ($sqlIdentifier as $property => $alias) {
289 13
            $this->rsm->addScalarResult($alias, $property);
290
        }
291
292
        // Restore orderByClause
293 13
        $AST->orderByClause = $orderByClause;
294
295 13
        return $sql;
296
    }
297
298
    /**
299
     * Finds all PathExpressions in an AST's OrderByClause, and ensures that
300
     * the referenced fields are present in the SelectClause of the passed AST.
301
     *
302
     * @param SelectStatement $AST
303
     */
304 10
    private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
305
    {
306 10
        $this->orderByPathExpressions = [];
307
308
        // We need to do this in another walker because otherwise we'll end up
309
        // polluting the state of this one.
310 10
        $walker = clone $this;
311
312
        // This will populate $orderByPathExpressions via
313
        // LimitSubqueryOutputWalker::walkPathExpression, which will be called
314
        // as the select statement is walked. We'll end up with an array of all
315
        // path expressions referenced in the query.
316 10
        $walker->walkSelectStatementWithoutRowNumber($AST, false);
317 10
        $orderByPathExpressions = $walker->getOrderByPathExpressions();
318
319
        // Get a map of referenced identifiers to field names.
320 10
        $selects = [];
321 10
        foreach ($orderByPathExpressions as $pathExpression) {
322 7
            $idVar = $pathExpression->identificationVariable;
323 7
            $field = $pathExpression->field;
324 7
            if (!isset($selects[$idVar])) {
325 7
                $selects[$idVar] = [];
326
            }
327 7
            $selects[$idVar][$field] = true;
328
        }
329
330
        // Loop the select clause of the AST and exclude items from $select
331
        // that are already being selected in the query.
332 10
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
333 10
            if ($selectExpression instanceof SelectExpression) {
334 10
                $idVar = $selectExpression->expression;
335 10
                if (!is_string($idVar)) {
336 3
                    continue;
337
                }
338 10
                $field = $selectExpression->fieldIdentificationVariable;
339 10
                if ($field === null) {
340
                    // No need to add this select, as we're already fetching the whole object.
341 10
                    unset($selects[$idVar]);
342
                } else {
343 10
                    unset($selects[$idVar][$field]);
344
                }
345
            }
346
        }
347
348
        // Add select items which were not excluded to the AST's select clause.
349 10
        foreach ($selects as $idVar => $fields) {
350 3
            $AST->selectClause->selectExpressions[] = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
351
        }
352 10
    }
353
354
    /**
355
     * Generates new SQL for statements with an order by clause
356
     *
357
     * @param array           $sqlIdentifier
358
     * @param string          $innerSql
359
     * @param string          $sql
360
     * @param OrderByClause   $orderByClause
361
     *
362
     * @return string
363
     */
364 13
    private function preserveSqlOrdering(array $sqlIdentifier, $innerSql, $sql, $orderByClause)
365
    {
366
        // If the sql statement has an order by clause, we need to wrap it in a new select distinct
367
        // statement
368 13
        if (! $orderByClause instanceof OrderByClause) {
369 3
            return $sql;
370
        }
371
372
        // Rebuild the order by clause to work in the scope of the new select statement
373
        /* @var array $orderBy an array of rebuilt order by items */
374 10
        $orderBy = $this->rebuildOrderByClauseForOuterScope($orderByClause);
375
376
        // Build the select distinct statement
377 10
        $sql = sprintf(
378 10
            'SELECT DISTINCT %s FROM (%s) dctrn_result ORDER BY %s',
379 10
            implode(', ', $sqlIdentifier),
380
            $innerSql,
381 10
            implode(', ', $orderBy)
382
        );
383
384 10
        return $sql;
385
    }
386
387
    /**
388
     * Generates a new order by clause that works in the scope of a select query wrapping the original
389
     *
390
     * @param OrderByClause $orderByClause
391
     * @return array
392
     */
393 10
    private function rebuildOrderByClauseForOuterScope(OrderByClause $orderByClause)
394
    {
395
        $dqlAliasToSqlTableAliasMap
396
            = $searchPatterns
397
            = $replacements
398
            = $dqlAliasToClassMap
399
            = $selectListAdditions
0 ignored issues
show
Unused Code introduced by
$selectListAdditions is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
400
            = $orderByItems
401 10
            = [];
402
403
        // Generate DQL alias -> SQL table alias mapping
404 10
        foreach(array_keys($this->rsm->aliasMap) as $dqlAlias) {
405 10
            $dqlAliasToClassMap[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata'];
406 10
            $dqlAliasToSqlTableAliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
407
        }
408
409
        // Pattern to find table path expressions in the order by clause
410 10
        $fieldSearchPattern = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
411
412
        // Generate search patterns for each field's path expression in the order by clause
413 10
        foreach($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
414 10
            $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
415 10
            $class = $dqlAliasToClassMap[$dqlAliasForFieldAlias];
416
417
            // If the field is from a joined child table, we won't be ordering
418
            // on it.
419 10
            if (!isset($class->fieldMappings[$fieldName])) {
420
                continue;
421
            }
422
423 10
            $fieldMapping = $class->fieldMappings[$fieldName];
424
425
            // Get the proper column name as will appear in the select list
426 10
            $columnName = $this->quoteStrategy->getColumnName(
427
                $fieldName,
428 10
                $dqlAliasToClassMap[$dqlAliasForFieldAlias],
429 10
                $this->em->getConnection()->getDatabasePlatform()
430
            );
431
432
            // Get the SQL table alias for the entity and field
433 10
            $sqlTableAliasForFieldAlias = $dqlAliasToSqlTableAliasMap[$dqlAliasForFieldAlias];
434 10
            if (isset($fieldMapping['declared']) && $fieldMapping['declared'] !== $class->name) {
435
                // Field was declared in a parent class, so we need to get the proper SQL table alias
436
                // for the joined parent table.
437 1
                $otherClassMetadata = $this->em->getClassMetadata($fieldMapping['declared']);
438 1
                if (!$otherClassMetadata->isMappedSuperclass) {
439
                    $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias);
440
                }
441
            }
442
443
            // Compose search/replace patterns
444 10
            $searchPatterns[] = sprintf($fieldSearchPattern, $sqlTableAliasForFieldAlias, $columnName);
445 10
            $replacements[] = $fieldAlias;
446
        }
447
448 10
        foreach($orderByClause->orderByItems as $orderByItem) {
449
            // Walk order by item to get string representation of it
450 10
            $orderByItemString = $this->walkOrderByItem($orderByItem);
451
452
            // Replace path expressions in the order by clause with their column alias
453 10
            $orderByItemString = preg_replace($searchPatterns, $replacements, $orderByItemString);
454
455 10
            $orderByItems[] = $orderByItemString;
456
        }
457
458 10
        return $orderByItems;
459
    }
460
461
    /**
462
     * getter for $orderByPathExpressions
463
     *
464
     * @return array
465
     */
466 10
    public function getOrderByPathExpressions()
467
    {
468 10
        return $this->orderByPathExpressions;
469
    }
470
471
    /**
472
     * @param SelectStatement $AST
473
     *
474
     * @return string
475
     *
476
     * @throws \Doctrine\ORM\OptimisticLockException
477
     * @throws \Doctrine\ORM\Query\QueryException
478
     */
479 26
    private function getInnerSQL(SelectStatement $AST)
480
    {
481
        // Set every select expression as visible(hidden = false) to
482
        // make $AST have scalar mappings properly - this is relevant for referencing selected
483
        // fields from outside the subquery, for example in the ORDER BY segment
484 26
        $hiddens = [];
485
486 26
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
487 26
            $hiddens[$idx] = $expr->hiddenAliasResultVariable;
488 26
            $expr->hiddenAliasResultVariable = false;
489
        }
490
491 26
        $innerSql = parent::walkSelectStatement($AST);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (walkSelectStatement() instead of getInnerSQL()). Are you sure this is correct? If so, you might want to change this to $this->walkSelectStatement().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
492
493
        // Restore hiddens
494 26
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
495 26
            $expr->hiddenAliasResultVariable = $hiddens[$idx];
496
        }
497
498 26
        return $innerSql;
499
    }
500
501
    /**
502
     * @param SelectStatement $AST
503
     *
504
     * @return array
505
     */
506 26
    private function getSQLIdentifier(SelectStatement $AST)
507
    {
508
        // Find out the SQL alias of the identifier column of the root entity.
509
        // It may be possible to make this work with multiple root entities but that
510
        // would probably require issuing multiple queries or doing a UNION SELECT.
511
        // So for now, it's not supported.
512
513
        // Get the root entity and alias from the AST fromClause.
514 26
        $from = $AST->fromClause->identificationVariableDeclarations;
515 26
        if (count($from) !== 1) {
516
            throw new \RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
517
        }
518
519 26
        $fromRoot       = reset($from);
520 26
        $rootAlias      = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
521 26
        $rootClass      = $this->queryComponents[$rootAlias]['metadata'];
522 26
        $rootIdentifier = $rootClass->identifier;
523
524
        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
525 26
        $sqlIdentifier = [];
526 26
        foreach ($rootIdentifier as $property) {
527 26
            if (isset($rootClass->fieldMappings[$property])) {
528 26
                foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) {
529 26
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
530 26
                        $sqlIdentifier[$property] = $alias;
531
                    }
532
                }
533
            }
534
535 26
            if (isset($rootClass->associationMappings[$property])) {
536
                $joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];
537
538
                foreach (array_keys($this->rsm->metaMappings, $joinColumn) as $alias) {
539
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
540 26
                        $sqlIdentifier[$property] = $alias;
541
                    }
542
                }
543
            }
544
        }
545
546 26
        if (count($sqlIdentifier) === 0) {
547
            throw new \RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
548
        }
549
550 26
        if (count($rootIdentifier) != count($sqlIdentifier)) {
551
            throw new \RuntimeException(sprintf(
552
                'Not all identifier properties can be found in the ResultSetMapping: %s',
553
                implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
554
            ));
555
        }
556
557 26
        return $sqlIdentifier;
558
    }
559
560
    /**
561
     * {@inheritdoc}
562
     */
563 20
    public function walkPathExpression($pathExpr)
564
    {
565 20
        if (!$this->inSubSelect && !$this->platformSupportsRowNumber() && !in_array($pathExpr, $this->orderByPathExpressions)) {
566 8
            $this->orderByPathExpressions[] = $pathExpr;
567
        }
568
569 20
        return parent::walkPathExpression($pathExpr);
570
    }
571
572
    /**
573
     * {@inheritdoc}
574
     */
575 5
    public function walkSubSelect($subselect)
576
    {
577 5
        $this->inSubSelect = true;
578
579 5
        $sql = parent::walkSubselect($subselect);
580
581 5
        $this->inSubSelect = false;
582
583 5
        return $sql;
584
    }
585
}
586