Completed
Pull Request — master (#6143)
by chihiro
10:09
created

LimitSubqueryOutputWalker::preserveSqlOrdering()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 38
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 38
ccs 16
cts 16
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 4
nop 4
crap 4
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 65
    public function __construct($query, $parserResult, array $queryComponents)
106
    {
107 65
        $this->platform = $query->getEntityManager()->getConnection()->getDatabasePlatform();
108 65
        $this->rsm = $parserResult->getResultSetMapping();
109 65
        $this->queryComponents = $queryComponents;
110
111
        // Reset limit and offset
112 65
        $this->firstResult = $query->getFirstResult();
113 65
        $this->maxResults = $query->getMaxResults();
114 65
        $query->setFirstResult(null)->setMaxResults(null);
115
116 65
        $this->em               = $query->getEntityManager();
117 65
        $this->quoteStrategy    = $this->em->getConfiguration()->getQuoteStrategy();
118
119 65
        parent::__construct($query, $parserResult, $queryComponents);
120 65
    }
121
122
    /**
123
     * Check if the platform supports the ROW_NUMBER window function.
124
     *
125
     * @return bool
126
     */
127 65
    private function platformSupportsRowNumber()
128
    {
129 65
        return $this->platform instanceof PostgreSqlPlatform
130 58
            || $this->platform instanceof SQLServerPlatform
131 58
            || $this->platform instanceof OraclePlatform
132 52
            || $this->platform instanceof SQLAnywherePlatform
133 52
            || $this->platform instanceof DB2Platform
134 52
            || (method_exists($this->platform, 'supportsRowNumberFunction')
135 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...
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 65
    public function walkSelectStatement(SelectStatement $AST)
177
    {
178 65
        if ($this->platformSupportsRowNumber()) {
179 13
            return $this->walkSelectStatementWithRowNumber($AST);
180
        }
181 52
        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 52
    public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
254
    {
255
        // We don't want to call this recursively!
256 52
        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 44
            $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 52
        $orderByClause = $AST->orderByClause;
266 52
        $AST->orderByClause = null;
267
268 52
        $innerSql = $this->getInnerSQL($AST);
269
270 52
        $sqlIdentifier = $this->getSQLIdentifier($AST);
271
272
        // Build the counter query
273 52
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result',
274 52
            implode(', ', $sqlIdentifier), $innerSql);
275
276
        // http://www.doctrine-project.org/jira/browse/DDC-1958
277 52
        $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 52
        $sql = $this->platform->modifyLimitQuery(
281 52
            $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 52
        foreach ($sqlIdentifier as $property => $alias) {
289 52
            $this->rsm->addScalarResult($alias, $property);
290
        }
291
292
        // Restore orderByClause
293 52
        $AST->orderByClause = $orderByClause;
294
295 52
        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 44
    private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
305
    {
306 44
        $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 44
        $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 44
        $walker->walkSelectStatementWithoutRowNumber($AST, false);
317 44
        $orderByPathExpressions = $walker->getOrderByPathExpressions();
318
319
        // Get a map of referenced identifiers to field names.
320 44
        $selects = [];
321 44
        foreach ($orderByPathExpressions as $pathExpression) {
322 40
            $idVar = $pathExpression->identificationVariable;
323 40
            $field = $pathExpression->field;
324 40
            if (!isset($selects[$idVar])) {
325 40
                $selects[$idVar] = [];
326
            }
327 40
            $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 44
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
333 44
            if ($selectExpression instanceof SelectExpression) {
334 44
                $idVar = $selectExpression->expression;
335 44
                if (!is_string($idVar)) {
336 4
                    continue;
337
                }
338 44
                $field = $selectExpression->fieldIdentificationVariable;
339 44
                if ($field === null) {
340
                    // No need to add this select, as we're already fetching the whole object.
341 44
                    unset($selects[$idVar]);
342
                } else {
343 44
                    unset($selects[$idVar][$field]);
344
                }
345
            }
346
        }
347
348
        // Add select items which were not excluded to the AST's select clause.
349 44
        foreach ($selects as $idVar => $fields) {
350 9
            $AST->selectClause->selectExpressions[] = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
351
        }
352 44
    }
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 52
    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 52
        if (! $orderByClause instanceof OrderByClause) {
369 8
            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 44
        $orderBy = $this->rebuildOrderByClauseForOuterScope($orderByClause);
375
376 44
        $innerSqlIdentifier = $sqlIdentifier;
377
378 44
        foreach ($orderBy as $field) {
379 44
            $field = preg_replace('/((\S+)\s+(ASC|DESC)\s*,?)*/', '${2}', $field);
380
381
            // skip fields that are selected by identifiers,
382
            // if those are ordered by in the query
383 44
            if (in_array($field, $sqlIdentifier, true)) {
384 6
                continue;
385
            }
386 39
            $innerSqlIdentifier[] = $field;
387
        }
388
389
        // Build the innner select statement
390 44
        $sql = sprintf(
391 44
            'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s',
392 44
            implode(', ', $innerSqlIdentifier),
393
            $innerSql,
394 44
            implode(', ', $orderBy)
395
        );
396
397
        // now only select distinct identifier
398 44
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result', implode(', ', $sqlIdentifier), $sql);
399
400 44
        return $sql;
401
    }
402
403
    /**
404
     * Generates a new order by clause that works in the scope of a select query wrapping the original
405
     *
406
     * @param OrderByClause $orderByClause
407
     * @return array
408
     */
409 44
    private function rebuildOrderByClauseForOuterScope(OrderByClause $orderByClause)
410
    {
411
        $dqlAliasToSqlTableAliasMap
412
            = $searchPatterns
413
            = $replacements
414
            = $dqlAliasToClassMap
415
            = $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...
416
            = $orderByItems
417 44
            = [];
418
419
        // Generate DQL alias -> SQL table alias mapping
420 44
        foreach(array_keys($this->rsm->aliasMap) as $dqlAlias) {
421 44
            $dqlAliasToClassMap[$dqlAlias] = $class = $this->queryComponents[$dqlAlias]['metadata'];
422 44
            $dqlAliasToSqlTableAliasMap[$dqlAlias] = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
423
        }
424
425
        // Pattern to find table path expressions in the order by clause
426 44
        $fieldSearchPattern = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
427
428
        // Generate search patterns for each field's path expression in the order by clause
429 44
        foreach($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
430 44
            $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
431 44
            $class = $dqlAliasToClassMap[$dqlAliasForFieldAlias];
432
433
            // If the field is from a joined child table, we won't be ordering
434
            // on it.
435 44
            if (!isset($class->fieldMappings[$fieldName])) {
436 1
                continue;
437
            }
438
439 44
            $fieldMapping = $class->fieldMappings[$fieldName];
440
441
            // Get the proper column name as will appear in the select list
442 44
            $columnName = $this->quoteStrategy->getColumnName(
443
                $fieldName,
444 44
                $dqlAliasToClassMap[$dqlAliasForFieldAlias],
445 44
                $this->em->getConnection()->getDatabasePlatform()
446
            );
447
448
            // Get the SQL table alias for the entity and field
449 44
            $sqlTableAliasForFieldAlias = $dqlAliasToSqlTableAliasMap[$dqlAliasForFieldAlias];
450 44
            if (isset($fieldMapping['declared']) && $fieldMapping['declared'] !== $class->name) {
451
                // Field was declared in a parent class, so we need to get the proper SQL table alias
452
                // for the joined parent table.
453 2
                $otherClassMetadata = $this->em->getClassMetadata($fieldMapping['declared']);
454 2
                if (!$otherClassMetadata->isMappedSuperclass) {
455 1
                    $sqlTableAliasForFieldAlias = $this->getSQLTableAlias($otherClassMetadata->getTableName(), $dqlAliasForFieldAlias);
456
                }
457
            }
458
459
            // Compose search/replace patterns
460 44
            $searchPatterns[] = sprintf($fieldSearchPattern, $sqlTableAliasForFieldAlias, $columnName);
461 44
            $replacements[] = $fieldAlias;
462
        }
463
464 44
        foreach($orderByClause->orderByItems as $orderByItem) {
465
            // Walk order by item to get string representation of it
466 44
            $orderByItemString = $this->walkOrderByItem($orderByItem);
467
468
            // Replace path expressions in the order by clause with their column alias
469 44
            $orderByItemString = preg_replace($searchPatterns, $replacements, $orderByItemString);
470
471 44
            $orderByItems[] = $orderByItemString;
472
        }
473
474 44
        return $orderByItems;
475
    }
476
477
    /**
478
     * getter for $orderByPathExpressions
479
     *
480
     * @return array
481
     */
482 44
    public function getOrderByPathExpressions()
483
    {
484 44
        return $this->orderByPathExpressions;
485
    }
486
487
    /**
488
     * @param SelectStatement $AST
489
     *
490
     * @return string
491
     *
492
     * @throws \Doctrine\ORM\OptimisticLockException
493
     * @throws \Doctrine\ORM\Query\QueryException
494
     */
495 65
    private function getInnerSQL(SelectStatement $AST)
496
    {
497
        // Set every select expression as visible(hidden = false) to
498
        // make $AST have scalar mappings properly - this is relevant for referencing selected
499
        // fields from outside the subquery, for example in the ORDER BY segment
500 65
        $hiddens = [];
501
502 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
503 65
            $hiddens[$idx] = $expr->hiddenAliasResultVariable;
504 65
            $expr->hiddenAliasResultVariable = false;
505
        }
506
507 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...
508
509
        // Restore hiddens
510 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
511 65
            $expr->hiddenAliasResultVariable = $hiddens[$idx];
512
        }
513
514 65
        return $innerSql;
515
    }
516
517
    /**
518
     * @param SelectStatement $AST
519
     *
520
     * @return array
521
     */
522 65
    private function getSQLIdentifier(SelectStatement $AST)
523
    {
524
        // Find out the SQL alias of the identifier column of the root entity.
525
        // It may be possible to make this work with multiple root entities but that
526
        // would probably require issuing multiple queries or doing a UNION SELECT.
527
        // So for now, it's not supported.
528
529
        // Get the root entity and alias from the AST fromClause.
530 65
        $from = $AST->fromClause->identificationVariableDeclarations;
531 65
        if (count($from) !== 1) {
532
            throw new \RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
533
        }
534
535 65
        $fromRoot       = reset($from);
536 65
        $rootAlias      = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
537 65
        $rootClass      = $this->queryComponents[$rootAlias]['metadata'];
538 65
        $rootIdentifier = $rootClass->identifier;
539
540
        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
541 65
        $sqlIdentifier = [];
542 65
        foreach ($rootIdentifier as $property) {
543 65
            if (isset($rootClass->fieldMappings[$property])) {
544 64
                foreach (array_keys($this->rsm->fieldMappings, $property) as $alias) {
545 64
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
546 64
                        $sqlIdentifier[$property] = $alias;
547
                    }
548
                }
549
            }
550
551 65
            if (isset($rootClass->associationMappings[$property])) {
552 1
                $joinColumn = $rootClass->associationMappings[$property]['joinColumns'][0]['name'];
553
554 1
                foreach (array_keys($this->rsm->metaMappings, $joinColumn) as $alias) {
555 1
                    if ($this->rsm->columnOwnerMap[$alias] == $rootAlias) {
556 65
                        $sqlIdentifier[$property] = $alias;
557
                    }
558
                }
559
            }
560
        }
561
562 65
        if (count($sqlIdentifier) === 0) {
563
            throw new \RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
564
        }
565
566 65
        if (count($rootIdentifier) != count($sqlIdentifier)) {
567
            throw new \RuntimeException(sprintf(
568
                'Not all identifier properties can be found in the ResultSetMapping: %s',
569
                implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
570
            ));
571
        }
572
573 65
        return $sqlIdentifier;
574
    }
575
576
    /**
577
     * {@inheritdoc}
578
     */
579 55
    public function walkPathExpression($pathExpr)
580
    {
581 55
        if (!$this->inSubSelect && !$this->platformSupportsRowNumber() && !in_array($pathExpr, $this->orderByPathExpressions)) {
582 42
            $this->orderByPathExpressions[] = $pathExpr;
583
        }
584
585 55
        return parent::walkPathExpression($pathExpr);
586
    }
587
588
    /**
589
     * {@inheritdoc}
590
     */
591 7
    public function walkSubSelect($subselect)
592
    {
593 7
        $this->inSubSelect = true;
594
595 7
        $sql = parent::walkSubselect($subselect);
596
597 7
        $this->inSubSelect = false;
598
599 7
        return $sql;
600
    }
601
}
602