Passed
Push — master ( 071f46...4f1dbb )
by Sergei
05:06 queued 02:49
created

AbstractDQLQueryBuilder   F

Complexity

Total Complexity 112

Size/Duplication

Total Lines 597
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 227
dl 0
loc 597
rs 2
c 3
b 0
f 0
wmc 112

29 Methods

Rating   Name   Duplication   Size   Complexity  
A defaultConditionClasses() 0 16 1
A createConditionFromArray() 0 15 2
A getExpressionBuilder() 0 11 2
A selectExists() 0 3 1
A setSeparator() 0 3 1
A setExpressionBuilders() 0 3 1
A setConditionClasses() 0 3 1
A extractAlias() 0 7 2
A hasLimit() 0 3 2
A hasOffset() 0 3 3
A defaultExpressionBuilders() 0 17 1
A buildFrom() 0 10 2
B quoteTableNames() 0 27 11
B buildOrderBy() 0 21 7
A buildExpression() 0 5 1
A buildHaving() 0 5 2
B build() 0 46 10
A buildCondition() 0 15 4
C buildSelect() 0 42 12
B buildGroupBy() 0 20 7
B buildJoin() 0 43 7
A buildLimit() 0 13 5
A buildColumns() 0 21 6
A buildOrderByAndLimit() 0 17 3
A __construct() 0 6 1
A buildWhere() 0 6 2
A buildUnion() 0 18 5
A buildWithQueries() 0 25 6
A quoteCteAlias() 0 19 4

How to fix   Complexity   

Complex Class

Complex classes like AbstractDQLQueryBuilder 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 AbstractDQLQueryBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\QueryBuilder;
6
7
use Yiisoft\Db\Command\Param;
8
use Yiisoft\Db\Command\ParamBuilder;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\NotSupportedException;
13
use Yiisoft\Db\Expression\Expression;
14
use Yiisoft\Db\Expression\ExpressionBuilder;
15
use Yiisoft\Db\Expression\ExpressionBuilderInterface;
16
use Yiisoft\Db\Expression\ExpressionInterface;
17
use Yiisoft\Db\QueryBuilder\Condition\HashCondition;
18
use Yiisoft\Db\QueryBuilder\Condition\Interface\ConditionInterface;
19
use Yiisoft\Db\QueryBuilder\Condition\SimpleCondition;
20
use Yiisoft\Db\Query\Query;
21
use Yiisoft\Db\Query\QueryExpressionBuilder;
22
use Yiisoft\Db\Query\QueryInterface;
23
use Yiisoft\Db\Schema\QuoterInterface;
24
25
use function array_filter;
26
use function array_merge;
27
use function array_shift;
28
use function ctype_digit;
29
use function implode;
30
use function is_array;
31
use function is_int;
32
use function is_string;
33
use function ltrim;
34
use function preg_match;
35
use function preg_split;
36
use function reset;
37
use function strtoupper;
38
use function trim;
39
40
/**
41
 * It's used to query data from a database.
42
 *
43
 * @link https://en.wikipedia.org/wiki/Data_query_language
44
 */
45
abstract class AbstractDQLQueryBuilder implements DQLQueryBuilderInterface
46
{
47
    protected string $separator = ' ';
48
    /**
49
     * @var array Map of condition aliases to condition classes. For example:
50
     *
51
     * ```php
52
     * return [
53
     *     'LIKE' => \Yiisoft\Db\Condition\LikeCondition::class,
54
     * ];
55
     * ```
56
     *
57
     * This property is used by {@see createConditionFromArray} method.
58
     *
59
     * See default condition classes list in {@see defaultConditionClasses()} method.
60
     *
61
     * In case you want to add custom conditions support, use the {@see setConditionClasses()} method.
62
     *
63
     * @see setConditonClasses()
64
     * @see defaultConditionClasses()
65
     */
66
    protected array $conditionClasses = [];
67
    /**
68
     * @var array Map of expression aliases to expression classes.
69
     *
70
     * For example:
71
     *
72
     * ```php
73
     * [
74
     *    Expression::class => ExpressionBuilder::class
75
     * ]
76
     * ```
77
     * This property is mainly used by {@see buildExpression()} to build SQL expressions form expression objects.
78
     * See default values in {@see defaultExpressionBuilders()} method.
79
     *
80
     * {@see setExpressionBuilders()}
81
     * {@see defaultExpressionBuilders()}
82
     *
83
     * @psalm-var array<string, class-string<ExpressionBuilderInterface>>
84
     */
85
    protected array $expressionBuilders = [];
86
87
    public function __construct(
88
        protected QueryBuilderInterface $queryBuilder,
89
        private QuoterInterface $quoter
90
    ) {
91
        $this->expressionBuilders = $this->defaultExpressionBuilders();
92
        $this->conditionClasses = $this->defaultConditionClasses();
93
    }
94
95
    public function build(QueryInterface $query, array $params = []): array
96
    {
97
        $query = $query->prepare($this->queryBuilder);
98
        $params = empty($params) ? $query->getParams() : array_merge($params, $query->getParams());
99
        $clauses = [
100
            $this->buildSelect($query->getSelect(), $params, $query->getDistinct(), $query->getSelectOption()),
101
            $this->buildFrom($query->getFrom(), $params),
102
            $this->buildJoin($query->getJoins(), $params),
103
            $this->buildWhere($query->getWhere(), $params),
104
            $this->buildGroupBy($query->getGroupBy(), $params),
105
            $this->buildHaving($query->getHaving(), $params),
106
        ];
107
        $sql = implode($this->separator, array_filter($clauses));
108
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
109
110
        if (!empty($query->getOrderBy())) {
111
            /** @psalm-var array<string, ExpressionInterface|string> */
112
            foreach ($query->getOrderBy() as $expression) {
113
                if ($expression instanceof ExpressionInterface) {
114
                    $this->buildExpression($expression, $params);
115
                }
116
            }
117
        }
118
119
        if (!empty($query->getGroupBy())) {
120
            /** @psalm-var array<string, ExpressionInterface|string> */
121
            foreach ($query->getGroupBy() as $expression) {
122
                if ($expression instanceof ExpressionInterface) {
123
                    $this->buildExpression($expression, $params);
124
                }
125
            }
126
        }
127
128
        $union = $this->buildUnion($query->getUnions(), $params);
129
130
        if ($union !== '') {
131
            $sql = "($sql)$this->separator$union";
132
        }
133
134
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
135
136
        if ($with !== '') {
137
            $sql = "$with$this->separator$sql";
138
        }
139
140
        return [$sql, $params];
141
    }
142
143
    public function buildColumns(array|string $columns): string
144
    {
145
        if (!is_array($columns)) {
0 ignored issues
show
introduced by
The condition is_array($columns) is always true.
Loading history...
146
            if (str_contains($columns, '(')) {
147
                return $columns;
148
            }
149
150
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
151
        }
152
153
        /** @psalm-var array<array-key, ExpressionInterface|string> $columns */
154
        foreach ($columns as $i => $column) {
155
            if ($column instanceof ExpressionInterface) {
156
                $columns[$i] = $this->buildExpression($column);
157
            } elseif (!str_contains($column, '(')) {
158
                $columns[$i] = $this->quoter->quoteColumnName($column);
159
            }
160
        }
161
162
        /** @psalm-var string[] $columns */
163
        return implode(', ', $columns);
164
    }
165
166
    public function buildCondition(array|string|ExpressionInterface|null $condition, array &$params = []): string
167
    {
168
        if (is_array($condition)) {
0 ignored issues
show
introduced by
The condition is_array($condition) is always true.
Loading history...
169
            if (empty($condition)) {
170
                return '';
171
            }
172
173
            $condition = $this->createConditionFromArray($condition);
174
        }
175
176
        if ($condition instanceof ExpressionInterface) {
0 ignored issues
show
introduced by
$condition is always a sub-type of Yiisoft\Db\Expression\ExpressionInterface.
Loading history...
177
            return $this->buildExpression($condition, $params);
178
        }
179
180
        return $condition ?? '';
181
    }
182
183
    public function buildExpression(ExpressionInterface $expression, array &$params = []): string
184
    {
185
        $builder = $this->queryBuilder->getExpressionBuilder($expression);
186
        /** @psalm-suppress MixedMethodCall */
187
        return (string) $builder->build($expression, $params);
188
    }
189
190
    public function buildFrom(array|null $tables, array &$params): string
191
    {
192
        if (empty($tables)) {
193
            return '';
194
        }
195
196
        /** @psalm-var string[] $tables */
197
        $tables = $this->quoteTableNames($tables, $params);
198
199
        return 'FROM ' . implode(', ', $tables);
200
    }
201
202
    public function buildGroupBy(array $columns, array &$params = []): string
203
    {
204
        if (empty($columns)) {
205
            return '';
206
        }
207
208
        /** @psalm-var array<string, ExpressionInterface|string> $columns */
209
        foreach ($columns as $i => $column) {
210
            if ($column instanceof ExpressionInterface) {
211
                $columns[$i] = $this->buildExpression($column);
212
                if ($column instanceof Expression || $column instanceof QueryInterface) {
213
                    $params = array_merge($params, $column->getParams());
214
                }
215
            } elseif (!str_contains($column, '(')) {
216
                $columns[$i] = $this->quoter->quoteColumnName($column);
217
            }
218
        }
219
220
        /** @psalm-var array<string, Expression|string> $columns */
221
        return 'GROUP BY ' . implode(', ', $columns);
222
    }
223
224
    public function buildHaving(array|ExpressionInterface|string|null $condition, array &$params = []): string
225
    {
226
        $having = $this->buildCondition($condition, $params);
227
228
        return ($having === '') ? '' : ('HAVING ' . $having);
229
    }
230
231
    public function buildJoin(array $joins, array &$params): string
232
    {
233
        if (empty($joins)) {
234
            return '';
235
        }
236
237
        /**
238
         * @psalm-var array<
239
         *   array-key,
240
         *   array{
241
         *     0?:string,
242
         *     1?:array<array-key, Query|string>|string,
243
         *     2?:array|ExpressionInterface|string|null
244
         *   }|null
245
         * > $joins
246
         */
247
        foreach ($joins as $i => $join) {
248
            if (!is_array($join) || !isset($join[0], $join[1])) {
249
                throw new Exception(
250
                    'A join clause must be specified as an array of join type, join table, and optionally join '
251
                    . 'condition.'
252
                );
253
            }
254
255
            /* 0:join type, 1:join table, 2:on-condition (optional) */
256
            [$joinType, $table] = $join;
257
258
            $tables = $this->quoteTableNames((array) $table, $params);
259
260
            /** @var string $table */
261
            $table = reset($tables);
262
            $joins[$i] = "$joinType $table";
263
264
            if (isset($join[2])) {
265
                $condition = $this->buildCondition($join[2], $params);
266
                if ($condition !== '') {
267
                    $joins[$i] .= ' ON ' . $condition;
268
                }
269
            }
270
        }
271
272
        /** @psalm-var array<string> $joins */
273
        return implode($this->separator, $joins);
274
    }
275
276
    public function buildLimit(ExpressionInterface|int|null $limit, ExpressionInterface|int|null $offset): string
277
    {
278
        $sql = '';
279
280
        if ($this->hasLimit($limit)) {
281
            $sql = 'LIMIT ' . ($limit instanceof ExpressionInterface ? $this->buildExpression($limit) : (string) $limit);
282
        }
283
284
        if ($this->hasOffset($offset)) {
285
            $sql .= ' OFFSET ' . ($offset instanceof ExpressionInterface ? $this->buildExpression($offset) : (string) $offset);
286
        }
287
288
        return ltrim($sql);
289
    }
290
291
    public function buildOrderBy(array $columns, array &$params = []): string
292
    {
293
        if (empty($columns)) {
294
            return '';
295
        }
296
297
        $orders = [];
298
299
        /** @psalm-var array<string, ExpressionInterface|int|string> $columns */
300
        foreach ($columns as $name => $direction) {
301
            if ($direction instanceof ExpressionInterface) {
302
                $orders[] = $this->buildExpression($direction);
303
                if ($direction instanceof Expression || $direction instanceof QueryInterface) {
304
                    $params = array_merge($params, $direction->getParams());
305
                }
306
            } else {
307
                $orders[] = $this->quoter->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
308
            }
309
        }
310
311
        return 'ORDER BY ' . implode(', ', $orders);
312
    }
313
314
    public function buildOrderByAndLimit(
315
        string $sql,
316
        array $orderBy,
317
        ExpressionInterface|int|null $limit,
318
        ExpressionInterface|int|null $offset,
319
        array &$params = []
320
    ): string {
321
        $orderBy = $this->buildOrderBy($orderBy, $params);
322
        if ($orderBy !== '') {
323
            $sql .= $this->separator . $orderBy;
324
        }
325
        $limit = $this->buildLimit($limit, $offset);
326
        if ($limit !== '') {
327
            $sql .= $this->separator . $limit;
328
        }
329
330
        return $sql;
331
    }
332
333
    public function buildSelect(
334
        array $columns,
335
        array &$params,
336
        bool|null $distinct = false,
337
        string $selectOption = null
338
    ): string {
339
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
340
341
        if ($selectOption !== null) {
342
            $select .= ' ' . $selectOption;
343
        }
344
345
        if (empty($columns)) {
346
            return $select . ' *';
347
        }
348
349
        /** @psalm-var array<array-key, ExpressionInterface|string> $columns */
350
        foreach ($columns as $i => $column) {
351
            if ($column instanceof ExpressionInterface) {
352
                if (is_int($i)) {
353
                    $columns[$i] = $this->buildExpression($column, $params);
354
                } else {
355
                    $columns[$i] = $this->buildExpression($column, $params) . ' AS '
356
                        . $this->quoter->quoteColumnName($i);
357
                }
358
            } elseif (is_string($i) && $i !== $column) {
359
                if (!str_contains($column, '(')) {
360
                    $column = $this->quoter->quoteColumnName($column);
361
                }
362
                $columns[$i] = "$column AS " . $this->quoter->quoteColumnName($i);
363
            } elseif (!str_contains($column, '(')) {
364
                if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $column, $matches)) {
365
                    $columns[$i] = $this->quoter->quoteColumnName($matches[1])
366
                        . ' AS ' . $this->quoter->quoteColumnName($matches[2]);
367
                } else {
368
                    $columns[$i] = $this->quoter->quoteColumnName($column);
369
                }
370
            }
371
        }
372
373
        /** @psalm-var array<string, Expression|string> $columns */
374
        return $select . ' ' . implode(', ', $columns);
375
    }
376
377
    public function buildUnion(array $unions, array &$params): string
378
    {
379
        if (empty($unions)) {
380
            return '';
381
        }
382
383
        $result = '';
384
385
        /** @psalm-var array<array{query:string|Query, all:bool}> $unions */
386
        foreach ($unions as $union) {
387
            if ($union['query'] instanceof QueryInterface) {
388
                [$union['query'], $params] = $this->build($union['query'], $params);
389
            }
390
391
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $union['query'] . ' ) ';
392
        }
393
394
        return trim($result);
395
    }
396
397
    public function buildWhere(
398
        array|string|ConditionInterface|ExpressionInterface|null $condition,
399
        array &$params = []
400
    ): string {
401
        $where = $this->buildCondition($condition, $params);
402
        return ($where === '') ? '' : ('WHERE ' . $where);
403
    }
404
405
    public function buildWithQueries(array $withs, array &$params): string
406
    {
407
        if (empty($withs)) {
408
            return '';
409
        }
410
411
        $recursive = false;
412
        $result = [];
413
414
        /** @psalm-var array{query:string|Query, alias:ExpressionInterface|string, recursive:bool}[] $withs */
415
        foreach ($withs as $with) {
416
            if ($with['recursive']) {
417
                $recursive = true;
418
            }
419
420
            if ($with['query'] instanceof QueryInterface) {
421
                [$with['query'], $params] = $this->build($with['query'], $params);
422
            }
423
424
            $quotedAlias = $this->quoteCteAlias($with['alias']);
425
426
            $result[] = $quotedAlias . ' AS (' . $with['query'] . ')';
427
        }
428
429
        return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . implode(', ', $result);
430
    }
431
432
    public function createConditionFromArray(array $condition): ConditionInterface
433
    {
434
        /** operator format: operator, operand 1, operand 2, ... */
435
        if (isset($condition[0])) {
436
            $operator = strtoupper((string) array_shift($condition));
437
438
            /** @var string $className */
439
            $className = $this->conditionClasses[$operator] ?? SimpleCondition::class;
440
441
            /** @var ConditionInterface $className */
442
            return $className::fromArrayDefinition($operator, $condition);
443
        }
444
445
        /** hash format: 'column1' => 'value1', 'column2' => 'value2', ... */
446
        return new HashCondition($condition);
447
    }
448
449
    public function getExpressionBuilder(ExpressionInterface $expression): object
450
    {
451
        $className = $expression::class;
452
453
        if (!isset($this->expressionBuilders[$className])) {
454
            throw new InvalidArgumentException(
455
                'Expression of class ' . $className . ' can not be built in ' . static::class
456
            );
457
        }
458
459
        return new $this->expressionBuilders[$className]($this->queryBuilder);
460
    }
461
462
    public function selectExists(string $rawSql): string
463
    {
464
        return 'SELECT EXISTS(' . $rawSql . ')';
465
    }
466
467
    public function setConditionClasses(array $classes): void
468
    {
469
        $this->conditionClasses = array_merge($this->conditionClasses, $classes);
470
    }
471
472
    public function setExpressionBuilders(array $builders): void
473
    {
474
        $this->expressionBuilders = array_merge($this->expressionBuilders, $builders);
475
    }
476
477
    /**
478
     * @param string $separator The separator between different fragments of an SQL statement.
479
     *
480
     * Defaults to an empty space. This is mainly used by {@see build()} when generating a SQL statement.
481
     */
482
    public function setSeparator(string $separator): void
483
    {
484
        $this->separator = $separator;
485
    }
486
487
    /**
488
     * Has an array of default condition classes.
489
     *
490
     * Extend this method if you want to change default condition classes for the query builder.
491
     *
492
     * See {@see conditionClasses} docs for details.
493
     */
494
    protected function defaultConditionClasses(): array
495
    {
496
        return [
497
            'NOT' => Condition\NotCondition::class,
498
            'AND' => Condition\AndCondition::class,
499
            'OR' => Condition\OrCondition::class,
500
            'BETWEEN' => Condition\BetweenCondition::class,
501
            'NOT BETWEEN' => Condition\BetweenCondition::class,
502
            'IN' => Condition\InCondition::class,
503
            'NOT IN' => Condition\InCondition::class,
504
            'LIKE' => Condition\LikeCondition::class,
505
            'NOT LIKE' => Condition\LikeCondition::class,
506
            'OR LIKE' => Condition\LikeCondition::class,
507
            'OR NOT LIKE' => Condition\LikeCondition::class,
508
            'EXISTS' => Condition\ExistsCondition::class,
509
            'NOT EXISTS' => Condition\ExistsCondition::class,
510
        ];
511
    }
512
513
    /**
514
     * Has an array of default expression builders.
515
     *
516
     * Extend this method and override it if you want to change default expression builders for this query builder.
517
     *
518
     * See {@see expressionBuilders} docs for details.
519
     *
520
     * @psalm-return array<string, class-string<ExpressionBuilderInterface>>
521
     */
522
    protected function defaultExpressionBuilders(): array
523
    {
524
        return [
525
            Query::class => QueryExpressionBuilder::class,
526
            Param::class => ParamBuilder::class,
527
            Expression::class => ExpressionBuilder::class,
528
            Condition\AbstractConjunctionCondition::class => Condition\Builder\ConjunctionConditionBuilder::class,
529
            Condition\NotCondition::class => Condition\Builder\NotConditionBuilder::class,
530
            Condition\AndCondition::class => Condition\Builder\ConjunctionConditionBuilder::class,
531
            Condition\OrCondition::class => Condition\Builder\ConjunctionConditionBuilder::class,
532
            Condition\BetweenCondition::class => Condition\Builder\BetweenConditionBuilder::class,
533
            Condition\InCondition::class => Condition\Builder\InConditionBuilder::class,
534
            Condition\LikeCondition::class => Condition\Builder\LikeConditionBuilder::class,
535
            Condition\ExistsCondition::class => Condition\Builder\ExistsConditionBuilder::class,
536
            Condition\SimpleCondition::class => Condition\Builder\SimpleConditionBuilder::class,
537
            Condition\HashCondition::class => Condition\Builder\HashConditionBuilder::class,
538
            Condition\BetweenColumnsCondition::class => Condition\Builder\BetweenColumnsConditionBuilder::class,
539
        ];
540
    }
541
542
    /**
543
     * Extracts table alias if there is one or returns false.
544
     *
545
     * @psalm-return string[]|bool
546
     */
547
    protected function extractAlias(string $table): array|bool
548
    {
549
        if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) {
550
            return $matches;
551
        }
552
553
        return false;
554
    }
555
556
    /**
557
     * Checks to see if the given limit is effective.
558
     *
559
     * @param mixed $limit The given limit.
560
     *
561
     * @return bool Whether the limit is effective.
562
     */
563
    protected function hasLimit(mixed $limit): bool
564
    {
565
        return ($limit instanceof ExpressionInterface) || ctype_digit((string) $limit);
566
    }
567
568
    /**
569
     * Checks to see if the given offset is effective.
570
     *
571
     * @param mixed $offset The given offset.
572
     *
573
     * @return bool Whether the offset is effective.
574
     */
575
    protected function hasOffset(mixed $offset): bool
576
    {
577
        return ($offset instanceof ExpressionInterface) || (ctype_digit((string)$offset) && (string)$offset !== '0');
578
    }
579
580
    /**
581
     * @throws Exception
582
     * @throws InvalidConfigException
583
     * @throws NotSupportedException
584
     *
585
     * @return array The list of table names with quote.
586
     */
587
    private function quoteTableNames(array $tables, array &$params): array
588
    {
589
        /** @psalm-var array<array-key, array|QueryInterface|string> $tables */
590
        foreach ($tables as $i => $table) {
591
            if ($table instanceof QueryInterface) {
592
                [$sql, $params] = $this->build($table, $params);
593
                $tables[$i] = "($sql) " . $this->quoter->quoteTableName((string) $i);
594
            } elseif (is_string($table) && is_string($i)) {
595
                if (!str_contains($table, '(')) {
596
                    $table = $this->quoter->quoteTableName($table);
597
                }
598
                $tables[$i] = "$table " . $this->quoter->quoteTableName($i);
599
            } elseif ($table instanceof ExpressionInterface && is_string($i)) {
600
                $table = $this->buildExpression($table, $params);
601
                $tables[$i] = "$table " . $this->quoter->quoteTableName($i);
602
            } elseif (is_string($table) && !str_contains($table, '(')) {
603
                $tableWithAlias = $this->extractAlias($table);
604
                if (is_array($tableWithAlias)) { // with alias
605
                    $tables[$i] = $this->quoter->quoteTableName($tableWithAlias[1]) . ' '
606
                        . $this->quoter->quoteTableName($tableWithAlias[2]);
607
                } else {
608
                    $tables[$i] = $this->quoter->quoteTableName($table);
609
                }
610
            }
611
        }
612
613
        return $tables;
614
    }
615
616
    /**
617
     * Quotes an alias of Common Table Expressions (CTE)
618
     *
619
     * @param ExpressionInterface|string $name The alias name with or without column names to quote.
620
     *
621
     * @return string The quoted alias.
622
     */
623
    private function quoteCteAlias(ExpressionInterface|string $name): string
624
    {
625
        if ($name instanceof ExpressionInterface) {
626
            return $this->buildExpression($name);
627
        }
628
629
        if (!str_contains($name, '(')) {
630
            return $this->quoter->quoteTableName($name);
631
        }
632
633
        if (!str_ends_with($name, ')')) {
634
            return $name;
635
        }
636
637
        /** @psalm-suppress PossiblyUndefinedArrayOffset */
638
        [$name, $columns] = explode('(', substr($name, 0, -1), 2);
639
        $name = trim($name);
640
641
        return $this->quoter->quoteTableName($name) . '(' . $this->buildColumns($columns) . ')';
642
    }
643
}
644