Failed Conditions
Pull Request — 2.8.x (#7946)
by
unknown
09:22 queued 12s
created

LimitSubqueryOutputWalker   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 560
Duplicated Lines 0 %

Test Coverage

Coverage 96.39%

Importance

Changes 0
Metric Value
wmc 64
eloc 188
c 0
b 0
f 0
dl 0
loc 560
ccs 187
cts 194
cp 0.9639
rs 3.28

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getOrderByPathExpressions() 0 3 1
B getSQLIdentifier() 0 52 11
A walkSelectStatement() 0 6 2
A rebuildOrderByForRowNumber() 0 21 5
A getInnerSQL() 0 20 3
A preserveSqlOrdering() 0 16 2
A recreateInnerSql() 0 31 3
A walkSelectStatementWithRowNumber() 0 46 5
A __construct() 0 15 1
A walkSubSelect() 0 9 1
A walkPathExpression() 0 7 4
A walkSelectStatementWithoutRowNumber() 0 43 4
B platformSupportsRowNumber() 0 9 7
B addMissingItemsFromOrderByToSelect() 0 47 8
B generateSqlAliasReplacements() 0 48 7

How to fix   Complexity   

Complex Class

Complex classes like LimitSubqueryOutputWalker often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LimitSubqueryOutputWalker, and based on these observations, apply Extract Interface, too.

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;
52
53
    /**
54
     * @var \Doctrine\ORM\Query\ResultSetMapping
55
     */
56
    private $rsm;
57
58
    /**
59
     * @var array
60
     */
61
    private $queryComponents;
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;
77
78
    /**
79
     * The quote strategy.
80
     *
81
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
82
     */
83
    private $quoteStrategy;
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 49
    public function __construct($query, $parserResult, array $queryComponents)
108
    {
109 49
        $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
110 49
        $this->rsm = $parserResult->getResultSetMapping();
111 49
        $this->queryComponents = $queryComponents;
112
113
        // Reset limit and offset
114 49
        $this->firstResult = $query->getFirstResult();
115 49
        $this->maxResults = $query->getMaxResults();
116 49
        $query->setFirstResult(null)->setMaxResults(null);
117
118 49
        $this->em               = $query->getEntityManager();
119 49
        $this->quoteStrategy    = $this->em->getConfiguration()->getQuoteStrategy();
120
121 49
        parent::__construct($query, $parserResult, $queryComponents);
122 49
    }
123
124
    /**
125
     * Check if the platform supports the ROW_NUMBER window function.
126
     *
127
     * @return bool
128
     */
129 49
    private function platformSupportsRowNumber()
130
    {
131 49
        return $this->platform instanceof PostgreSqlPlatform
132 42
            || $this->platform instanceof SQLServerPlatform
133 42
            || $this->platform instanceof OraclePlatform
134 36
            || $this->platform instanceof SQLAnywherePlatform
135 36
            || $this->platform instanceof DB2Platform
136 36
            || (method_exists($this->platform, 'supportsRowNumberFunction')
137 49
                && $this->platform->supportsRowNumberFunction());
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 7
                $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 49
    public function walkSelectStatement(SelectStatement $AST)
179
    {
180 49
        if ($this->platformSupportsRowNumber()) {
181 13
            return $this->walkSelectStatementWithRowNumber($AST);
182
        }
183 36
        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 36
    public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
256
    {
257
        // We don't want to call this recursively!
258 36
        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 30
            $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 36
        $orderByClause = $AST->orderByClause;
268 36
        $AST->orderByClause = null;
269
270 36
        $innerSql = $this->getInnerSQL($AST);
271
272 36
        $sqlIdentifier = $this->getSQLIdentifier($AST);
273
274
        // Build the counter query
275 36
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result',
276 36
            implode(', ', $sqlIdentifier), $innerSql);
277
278
        // http://www.doctrine-project.org/jira/browse/DDC-1958
279 36
        $sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
280
281
        // Apply the limit and offset.
282 36
        $sql = $this->platform->modifyLimitQuery(
283 36
            $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 36
        foreach ($sqlIdentifier as $property => $alias) {
291 36
            $this->rsm->addScalarResult($alias, $property);
292
        }
293
294
        // Restore orderByClause
295 36
        $AST->orderByClause = $orderByClause;
296
297 36
        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 30
    private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
307
    {
308 30
        $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 30
        $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 30
        $walker->walkSelectStatementWithoutRowNumber($AST, false);
319 30
        $orderByPathExpressions = $walker->getOrderByPathExpressions();
320
321
        // Get a map of referenced identifiers to field names.
322 30
        $selects = [];
323 30
        foreach ($orderByPathExpressions as $pathExpression) {
324 27
            $idVar = $pathExpression->identificationVariable;
325 27
            $field = $pathExpression->field;
326 27
            if (!isset($selects[$idVar])) {
327 27
                $selects[$idVar] = [];
328
            }
329 27
            $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 30
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
335 30
            if ($selectExpression instanceof SelectExpression) {
336 30
                $idVar = $selectExpression->expression;
337 30
                if (!is_string($idVar)) {
338 3
                    continue;
339
                }
340 30
                $field = $selectExpression->fieldIdentificationVariable;
341 30
                if ($field === null) {
342
                    // No need to add this select, as we're already fetching the whole object.
343 30
                    unset($selects[$idVar]);
344
                } else {
345
                    unset($selects[$idVar][$field]);
346
                }
347
            }
348
        }
349
350
        // Add select items which were not excluded to the AST's select clause.
351 30
        foreach ($selects as $idVar => $fields) {
352 7
            $AST->selectClause->selectExpressions[] = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
353
        }
354 30
    }
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 36
    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 36
        if (! $orderByClause) {
374 6
            return $sql;
375
        }
376
377
        // now only select distinct identifier
378 30
        return \sprintf(
379 30
            'SELECT DISTINCT %s FROM (%s) dctrn_result',
380 30
            \implode(', ', $sqlIdentifier),
381 30
            $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 30
    private function recreateInnerSql(
395
        OrderByClause $orderByClause,
396
        array $identifiers,
397
        string $innerSql
398
    ) : string {
399 30
        [$searchPatterns, $replacements] = $this->generateSqlAliasReplacements();
400
401 30
        $orderByItems = [];
402
403 30
        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 30
            $orderByItemString = \preg_replace(
407 30
                $searchPatterns,
408 30
                $replacements,
409 30
                $this->walkOrderByItem($orderByItem)
410
            );
411
412 30
            $orderByItems[] = $orderByItemString;
413 30
            $identifier     = \substr($orderByItemString, 0, \strrpos($orderByItemString, ' '));
414
415 30
            if (! \in_array($identifier, $identifiers, true)) {
416 27
                $identifiers[] = $identifier;
417
            }
418
        }
419
420 30
        return $sql = \sprintf(
0 ignored issues
show
Unused Code introduced by
The assignment to $sql is dead and can be removed.
Loading history...
421 30
            'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s',
422 30
            \implode(', ', $identifiers),
423 30
            $innerSql,
424 30
            \implode(', ', $orderByItems)
425
        );
426
    }
427
428
    /**
429
     * @return string[][]
430
     */
431 30
    private function generateSqlAliasReplacements() : array
432
    {
433 30
        $aliasMap = $searchPatterns = $replacements = $metadataList = [];
434
435
        // Generate DQL alias -> SQL table alias mapping
436 30
        foreach (\array_keys($this->rsm->aliasMap) as $dqlAlias) {
437 30
            $metadataList[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata'];
438 30
            $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 30
        foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
443 30
            $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
444 30
            $class = $metadataList[$dqlAliasForFieldAlias];
445
446
            // If the field is from a joined child table, we won't be ordering on it.
447 30
            if (! isset($class->fieldMappings[$fieldName])) {
448 1
                continue;
449
            }
450
451 30
            $fieldMapping = $class->fieldMappings[$fieldName];
452
453
            // Get the proper column name as will appear in the select list
454 30
            $columnName = $this->quoteStrategy->getColumnName(
455 30
                $fieldName,
456 30
                $metadataList[$dqlAliasForFieldAlias],
457 30
                $this->em->getConnection()->getDatabasePlatform()
458
            );
459
460
            // Get the SQL table alias for the entity and field
461 30
            $sqlTableAliasForFieldAlias = $aliasMap[$dqlAliasForFieldAlias];
462
463 30
            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 1
                $otherClassMetadata = $this->em->getClassMetadata($fieldMapping['declared']);
467
468 1
                if (! $otherClassMetadata->isMappedSuperclass) {
469
                    $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias);
470
                }
471
            }
472
473
            // Compose search and replace patterns
474 30
            $searchPatterns[] = \sprintf(self::ORDER_BY_PATH_EXPRESSION, $sqlTableAliasForFieldAlias, $columnName);
475 30
            $replacements[]   = $fieldAlias;
476
        }
477
478 30
        return [$searchPatterns, $replacements];
479
    }
480
481
    /**
482
     * getter for $orderByPathExpressions
483
     *
484
     * @return array
485
     */
486 30
    public function getOrderByPathExpressions()
487
    {
488 30
        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 49
    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 49
        $hiddens = [];
505
506 49
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
507 49
            $hiddens[$idx] = $expr->hiddenAliasResultVariable;
508 49
            $expr->hiddenAliasResultVariable = false;
509
        }
510
511 49
        $innerSql = parent::walkSelectStatement($AST);
512
513
        // Restore hiddens
514 49
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
515 49
            $expr->hiddenAliasResultVariable = $hiddens[$idx];
516
        }
517
518 49
        return $innerSql;
519
    }
520
521
    /**
522
     * @param SelectStatement $AST
523
     *
524
     * @return array
525
     */
526 49
    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 49
        $from = $AST->fromClause->identificationVariableDeclarations;
535 49
        if (count($from) !== 1) {
536
            throw new \RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
537
        }
538
539 49
        $fromRoot       = reset($from);
540 49
        $rootAlias      = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
541 49
        $rootClass      = $this->queryComponents[$rootAlias]['metadata'];
542 49
        $rootIdentifier = $rootClass->identifier;
543
544
        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
545 49
        $sqlIdentifier = [];
546 49
        foreach ($rootIdentifier as $property) {
547 49
            if (isset($rootClass->fieldMappings[$property])) {
548 48
                foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) {
549 48
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
550 48
                        $sqlIdentifier[$property] = $alias;
551
                    }
552
                }
553
            }
554
555 49
            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 1
                        $sqlIdentifier[$property] = $alias;
561
                    }
562
                }
563
            }
564
        }
565
566 49
        if (count($sqlIdentifier) === 0) {
567
            throw new \RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
568
        }
569
570 49
        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 49
        return $sqlIdentifier;
578
    }
579
580
    /**
581
     * {@inheritdoc}
582
     */
583 40
    public function walkPathExpression($pathExpr)
584
    {
585 40
        if (!$this->inSubSelect && !$this->platformSupportsRowNumber() && !in_array($pathExpr, $this->orderByPathExpressions)) {
586 28
            $this->orderByPathExpressions[] = $pathExpr;
587
        }
588
589 40
        return parent::walkPathExpression($pathExpr);
590
    }
591
592
    /**
593
     * {@inheritdoc}
594
     */
595 5
    public function walkSubSelect($subselect)
596
    {
597 5
        $this->inSubSelect = true;
598
599 5
        $sql = parent::walkSubselect($subselect);
600
601 5
        $this->inSubSelect = false;
602
603 5
        return $sql;
604
    }
605
}
606