LimitSubqueryOutputWalker   F
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 521
Duplicated Lines 0 %

Test Coverage

Coverage 96.94%

Importance

Changes 0
Metric Value
eloc 191
c 0
b 0
f 0
dl 0
loc 521
ccs 190
cts 196
cp 0.9694
rs 3.52
wmc 61

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 1
B getSQLIdentifier() 0 61 11
A walkSelectStatement() 0 7 2
A rebuildOrderByForRowNumber() 0 23 5
A getInnerSQL() 0 20 3
A getOrderByPathExpressions() 0 3 1
A preserveSqlOrdering() 0 16 2
A recreateInnerSql() 0 31 3
A walkSelectStatementWithRowNumber() 0 48 5
A walkSubSelect() 0 9 1
B addMissingItemsFromOrderByToSelect() 0 55 8
A walkPathExpression() 0 7 4
A generateSqlAliasReplacements() 0 26 3
A walkSelectStatementWithoutRowNumber() 0 46 4
B platformSupportsRowNumber() 0 10 8

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
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\PostgreSQL100Platform;
11
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
12
use Doctrine\DBAL\Platforms\SQLAnywhere16Platform;
13
use Doctrine\DBAL\Platforms\SQLServer2012Platform;
14
use Doctrine\DBAL\Types\Type;
15
use Doctrine\ORM\EntityManagerInterface;
16
use Doctrine\ORM\Mapping\AssociationMetadata;
17
use Doctrine\ORM\Mapping\FieldMetadata;
18
use Doctrine\ORM\OptimisticLockException;
19
use Doctrine\ORM\Query;
20
use Doctrine\ORM\Query\AST\OrderByClause;
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 PostgreSQL94Platform
120 58
            || $this->platform instanceof PostgreSQL100Platform
121 58
            || $this->platform instanceof SQLServer2012Platform
122 58
            || $this->platform instanceof OraclePlatform
123 52
            || $this->platform instanceof SQLAnywhere16Platform
124 52
            || $this->platform instanceof DB2Platform
125 52
            || (method_exists($this->platform, 'supportsRowNumberFunction')
126 65
                && $this->platform->supportsRowNumberFunction());
127
    }
128
129
    /**
130
     * Rebuilds a select statement's order by clause for use in a
131
     * ROW_NUMBER() OVER() expression.
132
     */
133 11
    private function rebuildOrderByForRowNumber(SelectStatement $AST)
134
    {
135 11
        $orderByClause              = $AST->orderByClause;
136 11
        $selectAliasToExpressionMap = [];
137
        // Get any aliases that are available for select expressions.
138 11
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
139 11
            $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression;
140
        }
141
142
        // Rebuild string orderby expressions to use the select expression they're referencing
143 11
        foreach ($orderByClause->orderByItems as $orderByItem) {
144 11
            if (is_string($orderByItem->expression) && isset($selectAliasToExpressionMap[$orderByItem->expression])) {
145 7
                $orderByItem->expression = $selectAliasToExpressionMap[$orderByItem->expression];
146
            }
147
        }
148
149 11
        $func = new RowNumberOverFunction('dctrn_rownum');
150
151 11
        $func->orderByClause                    = $AST->orderByClause;
152 11
        $AST->selectClause->selectExpressions[] = new SelectExpression($func, 'dctrn_rownum', true);
153
154
        // No need for an order by clause, we'll order by rownum in the outer query.
155 11
        $AST->orderByClause = null;
156 11
    }
157
158
    /**
159
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
160
     *
161
     * @return string
162
     *
163
     * @throws RuntimeException
164
     */
165 65
    public function walkSelectStatement(SelectStatement $AST)
166
    {
167 65
        if ($this->platformSupportsRowNumber()) {
168 13
            return $this->walkSelectStatementWithRowNumber($AST);
169
        }
170
171 52
        return $this->walkSelectStatementWithoutRowNumber($AST);
172
    }
173
174
    /**
175
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
176
     * This method is for use with platforms which support ROW_NUMBER.
177
     *
178
     * @return string
179
     *
180
     * @throws RuntimeException
181
     */
182 13
    public function walkSelectStatementWithRowNumber(SelectStatement $AST)
183
    {
184 13
        $hasOrderBy   = false;
185 13
        $outerOrderBy = ' ORDER BY dctrn_minrownum ASC';
186 13
        $orderGroupBy = '';
187
188 13
        if ($AST->orderByClause instanceof OrderByClause) {
189 11
            $hasOrderBy = true;
190
191 11
            $this->rebuildOrderByForRowNumber($AST);
192
        }
193
194 13
        $innerSql           = $this->getInnerSQL($AST);
195 13
        $sqlIdentifier      = $this->getSQLIdentifier($AST);
196
        $sqlAliasIdentifier = array_map(static function ($info) {
197 13
            return $info['alias'];
198 13
        }, $sqlIdentifier);
199
200 13
        if ($hasOrderBy) {
201 11
            $orderGroupBy = ' GROUP BY ' . implode(', ', $sqlAliasIdentifier);
202 11
            $sqlPiece     = 'MIN(' . $this->walkResultVariable('dctrn_rownum') . ') AS dctrn_minrownum';
203
204 11
            $sqlAliasIdentifier[] = $sqlPiece;
205 11
            $sqlIdentifier[]      = [
206 11
                'alias' => $sqlPiece,
207 11
                'type'  => Type::getType('integer'),
208
            ];
209
        }
210
211
        // Build the counter query
212 13
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result', implode(', ', $sqlAliasIdentifier), $innerSql);
213
214 13
        if ($hasOrderBy) {
215 11
            $sql .= $orderGroupBy . $outerOrderBy;
216
        }
217
218
        // Apply the limit and offset.
219 13
        $sql = $this->platform->modifyLimitQuery($sql, $this->maxResults, $this->firstResult ?? 0);
220
221
        // Add the columns to the ResultSetMapping. It's not really nice but
222
        // it works. Preferably I'd clear the RSM or simply create a new one
223
        // but that is not possible from inside the output walker, so we dirty
224
        // up the one we have.
225 13
        foreach ($sqlIdentifier as $property => $propertyMapping) {
226 13
            $this->rsm->addScalarResult($propertyMapping['alias'], $property, $propertyMapping['type']);
227
        }
228
229 13
        return $sql;
230
    }
231
232
    /**
233
     * Walks down a SelectStatement AST node, wrapping it in a SELECT DISTINCT.
234
     * This method is for platforms which DO NOT support ROW_NUMBER.
235
     *
236
     * @param bool $addMissingItemsFromOrderByToSelect
237
     *
238
     * @return string
239
     *
240
     * @throws RuntimeException
241
     */
242 52
    public function walkSelectStatementWithoutRowNumber(SelectStatement $AST, $addMissingItemsFromOrderByToSelect = true)
243
    {
244
        // We don't want to call this recursively!
245 52
        if ($AST->orderByClause instanceof OrderByClause && $addMissingItemsFromOrderByToSelect) {
246
            // In the case of ordering a query by columns from joined tables, we
247
            // must add those columns to the select clause of the query BEFORE
248
            // the SQL is generated.
249 44
            $this->addMissingItemsFromOrderByToSelect($AST);
250
        }
251
252
        // Remove order by clause from the inner query
253
        // It will be re-appended in the outer select generated by this method
254 52
        $orderByClause      = $AST->orderByClause;
255 52
        $AST->orderByClause = null;
256
257 52
        $innerSql           = $this->getInnerSQL($AST);
258 52
        $sqlIdentifier      = $this->getSQLIdentifier($AST);
259
        $sqlAliasIdentifier = array_map(static function ($info) {
260 52
            return $info['alias'];
261 52
        }, $sqlIdentifier);
262
263
        // Build the counter query
264 52
        $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result', implode(', ', $sqlAliasIdentifier), $innerSql);
265
266
        // http://www.doctrine-project.org/jira/browse/DDC-1958
267 52
        $sql = $this->preserveSqlOrdering($sqlAliasIdentifier, $innerSql, $sql, $orderByClause);
268
269
        // Apply the limit and offset.
270 52
        $sql = $this->platform->modifyLimitQuery(
271 52
            $sql,
272 52
            $this->maxResults,
273 52
            $this->firstResult ?? 0
274
        );
275
276
        // Add the columns to the ResultSetMapping. It's not really nice but
277
        // it works. Preferably I'd clear the RSM or simply create a new one
278
        // but that is not possible from inside the output walker, so we dirty
279
        // up the one we have.
280 52
        foreach ($sqlIdentifier as $property => $propertyMapping) {
281 52
            $this->rsm->addScalarResult($propertyMapping['alias'], $property, $propertyMapping['type']);
282
        }
283
284
        // Restore orderByClause
285 52
        $AST->orderByClause = $orderByClause;
286
287 52
        return $sql;
288
    }
289
290
    /**
291
     * Finds all PathExpressions in an AST's OrderByClause, and ensures that
292
     * the referenced fields are present in the SelectClause of the passed AST.
293
     */
294 44
    private function addMissingItemsFromOrderByToSelect(SelectStatement $AST)
295
    {
296 44
        $this->orderByPathExpressions = [];
297
298
        // We need to do this in another walker because otherwise we'll end up
299
        // polluting the state of this one.
300 44
        $walker = clone $this;
301
302
        // This will populate $orderByPathExpressions via
303
        // LimitSubqueryOutputWalker::walkPathExpression, which will be called
304
        // as the select statement is walked. We'll end up with an array of all
305
        // path expressions referenced in the query.
306 44
        $walker->walkSelectStatementWithoutRowNumber($AST, false);
307 44
        $orderByPathExpressions = $walker->getOrderByPathExpressions();
308
309
        // Get a map of referenced identifiers to field names.
310 44
        $selects = [];
311
312 44
        foreach ($orderByPathExpressions as $pathExpression) {
313 40
            $idVar = $pathExpression->identificationVariable;
314 40
            $field = $pathExpression->field;
315
316 40
            if (! isset($selects[$idVar])) {
317 40
                $selects[$idVar] = [];
318
            }
319
320 40
            $selects[$idVar][$field] = true;
321
        }
322
323
        // Loop the select clause of the AST and exclude items from $select
324
        // that are already being selected in the query.
325 44
        foreach ($AST->selectClause->selectExpressions as $selectExpression) {
326 44
            if ($selectExpression instanceof SelectExpression) {
327 44
                $idVar = $selectExpression->expression;
328
329 44
                if (! is_string($idVar)) {
330 4
                    continue;
331
                }
332
333 44
                $field = $selectExpression->fieldIdentificationVariable;
334
335 44
                if ($field === null) {
336
                    // No need to add this select, as we're already fetching the whole object.
337 44
                    unset($selects[$idVar]);
338
                } else {
339
                    unset($selects[$idVar][$field]);
340
                }
341
            }
342
        }
343
344
        // Add select items which were not excluded to the AST's select clause.
345 44
        foreach ($selects as $idVar => $fields) {
346 9
            $selectExpression = new SelectExpression(new PartialObjectExpression($idVar, array_keys($fields)), null, true);
347
348 9
            $AST->selectClause->selectExpressions[] = $selectExpression;
349
        }
350 44
    }
351
352
    /**
353
     * Generates new SQL for statements with an order by clause
354
     *
355
     * @param mixed[][] $sqlIdentifier
356
     */
357 52
    private function preserveSqlOrdering(
358
        array $sqlIdentifier,
359
        string $innerSql,
360
        string $sql,
361
        ?OrderByClause $orderByClause
362
    ) : string {
363
        // If the sql statement has an order by clause, we need to wrap it in a new select distinct statement
364 52
        if (! $orderByClause) {
365 8
            return $sql;
366
        }
367
368
        // now only select distinct identifier
369 44
        return sprintf(
370 44
            'SELECT DISTINCT %s FROM (%s) dctrn_result',
371 44
            implode(', ', $sqlIdentifier),
372 44
            $this->recreateInnerSql($orderByClause, $sqlIdentifier, $innerSql)
373
        );
374
    }
375
376
    /**
377
     * Generates a new SQL statement for the inner query to keep the correct sorting
378
     *
379
     * @param mixed[][] $identifiers
380
     */
381 44
    private function recreateInnerSql(
382
        OrderByClause $orderByClause,
383
        array $identifiers,
384
        string $innerSql
385
    ) : string {
386 44
        [$searchPatterns, $replacements] = $this->generateSqlAliasReplacements();
387
388 44
        $orderByItems = [];
389
390 44
        foreach ($orderByClause->orderByItems as $orderByItem) {
391
            // Walk order by item to get string representation of it and
392
            // replace path expressions in the order by clause with their column alias
393 44
            $orderByItemString = preg_replace(
394 44
                $searchPatterns,
395 44
                $replacements,
396 44
                $this->walkOrderByItem($orderByItem)
397
            );
398
399 44
            $orderByItems[] = $orderByItemString;
400 44
            $identifier     = substr($orderByItemString, 0, strrpos($orderByItemString, ' '));
401
402 44
            if (! in_array($identifier, $identifiers, true)) {
403 39
                $identifiers[] = $identifier;
404
            }
405
        }
406
407 44
        return $sql = sprintf(
0 ignored issues
show
Unused Code introduced by
The assignment to $sql is dead and can be removed.
Loading history...
408 44
            'SELECT DISTINCT %s FROM (%s) dctrn_result_inner ORDER BY %s',
409 44
            implode(', ', $identifiers),
410 44
            $innerSql,
411 44
            implode(', ', $orderByItems)
412
        );
413
    }
414
415
    /**
416
     * @return string[][]
417
     */
418 44
    private function generateSqlAliasReplacements() : array
419
    {
420 44
        $platform       = $this->em->getConnection()->getDatabasePlatform();
421 44
        $searchPatterns = $replacements = [];
422
423
        // Generate search patterns for each field's path expression in the order by clause
424 44
        foreach ($this->rsm->fieldMappings as $fieldAlias => $fieldName) {
425 44
            $dqlAliasForFieldAlias = $this->rsm->columnOwnerMap[$fieldAlias];
426 44
            $class                 = $this->queryComponents[$dqlAliasForFieldAlias]['metadata'];
427 44
            $property              = $class->getProperty($fieldName);
428
429
            // If the field is from a joined child table, we won't be ordering on it.
430 44
            if ($property === null) {
431 1
                continue;
432
            }
433
434
            // Get the SQL table alias for the entity and field and the column name as will appear in the select list
435 44
            $tableAlias = $this->getSQLTableAlias($property->getTableName(), $dqlAliasForFieldAlias);
436 44
            $columnName = $platform->quoteIdentifier($property->getColumnName());
437
438
            // Compose search and replace patterns
439 44
            $searchPatterns[] = sprintf(self::ORDER_BY_PATH_EXPRESSION, $tableAlias, $columnName);
440 44
            $replacements[]   = $fieldAlias;
441
        }
442
443 44
        return [$searchPatterns, $replacements];
444
    }
445
446
    /**
447
     * getter for $orderByPathExpressions
448
     *
449
     * @return PathExpression[]
450
     */
451 44
    public function getOrderByPathExpressions()
452
    {
453 44
        return $this->orderByPathExpressions;
454
    }
455
456
    /**
457
     * @return string
458
     *
459
     * @throws OptimisticLockException
460
     * @throws QueryException
461
     */
462 65
    private function getInnerSQL(SelectStatement $AST)
463
    {
464
        // Set every select expression as visible(hidden = false) to
465
        // make $AST have scalar mappings properly - this is relevant for referencing selected
466
        // fields from outside the subquery, for example in the ORDER BY segment
467 65
        $hiddens = [];
468
469 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
470 65
            $hiddens[$idx]                   = $expr->hiddenAliasResultVariable;
471 65
            $expr->hiddenAliasResultVariable = false;
472
        }
473
474 65
        $innerSql = parent::walkSelectStatement($AST);
475
476
        // Restore hiddens
477 65
        foreach ($AST->selectClause->selectExpressions as $idx => $expr) {
478 65
            $expr->hiddenAliasResultVariable = $hiddens[$idx];
479
        }
480
481 65
        return $innerSql;
482
    }
483
484
    /**
485
     * @return mixed[][]
486
     */
487 65
    private function getSQLIdentifier(SelectStatement $AST)
488
    {
489
        // Find out the SQL alias of the identifier column of the root entity.
490
        // It may be possible to make this work with multiple root entities but that
491
        // would probably require issuing multiple queries or doing a UNION SELECT.
492
        // So for now, it's not supported.
493
494
        // Get the root entity and alias from the AST fromClause.
495 65
        $from = $AST->fromClause->identificationVariableDeclarations;
496
497 65
        if (count($from) !== 1) {
498
            throw new RuntimeException('Cannot count query which selects two FROM components, cannot make distinction');
499
        }
500
501 65
        $fromRoot       = reset($from);
502 65
        $rootAlias      = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
503 65
        $rootClass      = $this->queryComponents[$rootAlias]['metadata'];
504 65
        $rootIdentifier = $rootClass->identifier;
505
506
        // For every identifier, find out the SQL alias by combing through the ResultSetMapping
507 65
        $sqlIdentifier = [];
508
509 65
        foreach ($rootIdentifier as $identifier) {
510 65
            $property = $rootClass->getProperty($identifier);
511
512 65
            if ($property instanceof FieldMetadata) {
513 64
                foreach (array_keys($this->rsm->fieldMappings, $identifier, true) as $alias) {
514 64
                    if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
515 64
                        $sqlIdentifier[$identifier] = [
516 64
                            'type'  => $property->getType(),
517 64
                            'alias' => $alias,
518
                        ];
519
                    }
520
                }
521 1
            } elseif ($property instanceof AssociationMetadata) {
522 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

522
                /** @scrutinizer ignore-call */ 
523
                $joinColumns = $property->getJoinColumns();
Loading history...
523 1
                $joinColumn  = reset($joinColumns);
524
525 1
                foreach (array_keys($this->rsm->metaMappings, $joinColumn->getColumnName(), true) as $alias) {
526 1
                    if ($this->rsm->columnOwnerMap[$alias] === $rootAlias) {
527 1
                        $sqlIdentifier[$identifier] = [
528 1
                            'type'  => $this->rsm->typeMappings[$alias],
529 1
                            'alias' => $alias,
530
                        ];
531
                    }
532
                }
533
            }
534
        }
535
536 65
        if (count($sqlIdentifier) === 0) {
537
            throw new RuntimeException('The Paginator does not support Queries which only yield ScalarResults.');
538
        }
539
540 65
        if (count($rootIdentifier) !== count($sqlIdentifier)) {
541
            throw new RuntimeException(sprintf(
542
                'Not all identifier properties can be found in the ResultSetMapping: %s',
543
                implode(', ', array_diff($rootIdentifier, array_keys($sqlIdentifier)))
544
            ));
545
        }
546
547 65
        return $sqlIdentifier;
548
    }
549
550
    /**
551
     * {@inheritdoc}
552
     */
553 55
    public function walkPathExpression($pathExpr)
554
    {
555 55
        if (! $this->inSubSelect && ! $this->platformSupportsRowNumber() && ! in_array($pathExpr, $this->orderByPathExpressions, true)) {
556 42
            $this->orderByPathExpressions[] = $pathExpr;
557
        }
558
559 55
        return parent::walkPathExpression($pathExpr);
560
    }
561
562
    /**
563
     * {@inheritdoc}
564
     */
565 7
    public function walkSubSelect($subselect)
566
    {
567 7
        $this->inSubSelect = true;
568
569 7
        $sql = parent::walkSubselect($subselect);
570
571 7
        $this->inSubSelect = false;
572
573 7
        return $sql;
574
    }
575
}
576