Passed
Pull Request — master (#7581)
by
unknown
11:11
created

LimitSubqueryOutputWalker::getSQLIdentifier()   B

Complexity

Conditions 11
Paths 19

Size

Total Lines 61
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 11.4611

Importance

Changes 0
Metric Value
cc 11
eloc 31
nc 19
nop 1
dl 0
loc 61
ccs 27
cts 32
cp 0.8438
crap 11.4611
rs 7.3166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Tools\Pagination;
6
7
use Doctrine\DBAL\Platforms\AbstractPlatform;
8
use Doctrine\DBAL\Platforms\DB2Platform;
9
use Doctrine\DBAL\Platforms\OraclePlatform;
10
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
11
use Doctrine\DBAL\Platforms\SQLAnywherePlatform;
12
use Doctrine\DBAL\Platforms\SQLServerPlatform;
13
use Doctrine\DBAL\Types\Type;
14
use Doctrine\ORM\EntityManagerInterface;
15
use Doctrine\ORM\Mapping\AssociationMetadata;
16
use Doctrine\ORM\Mapping\FieldMetadata;
17
use Doctrine\ORM\OptimisticLockException;
18
use Doctrine\ORM\Query;
19
use Doctrine\ORM\Query\AST\OrderByClause;
20
use Doctrine\ORM\Query\AST\OrderByItem;
21
use Doctrine\ORM\Query\AST\PartialObjectExpression;
22
use Doctrine\ORM\Query\AST\PathExpression;
23
use Doctrine\ORM\Query\AST\SelectExpression;
24
use Doctrine\ORM\Query\AST\SelectStatement;
25
use Doctrine\ORM\Query\ParserResult;
26
use Doctrine\ORM\Query\QueryException;
27
use Doctrine\ORM\Query\ResultSetMapping;
28
use Doctrine\ORM\Query\SqlWalker;
29
use RuntimeException;
30
use function array_diff;
31
use function array_keys;
32
use function array_map;
33
use function count;
34
use function implode;
35
use function in_array;
36
use function is_string;
37
use function method_exists;
38
use function preg_replace;
39
use function reset;
40
use function sprintf;
41
use function strrpos;
42
use function substr;
43
44
/**
45
 * Wraps the query in order to select root entity IDs for pagination.
46
 *
47
 * Given a DQL like `SELECT u FROM User u` it will generate an SQL query like:
48
 * SELECT DISTINCT <id> FROM (<original SQL>) LIMIT x OFFSET y
49
 *
50
 * Works with composite keys but cannot deal with queries that have multiple
51
 * root entities (e.g. `SELECT f, b from Foo, Bar`)
52
 */
53
class LimitSubqueryOutputWalker extends SqlWalker
54
{
55
    private const ORDER_BY_PATH_EXPRESSION = '/(?<![a-z0-9_])%s\.%s(?![a-z0-9_])/i';
56
57
    /** @var AbstractPlatform */
58
    private $platform;
59
60
    /** @var ResultSetMapping */
61
    private $rsm;
62
63
    /** @var mixed[][] */
64
    private $queryComponents;
65
66
    /** @var int */
67
    private $firstResult;
68
69
    /** @var int */
70
    private $maxResults;
71
72
    /** @var EntityManagerInterface */
73
    private $em;
74
75
    /** @var PathExpression[] */
76
    private $orderByPathExpressions = [];
77
78
    /**
79
     * @var bool We don't want to add path expressions from sub-selects into the select clause of the containing query.
80
     *           This state flag simply keeps track on whether we are walking on a subquery or not
81
     */
82
    private $inSubSelect = false;
83
84
    /**
85
     * Stores various parameters that are otherwise unavailable
86
     * because Doctrine\ORM\Query\SqlWalker keeps everything private without
87
     * accessors.
88
     *
89
     * @param Query        $query
90
     * @param ParserResult $parserResult
91
     * @param mixed[][]    $queryComponents
92
     */
93 65
    public function __construct($query, $parserResult, array $queryComponents)
94
    {
95 65
        $this->platform        = $query->getEntityManager()->getConnection()->getDatabasePlatform();
96 65
        $this->rsm             = $parserResult->getResultSetMapping();
97 65
        $this->queryComponents = $queryComponents;
98
99
        // Reset limit and offset
100 65
        $this->firstResult = $query->getFirstResult();
101 65
        $this->maxResults  = $query->getMaxResults();
102
103
        $query
104 65
            ->setFirstResult(null)
105 65
            ->setMaxResults(null);
106
107 65
        $this->em = $query->getEntityManager();
108
109 65
        parent::__construct($query, $parserResult, $queryComponents);
110 65
    }
111
112
    /**
113
     * Check if the platform supports the ROW_NUMBER window function.
114
     *
115
     * @return bool
116
     */
117 65
    private function platformSupportsRowNumber()
118
    {
119 65
        return $this->platform instanceof PostgreSqlPlatform
120 58
            || $this->platform instanceof SQLServerPlatform
121 58
            || $this->platform instanceof OraclePlatform
122 52
            || $this->platform instanceof SQLAnywherePlatform
123 52
            || $this->platform instanceof DB2Platform
124 52
            || (method_exists($this->platform, 'supportsRowNumberFunction')
125 65
                && $this->platform->supportsRowNumberFunction());
126
    }
127
128
    /**
129
     * Rebuilds a select statement's order by clause for use in a
130
     * ROW_NUMBER() OVER() expression.
131
     */
132 11
    private function rebuildOrderByForRowNumber(SelectStatement $AST)
133
    {
134 11
        $orderByClause              = $AST->orderByClause;
135 11
        $selectAliasToExpressionMap = [];
136
        // Get any aliases that are available for select expressions.
137 11
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
138 11
            $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
139
        }
140
141
        // Rebuild string orderby expressions to use the select expression they're referencing
142 11
        foreach ($orderByClause->orderByItems as $orderByItem) {
143 11
            if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) {
144 7
                $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression];
145
            }
146
        }
147
148 11
        $func = new RowNumberOverFunction('dctrn_rownum');
149
150 11
        $func->orderByClause                    = $AST->orderByClause;
151 11
        $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true);
152
153
        // No need for an order by clause, we'll order by rownum in the outer query.
154 11
        $AST->orderByClause = null;
155 11
    }
156
157
    /**
158
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
159
     *
160
     * @return string
161
     *
162
     * @throws RuntimeException
163
     */
164 65
    public function walkSelectStatement(SelectStatement $AST)
165
    {
166 65
        if ($this->platformSupportsRowNumber()) {
167 13
            return $this->walkSelectStatementWithRowNumber($AST);
168
        }
169
170 52
        return $this->walkSelectStatementWithoutRowNumber($AST);
171
    }
172
173
    /**
174
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
175
     * This method is for use with platforms which support ROW_NUMBER.
176
     *
177
     * @return string
178
     *
179
     * @throws RuntimeException
180
     */
181 13
    public function walkSelectStatementWithRowNumber(SelectStatement $AST)
182
    {
183 13
        $hasOrderBy   = false;
184 13
        $outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
185 13
        $orderGroupBy = '';
186
187 13
        if ($AST->orderByClause instanceof OrderByClause) {
188 11
            $hasOrderBy = true;
189
190 11
            $this->rebuildOrderByForRowNumber($AST);
191
        }
192
193 13
        $innerSql           = $this->getInnerSQL($AST);
194 13
        $sqlIdentifier      = $this->getSQLIdentifier($AST);
195
        $sqlAliasIdentifier = array_map(static function ($info) {
196 13
            return $info['alias'];
197 13
        }, $sqlIdentifier);
198
199 13
        if ($hasOrderBy) {
200 11
            $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlAliasIdentifier);
201 11
            $sqlPiece     = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum';
202
203 11
            $sqlAliasIdentifier[] = $sqlPiece;
204 11
            $sqlIdentifier[]      = [
205 11
                'alias' => $sqlPiece,
206 11
                'type'  => Type::getType('integer'),
207
            ];
208
        }
209
210
        // Build the counter query
211 13
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result', implode(', ', $sqlAliasIdentifier), $innerSql);
212
213 13
        if ($hasOrderBy) {
214 11
            $sql .= $orderGroupBy . $outerOrderBy;
215
        }
216
217
        // Apply the limit and offset.
218 13
        $sql = $this->platform->modifyLimitQuery($sql, $this->maxResults, $this->firstResult ?? 0);
219
220
        // Add the columns to the ResultSetMapping. It's not really nice but
221
        // it works. Preferably I'd clear the RSM or simply create a new one
222
        // but that is not possible from inside the output walker, so we dirty
223
        // up the one we have.
224 13
        foreach ($sqlIdentifier as $property => $propertyMapping) {
225 13
            $this->rsm->addScalarResult($propertyMapping['alias'], $property, $propertyMapping['type']);
226
        }
227
228 13
        return $sql;
229
    }
230
231
    /**
232
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
233
     * This method is for platforms which DO NOT support ROW_NUMBER.
234
     *
235
     * @param bool $addMissingItemsFromOrderByToSelect
236
     *
237
     * @return string
238
     *
239
     * @throws RuntimeException
240
     */
241 52
    public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
242
    {
243
        // We don't want to call this recursively!
244 52
        if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
245
            // In the case of ordering a query by columns from joined tables, we
246
            // must add those columns to the select clause of the query BEFORE
247
            // the SQL is generated.
248 44
            $this->addMissingItemsFromOrderByToSelect($AST);
249
        }
250
251
        // Remove order by clause from the inner query
252
        // It will be re-appended in the outer select generated by this method
253 52
        $wrapOrderByClause = $orderByClause = $AST->orderByClause;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
254 52
        $AST->orderByClause = null;
255
256 52
        $innerSql           = $this->getInnerSQL($AST);
257 52
        $sqlIdentifier      = $this->getSQLIdentifier($AST);
258
        $sqlAliasIdentifier = array_map(static function ($info) {
259 52
            return $info['alias'];
260 52
        }, $sqlIdentifier);
261
262
        // Build the counter query
263 52
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result', implode(', ', $sqlAliasIdentifier), $innerSql);
264
265
        // For statement without no rowNumber, we need always order by to select by range from a sub-query. If not specified, add default one.
266 52
        if (empty($wrapOrderByClause)) {
267 8
            $from      = $AST->fromClause->identificationVariableDeclarations;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 17 spaces but found 6 spaces

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
268 8
            $fromRoot  = reset($from);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 13 spaces but found 2 spaces

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
269 8
            $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 12 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
270 8
            $rootClass = $this->queryComponents[$rootAlias]['metadata'];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 12 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
271 8
            $identifier = $rootClass->getSingleIdentifierFieldName();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 11 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
272 8
            $pathExpression = new PathExpression(
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
273 8
                PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
274 8
                $rootAlias,
275 8
                $identifier
276
            );
277 8
            $pathExpression->type = PathExpression::TYPE_STATE_FIELD;
278
279 8
            $orderByItem = new OrderByItem($pathExpression);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
280 8
            $orderByItem->type = 'ASC';
281 8
            $wrapOrderByClause = new OrderByClause([$orderByItem]);
282
        }
283
284
        // http://www.doctrine-project.org/jira/browse/DDC-1958
285 52
        $sql = $this->preserveSqlOrdering($sqlAliasIdentifier, $innerSql, $sql, $wrapOrderByClause);
286
287
        // Apply the limit and offset.
288 51
        $sql = $this->platform->modifyLimitQuery(
289 51
            $sql,
290 51
            $this->maxResults,
291 51
            $this->firstResult ?? 0
292
        );
293
294
        // Add the columns to the ResultSetMapping. It's not really nice but
295
        // it works. Preferably I'd clear the RSM or simply create a new one
296
        // but that is not possible from inside the output walker, so we dirty
297
        // up the one we have.
298 51
        foreach ($sqlIdentifier as $property => $propertyMapping) {
299 51
            $this->rsm->addScalarResult($propertyMapping['alias'], $property, $propertyMapping['type']);
300
        }
301
302
        // Restore orderByClause
303 51
        $AST->orderByClause = $orderByClause;
304
305 51
        return $sql;
306
    }
307
308
    /**
309
     * Finds all PathExpressions in an AST's OrderByClause, and ensures that
310
     * the referenced fields are present in the SelectClause of the passed AST.
311
     */
312 44
    private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
313
    {
314 44
        $this->orderByPathExpressions = [];
315
316
        // We need to do this in another walker because otherwise we'll end up
317
        // polluting the state of this one.
318 44
        $walker = clone $this;
319
320
        // This will populate $orderByPathExpressions via
321
        // LimitSubqueryOutputWalker::walkPathExpression, which will be called
322
        // as the select statement is walked. We'll end up with an array of all
323
        // path expressions referenced in the query.
324 44
        $walker->walkSelectStatementWithoutRowNumber($AST, false);
325 44
        $orderByPathExpressions = $walker->getOrderByPathExpressions();
326
327
        // Get a map of referenced identifiers to field names.
328 44
        $selects = [];
329
330 44
        foreach ($orderByPathExpressions as $pathExpression) {
331 40
            $idVar = $pathExpression->identificationVariable;
332 40
            $field = $pathExpression->field;
333
334 40
            if (! isset($selects[$idVar])) {
335 40
                $selects[$idVar] = [];
336
            }
337
338 40
            $selects[$idVar][$field] = true;
339
        }
340
341
        // Loop the select clause of the AST and exclude items from $select
342
        // that are already being selected in the query.
343 44
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
344 44
            if ($selectExpression instanceof SelectExpression) {
345 44
                $idVar = $selectExpression->expression;
346
347 44
                if (! is_string($idVar)) {
348 4
                    continue;
349
                }
350
351 44
                $field = $selectExpression->fieldIdentificationVariable;
352
353 44
                if ($field === null) {
354
                    // No need to add this select, as we're already fetching the whole object.
355 44
                    unset($selects[$idVar]);
356
                } else {
357
                    unset($selects[$idVar][$field]);
358
                }
359
            }
360
        }
361
362
        // Add select items which were not excluded to the AST's select clause.
363 44
        foreach ($selects as $idVar => $fields) {
364 9
            $selectExpression = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
365
366 9
            $AST->selectClause->selectExpressions[] = $selectExpression;
367
        }
368 44
    }
369
370
    /**
371
     * Generates new SQL for statements with an order by clause
372
     *
373
     * @param mixed[][] $sqlIdentifier
374
     */
375 52
    private function preserveSqlOrdering(
376
        array $sqlIdentifier,
377
        string $innerSql,
378
        string $sql,
379
        ?OrderByClause $orderByClause
380
    ) : string {
381
        // If the sql statement has an order by clause, we need to wrap it in a new select distinct statement
382 52
        if (! $orderByClause) {
383
            return $sql;
384
        }
385
386
        // now only select distinct identifier
387 52
        return sprintf(
388 52
            'SELECT DISTINCT %s FROM (%s) dctrn_result',
389 52
            implode(', ', $sqlIdentifier),
390 52
            $this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql)
391
        );
392
    }
393
394
    /**
395
     * Generates a new SQL statement for the inner query to keep the correct sorting
396
     *
397
     * @param mixed[][] $identifiers
398
     */
399 52
    private function recreateInnerSql(
400
        OrderByClause $orderByClause,
401
        array $identifiers,
402
        string $innerSql
403
    ) : string {
404 52
        [$searchPatterns, $replacements] = $this->generateSqlAliasReplacements();
405
406 52
        $orderByItems = [];
407
408 52
        foreach ($orderByClause->orderByItems as $orderByItem) {
409
            // Walk order by item to get string representation of it and
410
            // replace path expressions in the order by clause with their column alias
411 52
            $orderByItemString = preg_replace(
412 52
                $searchPatterns,
413 52
                $replacements,
414 52
                $this->walkOrderByItem($orderByItem)
415
            );
416
417 51
            $orderByItems[] = $orderByItemString;
418 51
            $identifier     = substr($orderByItemString, 0, strrpos($orderByItemString, ' '));
419
420 51
            if (! in_array($identifier, $identifiers, true)) {
421 39
                $identifiers[] = $identifier;
422
            }
423
        }
424
425 51
        return $sql = sprintf(
0 ignored issues
show
Unused Code introduced by
The assignment to $sql is dead and can be removed.
Loading history...
426 51
            'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s',
427 51
            implode(', ', $identifiers),
428 51
            $innerSql,
429 51
            implode(', ', $orderByItems)
430
        );
431
    }
432
433
    /**
434
     * @return string[][]
435
     */
436 52
    private function generateSqlAliasReplacements() : array
437
    {
438 52
        $platform       = $this->em->getConnection()->getDatabasePlatform();
439 52
        $searchPatterns = $replacements = [];
440
441
        // Generate search patterns for each field's path expression in the order by clause
442 52
        foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
443 52
            $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
444 52
            $class                 = $this->queryComponents[$dqlAliasForFieldAlias]['metadata'];
445 52
            $property              = $class->getProperty($fieldName);
446
447
            // If the field is from a joined child table, we won't be ordering on it.
448 52
            if ($property === null) {
449 1
                continue;
450
            }
451
452
            // Get the SQL table alias for the entity and field and the column name as will appear in the select list
453 52
            $tableAlias = $this->getSQLTableAlias($property->getTableName(), $dqlAliasForFieldAlias);
454 52
            $columnName = $platform->quoteIdentifier($property->getColumnName());
455
456
            // Compose search and replace patterns
457 52
            $searchPatterns[] = sprintf(self::ORDER_BY_PATH_EXPRESSION, $tableAlias, $columnName);
458 52
            $replacements[]   = $fieldAlias;
459
        }
460
461 52
        return [$searchPatterns, $replacements];
462
    }
463
464
    /**
465
     * getter for $orderByPathExpressions
466
     *
467
     * @return PathExpression[]
468
     */
469 44
    public function getOrderByPathExpressions()
470
    {
471 44
        return $this->orderByPathExpressions;
472
    }
473
474
    /**
475
     * @return string
476
     *
477
     * @throws OptimisticLockException
478
     * @throws QueryException
479
     */
480 65
    private function getInnerSQL(SelectStatement $AST)
481
    {
482
        // Set every select expression as visible(hidden = false) to
483
        // make $AST have scalar mappings properly - this is relevant for referencing selected
484
        // fields from outside the subquery, for example in the ORDER BY segment
485 65
        $hiddens = [];
486
487 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
488 65
            $hiddens[$idx]                   = $expr->hiddenAliasResultVariable;
489 65
            $expr->hiddenAliasResultVariable = false;
490
        }
491
492 65
        $innerSql = parent::walkSelectStatement($AST);
493
494
        // Restore hiddens
495 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
496 65
            $expr->hiddenAliasResultVariable = $hiddens[$idx];
497
        }
498
499 65
        return $innerSql;
500
    }
501
502
    /**
503
     * @return mixed[][]
504
     */
505 65
    private function getSQLIdentifier(SelectStatement $AST)
506
    {
507
        // Find out the SQL alias of the identifier column of the root entity.
508
        // It may be possible to make this work with multiple root entities but that
509
        // would probably require issuing multiple queries or doing a UNION SELECT.
510
        // So for now, it's not supported.
511
512
        // Get the root entity and alias from the AST fromClause.
513 65
        $from = $AST->fromClause->identificationVariableDeclarations;
514
515 65
        if (count($from) !== 1) {
516
            throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
517
        }
518
519 65
        $fromRoot       = reset($from);
520 65
        $rootAlias      = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
521 65
        $rootClass      = $this->queryComponents[$rootAlias]['metadata'];
522 65
        $rootIdentifier = $rootClass->identifier;
523
524
        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
525 65
        $sqlIdentifier = [];
526
527 65
        foreach ($rootIdentifier as $identifier) {
528 65
            $property = $rootClass->getProperty($identifier);
529
530 65
            if ($property instanceof FieldMetadata) {
531 64
                foreach (array_keys($this->rsm->fieldMappings, $identifier, true) as $alias) {
532 64
                    if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
533 64
                        $sqlIdentifier[$identifier] = [
534 64
                            'type'  => $property->getType(),
535 64
                            'alias' => $alias,
536
                        ];
537
                    }
538
                }
539 1
            } elseif ($property instanceof AssociationMetadata) {
540 1
                $joinColumns = $property->getJoinColumns();
0 ignored issues
show
Bug introduced by
The method getJoinColumns() does not exist on Doctrine\ORM\Mapping\AssociationMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\AssociationMetadata such as Doctrine\ORM\Mapping\ToOneAssociationMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

540
                /** @scrutinizer ignore-call */ 
541
                $joinColumns = $property->getJoinColumns();
Loading history...
541 1
                $joinColumn  = reset($joinColumns);
542
543 1
                foreach (array_keys($this->rsm->metaMappings, $joinColumn->getColumnName(), true) as $alias) {
544 1
                    if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
545 1
                        $sqlIdentifier[$identifier] = [
546 1
                            'type'  => $this->rsm->typeMappings[$alias],
547 1
                            'alias' => $alias,
548
                        ];
549
                    }
550
                }
551
            }
552
        }
553
554 65
        if (count($sqlIdentifier) === 0) {
555
            throw new RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
556
        }
557
558 65
        if (count($rootIdentifier) !== count($sqlIdentifier)) {
559
            throw new RuntimeException(sprintf(
560
                'Not all identifier properties can be found in the ResultSetMapping: %s',
561
                implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
562
            ));
563
        }
564
565 65
        return $sqlIdentifier;
566
    }
567
568
    /**
569
     * {@inheritdoc}
570
     */
571 61
    public function walkPathExpression($pathExpr)
572
    {
573 61
        if (! $this->inSubSelect && ! $this->platformSupportsRowNumber() && ! in_array($pathExpr, $this->orderByPathExpressions, true)) {
574 48
            $this->orderByPathExpressions[] = $pathExpr;
575
        }
576
577 61
        return parent::walkPathExpression($pathExpr);
578
    }
579
580
    /**
581
     * {@inheritdoc}
582
     */
583 7
    public function walkSubSelect($subselect)
584
    {
585 7
        $this->inSubSelect = true;
586
587 7
        $sql = parent::walkSubselect($subselect);
588
589 7
        $this->inSubSelect = false;
590
591 7
        return $sql;
592
    }
593
}
594