Failed Conditions
Push — master ( 668ad4...2fb3cf )
by Marco
17s
created

getOrderByPathExpressions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
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
    private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
47
48
    /**
49
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
50
     */
51
    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...
52
53
    /**
54
     * @var \Doctrine\ORM\Query\ResultSetMapping
55
     */
56
    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...
57
58
    /**
59
     * @var array
60
     */
61
    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...
62
63
    /**
64
     * @var int
65
     */
66
    private $firstResult;
67
68
    /**
69
     * @var int
70
     */
71
    private $maxResults;
72
73
    /**
74
     * @var \Doctrine\ORM\EntityManager
75
     */
76
    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...
77
78
    /**
79
     * The quote strategy.
80
     *
81
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
82
     */
83
    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...
84
85
    /**
86
     * @var array
87
     */
88
    private $orderByPathExpressions = [];
89
90
    /**
91
     * @var bool We don't want to add path expressions from sub-selects into the select clause of the containing query.
92
     *           This state flag simply keeps track on whether we are walking on a subquery or not
93
     */
94
    private $inSubSelect = false;
95
96
    /**
97
     * Constructor.
98
     *
99
     * Stores various parameters that are otherwise unavailable
100
     * because Doctrine\ORM\Query\SqlWalker keeps everything private without
101
     * accessors.
102
     *
103
     * @param \Doctrine\ORM\Query              $query
104
     * @param \Doctrine\ORM\Query\ParserResult $parserResult
105
     * @param array                            $queryComponents
106
     */
107 65
    public function __construct($query, $parserResult, array $queryComponents)
108
    {
109 65
        $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
110 65
        $this->rsm = $parserResult->getResultSetMapping();
111 65
        $this->queryComponents = $queryComponents;
112
113
        // Reset limit and offset
114 65
        $this->firstResult = $query->getFirstResult();
115 65
        $this->maxResults = $query->getMaxResults();
116 65
        $query->setFirstResult(null)->setMaxResults(null);
117
118 65
        $this->em               = $query->getEntityManager();
119 65
        $this->quoteStrategy    = $this->em->getConfiguration()->getQuoteStrategy();
120
121 65
        parent::__construct($query, $parserResult, $queryComponents);
122 65
    }
123
124
    /**
125
     * Check if the platform supports the ROW_NUMBER window function.
126
     *
127
     * @return bool
128
     */
129 65
    private function platformSupportsRowNumber()
130
    {
131 65
        return $this->platform instanceof PostgreSqlPlatform
132 58
            || $this->platform instanceof SQLServerPlatform
133 58
            || $this->platform instanceof OraclePlatform
134 52
            || $this->platform instanceof SQLAnywherePlatform
135 52
            || $this->platform instanceof DB2Platform
136 52
            || (method_exists($this->platform, 'supportsRowNumberFunction')
137 65
                && $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...
138
    }
139
140
    /**
141
     * Rebuilds a select statement's order by clause for use in a
142
     * ROW_NUMBER() OVER() expression.
143
     *
144
     * @param SelectStatement $AST
145
     */
146 11
    private function rebuildOrderByForRowNumber(SelectStatement $AST)
147
    {
148 11
        $orderByClause = $AST->orderByClause;
149 11
        $selectAliasToExpressionMap = [];
150
        // Get any aliases that are available for select expressions.
151 11
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
152 11
            $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
153
        }
154
155
        // Rebuild string orderby expressions to use the select expression they're referencing
156 11
        foreach ($orderByClause->orderByItems as $orderByItem) {
157 11
            if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) {
158 11
                $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression];
159
            }
160
        }
161 11
        $func = new RowNumberOverFunction('dctrn_rownum');
162 11
        $func->orderByClause = $AST->orderByClause;
163 11
        $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true);
164
165
        // No need for an order by clause, we'll order by rownum in the outer query.
166 11
        $AST->orderByClause = null;
167 11
    }
168
169
    /**
170
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
171
     *
172
     * @param SelectStatement $AST
173
     *
174
     * @return string
175
     *
176
     * @throws \RuntimeException
177
     */
178 65
    public function walkSelectStatement(SelectStatement $AST)
179
    {
180 65
        if ($this->platformSupportsRowNumber()) {
181 13
            return $this->walkSelectStatementWithRowNumber($AST);
182
        }
183 52
        return $this->walkSelectStatementWithoutRowNumber($AST);
184
    }
185
186
    /**
187
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
188
     * This method is for use with platforms which support ROW_NUMBER.
189
     *
190
     * @param SelectStatement $AST
191
     *
192
     * @return string
193
     *
194
     * @throws \RuntimeException
195
     */
196 13
    public function walkSelectStatementWithRowNumber(SelectStatement $AST)
197
    {
198 13
        $hasOrderBy = false;
199 13
        $outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
200 13
        $orderGroupBy = '';
201 13
        if ($AST->orderByClause instanceof OrderByClause) {
202 11
            $hasOrderBy = true;
203 11
            $this->rebuildOrderByForRowNumber($AST);
204
        }
205
206 13
        $innerSql = $this->getInnerSQL($AST);
207
208 13
        $sqlIdentifier = $this->getSQLIdentifier($AST);
209
210 13
        if ($hasOrderBy) {
211 11
            $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlIdentifier);
212 11
            $sqlIdentifier[] = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum';
213
        }
214
215
        // Build the counter query
216 13
        $sql = sprintf(
217 13
            'SELECT DISTINCT %s FROM (%s) dctrn_result',
218 13
            implode(', ', $sqlIdentifier),
219 13
            $innerSql
220
        );
221
222 13
        if ($hasOrderBy) {
223 11
            $sql .= $orderGroupBy . $outerOrderBy;
224
        }
225
226
        // Apply the limit and offset.
227 13
        $sql = $this->platform->modifyLimitQuery(
228 13
            $sql,
229 13
            $this->maxResults,
230 13
            $this->firstResult
231
        );
232
233
        // Add the columns to the ResultSetMapping. It's not really nice but
234
        // it works. Preferably I'd clear the RSM or simply create a new one
235
        // but that is not possible from inside the output walker, so we dirty
236
        // up the one we have.
237 13
        foreach ($sqlIdentifier as $property => $alias) {
238 13
            $this->rsm->addScalarResult($alias, $property);
239
        }
240
241 13
        return $sql;
242
    }
243
244
    /**
245
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
246
     * This method is for platforms which DO NOT support ROW_NUMBER.
247
     *
248
     * @param SelectStatement $AST
249
     * @param bool $addMissingItemsFromOrderByToSelect
250
     *
251
     * @return string
252
     *
253
     * @throws \RuntimeException
254
     */
255 52
    public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
256
    {
257
        // We don't want to call this recursively!
258 52
        if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
259
            // In the case of ordering a query by columns from joined tables, we
260
            // must add those columns to the select clause of the query BEFORE
261
            // the SQL is generated.
262 44
            $this->addMissingItemsFromOrderByToSelect($AST);
263
        }
264
265
        // Remove order by clause from the inner query
266
        // It will be re-appended in the outer select generated by this method
267 52
        $orderByClause = $AST->orderByClause;
268 52
        $AST->orderByClause = null;
269
270 52
        $innerSql = $this->getInnerSQL($AST);
271
272 52
        $sqlIdentifier = $this->getSQLIdentifier($AST);
273
274
        // Build the counter query
275 52
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result',
276 52
            implode(', ', $sqlIdentifier), $innerSql);
277
278
        // http://www.doctrine-project.org/jira/browse/DDC-1958
279 52
        $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
280
281
        // Apply the limit and offset.
282 52
        $sql = $this->platform->modifyLimitQuery(
283 52
            $sql, $this->maxResults, $this->firstResult
284
        );
285
286
        // Add the columns to the ResultSetMapping. It's not really nice but
287
        // it works. Preferably I'd clear the RSM or simply create a new one
288
        // but that is not possible from inside the output walker, so we dirty
289
        // up the one we have.
290 52
        foreach ($sqlIdentifier as $property => $alias) {
291 52
            $this->rsm->addScalarResult($alias, $property);
292
        }
293
294
        // Restore orderByClause
295 52
        $AST->orderByClause = $orderByClause;
296
297 52
        return $sql;
298
    }
299
300
    /**
301
     * Finds all PathExpressions in an AST's OrderByClause, and ensures that
302
     * the referenced fields are present in the SelectClause of the passed AST.
303
     *
304
     * @param SelectStatement $AST
305
     */
306 44
    private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
307
    {
308 44
        $this->orderByPathExpressions = [];
309
310
        // We need to do this in another walker because otherwise we'll end up
311
        // polluting the state of this one.
312 44
        $walker = clone $this;
313
314
        // This will populate $orderByPathExpressions via
315
        // LimitSubqueryOutputWalker::walkPathExpression, which will be called
316
        // as the select statement is walked. We'll end up with an array of all
317
        // path expressions referenced in the query.
318 44
        $walker->walkSelectStatementWithoutRowNumber($AST, false);
319 44
        $orderByPathExpressions = $walker->getOrderByPathExpressions();
320
321
        // Get a map of referenced identifiers to field names.
322 44
        $selects = [];
323 44
        foreach ($orderByPathExpressions as $pathExpression) {
324 40
            $idVar = $pathExpression->identificationVariable;
325 40
            $field = $pathExpression->field;
326 40
            if (!isset($selects[$idVar])) {
327 40
                $selects[$idVar] = [];
328
            }
329 40
            $selects[$idVar][$field] = true;
330
        }
331
332
        // Loop the select clause of the AST and exclude items from $select
333
        // that are already being selected in the query.
334 44
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
335 44
            if ($selectExpression instanceof SelectExpression) {
336 44
                $idVar = $selectExpression->expression;
337 44
                if (!is_string($idVar)) {
338 4
                    continue;
339
                }
340 44
                $field = $selectExpression->fieldIdentificationVariable;
341 44
                if ($field === null) {
342
                    // No need to add this select, as we're already fetching the whole object.
343 44
                    unset($selects[$idVar]);
344
                } else {
345 44
                    unset($selects[$idVar][$field]);
346
                }
347
            }
348
        }
349
350
        // Add select items which were not excluded to the AST's select clause.
351 44
        foreach ($selects as $idVar => $fields) {
352 9
            $AST->selectClause->selectExpressions[] = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
353
        }
354 44
    }
355
356
    /**
357
     * Generates new SQL for statements with an order by clause
358
     *
359
     * @param array              $sqlIdentifier
360
     * @param string             $innerSql
361
     * @param string             $sql
362
     * @param OrderByClause|null $orderByClause
363
     *
364
     * @return string
365
     */
366 52
    private function preserveSqlOrdering(
367
        array $sqlIdentifier,
368
        string $innerSql,
369
        string $sql,
370
        ?OrderByClause $orderByClause
371
    ) : string {
372
        // If the sql statement has an order by clause, we need to wrap it in a new select distinct statement
373 52
        if (! $orderByClause) {
374 8
            return $sql;
375
        }
376
377
        // now only select distinct identifier
378 44
        return \sprintf(
379 44
            'SELECT DISTINCT %s FROM (%s) dctrn_result',
380 44
            \implode(', ', $sqlIdentifier),
381 44
            $this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql)
382
        );
383
    }
384
385
    /**
386
     * Generates a new SQL statement for the inner query to keep the correct sorting
387
     *
388
     * @param OrderByClause $orderByClause
389
     * @param array         $identifiers
390
     * @param string        $innerSql
391
     *
392
     * @return string
393
     */
394 44
    private function recreateInnerSql(
395
        OrderByClause $orderByClause,
396
        array $identifiers,
397
        string $innerSql
398
    ) : string {
399 44
        [$searchPatterns, $replacements] = $this->generateSqlAliasReplacements();
0 ignored issues
show
Bug introduced by
The variable $searchPatterns does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $replacements does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
400
401 44
        $orderByItems = [];
402
403 44
        foreach ($orderByClause->orderByItems as $orderByItem) {
404
            // Walk order by item to get string representation of it and
405
            // replace path expressions in the order by clause with their column alias
406 44
            $orderByItemString = \preg_replace(
407 44
                $searchPatterns,
408 44
                $replacements,
409 44
                $this->walkOrderByItem($orderByItem)
410
            );
411
412 44
            $orderByItems[] = $orderByItemString;
413 44
            $identifier     = \substr($orderByItemString, 0, \strrpos($orderByItemString, ' '));
414
415 44
            if (! \in_array($identifier, $identifiers, true)) {
416 44
                $identifiers[] = $identifier;
417
            }
418
        }
419
420 44
        return $sql = \sprintf(
0 ignored issues
show
Unused Code introduced by
$sql 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...
421 44
            'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s',
422 44
            \implode(', ', $identifiers),
423 44
            $innerSql,
424 44
            \implode(', ', $orderByItems)
425
        );
426
    }
427
428
    /**
429
     * @return string[][]
430
     */
431 44
    private function generateSqlAliasReplacements() : array
432
    {
433 44
        $aliasMap = $searchPatterns = $replacements = $metadataList = [];
434
435
        // Generate DQL alias -> SQL table alias mapping
436 44
        foreach (\array_keys($this->rsm->aliasMap) as $dqlAlias) {
437 44
            $metadataList[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata'];
438 44
            $aliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
439
        }
440
441
        // Generate search patterns for each field's path expression in the order by clause
442 44
        foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
443 44
            $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
444 44
            $class = $metadataList[$dqlAliasForFieldAlias];
445
446
            // If the field is from a joined child table, we won't be ordering on it.
447 44
            if (! isset($class->fieldMappings[$fieldName])) {
448 1
                continue;
449
            }
450
451 44
            $fieldMapping = $class->fieldMappings[$fieldName];
452
453
            // Get the proper column name as will appear in the select list
454 44
            $columnName = $this->quoteStrategy->getColumnName(
455 44
                $fieldName,
456 44
                $metadataList[$dqlAliasForFieldAlias],
457 44
                $this->em->getConnection()->getDatabasePlatform()
458
            );
459
460
            // Get the SQL table alias for the entity and field
461 44
            $sqlTableAliasForFieldAlias = $aliasMap[$dqlAliasForFieldAlias];
462
463 44
            if (isset($fieldMapping['declared']) && $fieldMapping['declared'] !== $class->name) {
464
                // Field was declared in a parent class, so we need to get the proper SQL table alias
465
                // for the joined parent table.
466 2
                $otherClassMetadata = $this->em->getClassMetadata($fieldMapping['declared']);
467
468 2
                if (! $otherClassMetadata->isMappedSuperclass) {
469 1
                    $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias);
470
                }
471
            }
472
473
            // Compose search and replace patterns
474 44
            $searchPatterns[] = \sprintf(self::ORDER_BY_PATH_EXPRESSION, $sqlTableAliasForFieldAlias, $columnName);
475 44
            $replacements[]   = $fieldAlias;
476
        }
477
478 44
        return [$searchPatterns, $replacements];
479
    }
480
481
    /**
482
     * getter for $orderByPathExpressions
483
     *
484
     * @return array
485
     */
486 44
    public function getOrderByPathExpressions()
487
    {
488 44
        return $this->orderByPathExpressions;
489
    }
490
491
    /**
492
     * @param SelectStatement $AST
493
     *
494
     * @return string
495
     *
496
     * @throws \Doctrine\ORM\OptimisticLockException
497
     * @throws \Doctrine\ORM\Query\QueryException
498
     */
499 65
    private function getInnerSQL(SelectStatement $AST)
500
    {
501
        // Set every select expression as visible(hidden = false) to
502
        // make $AST have scalar mappings properly - this is relevant for referencing selected
503
        // fields from outside the subquery, for example in the ORDER BY segment
504 65
        $hiddens = [];
505
506 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
507 65
            $hiddens[$idx] = $expr->hiddenAliasResultVariable;
508 65
            $expr->hiddenAliasResultVariable = false;
509
        }
510
511 65
        $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...
512
513
        // Restore hiddens
514 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
515 65
            $expr->hiddenAliasResultVariable = $hiddens[$idx];
516
        }
517
518 65
        return $innerSql;
519
    }
520
521
    /**
522
     * @param SelectStatement $AST
523
     *
524
     * @return array
525
     */
526 65
    private function getSQLIdentifier(SelectStatement $AST)
527
    {
528
        // Find out the SQL alias of the identifier column of the root entity.
529
        // It may be possible to make this work with multiple root entities but that
530
        // would probably require issuing multiple queries or doing a UNION SELECT.
531
        // So for now, it's not supported.
532
533
        // Get the root entity and alias from the AST fromClause.
534 65
        $from = $AST->fromClause->identificationVariableDeclarations;
535 65
        if (count($from) !== 1) {
536
            throw new \RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
537
        }
538
539 65
        $fromRoot       = reset($from);
540 65
        $rootAlias      = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
541 65
        $rootClass      = $this->queryComponents[$rootAlias]['metadata'];
542 65
        $rootIdentifier = $rootClass->identifier;
543
544
        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
545 65
        $sqlIdentifier = [];
546 65
        foreach ($rootIdentifier as $property) {
547 65
            if (isset($rootClass->fieldMappings[$property])) {
548 64
                foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) {
549 64
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
550 64
                        $sqlIdentifier[$property] = $alias;
551
                    }
552
                }
553
            }
554
555 65
            if (isset($rootClass->associationMappings[$property])) {
556 1
                $joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];
557
558 1
                foreach (array_keys($this->rsm->metaMappings, $joinColumn) as $alias) {
559 1
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
560 65
                        $sqlIdentifier[$property] = $alias;
561
                    }
562
                }
563
            }
564
        }
565
566 65
        if (count($sqlIdentifier) === 0) {
567
            throw new \RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
568
        }
569
570 65
        if (count($rootIdentifier) != count($sqlIdentifier)) {
571
            throw new \RuntimeException(sprintf(
572
                'Not all identifier properties can be found in the ResultSetMapping: %s',
573
                implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
574
            ));
575
        }
576
577 65
        return $sqlIdentifier;
578
    }
579
580
    /**
581
     * {@inheritdoc}
582
     */
583 55
    public function walkPathExpression($pathExpr)
584
    {
585 55
        if (!$this->inSubSelect && !$this->platformSupportsRowNumber() && !in_array($pathExpr, $this->orderByPathExpressions)) {
586 42
            $this->orderByPathExpressions[] = $pathExpr;
587
        }
588
589 55
        return parent::walkPathExpression($pathExpr);
590
    }
591
592
    /**
593
     * {@inheritdoc}
594
     */
595 7
    public function walkSubSelect($subselect)
596
    {
597 7
        $this->inSubSelect = true;
598
599 7
        $sql = parent::walkSubselect($subselect);
600
601 7
        $this->inSubSelect = false;
602
603 7
        return $sql;
604
    }
605
}
606