Passed
Pull Request — master (#282)
by Wilmer
13:20
created

QueryBuilder::buildSelect()   C

Complexity

Conditions 13
Paths 40

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 15.3362

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 26
c 1
b 0
f 0
nc 40
nop 4
dl 0
loc 43
ccs 19
cts 25
cp 0.76
crap 15.3362
rs 6.6166

How to fix   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 Yiisoft\Db\Query;
6
7
use Generator;
8
use JsonException;
9
use Yiisoft\Db\Command\Command;
10
use Yiisoft\Db\Constraint\Constraint;
11
use Yiisoft\Db\Exception\Exception;
12
use Yiisoft\Db\Exception\InvalidArgumentException;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
use Yiisoft\Db\Expression\Expression;
16
use Yiisoft\Db\Expression\ExpressionBuilder;
17
use Yiisoft\Db\Expression\ExpressionBuilderInterface;
18
use Yiisoft\Db\Expression\ExpressionInterface;
19
use Yiisoft\Db\Pdo\PdoValue;
20
use Yiisoft\Db\Pdo\PdoValueBuilder;
21
use Yiisoft\Db\Query\Conditions\HashCondition;
22
use Yiisoft\Db\Query\Conditions\Interface\ConditionInterface;
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_INTERFACE, expecting T_STRING or '{' on line 22 at column 32
Loading history...
23
use Yiisoft\Db\Query\Conditions\SimpleCondition;
24
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
25
use Yiisoft\Db\Schema\QuoterInterface;
26
use Yiisoft\Db\Schema\SchemaInterface;
27
28
use function array_combine;
29
use function array_diff;
30
use function array_filter;
31
use function array_keys;
32
use function array_map;
33
use function array_merge;
34
use function array_shift;
35
use function array_unique;
36
use function array_values;
37
use function count;
38
use function ctype_digit;
39
use function get_class;
40
use function implode;
41
use function in_array;
42
use function is_array;
43
use function is_int;
44
use function is_string;
45
use function json_encode;
46
use function ltrim;
47
use function preg_match;
48
use function preg_replace;
49
use function preg_split;
50
use function reset;
51
use function strtoupper;
52
use function trim;
53
54
/**
55
 * QueryBuilder builds a SELECT SQL statement based on the specification given as a {@see Query} object.
56
 *
57
 * SQL statements are created from {@see Query} objects using the {@see build()}-method.
58
 *
59
 * QueryBuilder is also used by {@see Command} to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE.
60
 *
61
 * For more details and usage information on QueryBuilder:
62
 * {@see [guide article on query builders](guide:db-query-builder)}.
63
 *
64
 * @property string[] $conditionClasses Map of condition aliases to condition classes. This property is write-only.
65
 *
66
 * For example:
67
 * ```php
68
 *     ['LIKE' => \Yiisoft\Db\Condition\LikeCondition::class]
69
 * ```
70
 * @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined one's in
71
 * {@see expressionBuilders} property. This property is write-only.
72
 */
73
abstract class QueryBuilder implements QueryBuilderInterface
74
{
75
    /**
76
     * The prefix for automatically generated query binding parameters.
77
     */
78
    public const PARAM_PREFIX = ':qp';
79
80
    /**
81
     * @var array the abstract column types mapped to physical column types.
82
     * This is mainly used to support creating/modifying tables using DB-independent data type specifications.
83
     * Child classes should override this property to declare supported type mappings.
84
     *
85
     * @psalm-var string[]
86
     */
87
    protected array $typeMap = [];
88
89
    /**
90
     * @var array map of condition aliases to condition classes. For example:
91
     *
92
     * ```php
93
     * return [
94
     *     'LIKE' => \Yiisoft\Db\Condition\LikeCondition::class,
95
     * ];
96
     * ```
97
     *
98
     * This property is used by {@see createConditionFromArray} method.
99
     * See default condition classes list in {@see defaultConditionClasses()} method.
100
     *
101
     * In case you want to add custom conditions support, use the {@see setConditionClasses()} method.
102
     *
103
     * @see setConditonClasses()
104
     * @see defaultConditionClasses()
105
     */
106
    protected array $conditionClasses = [];
107
108
    /**
109
     * @psalm-var string[] maps expression class to expression builder class.
110
     * For example:
111
     *
112
     * ```php
113
     * [
114
     *    Expression::class => ExpressionBuilder::class
115
     * ]
116
     * ```
117
     * This property is mainly used by {@see buildExpression()} to build SQL expressions form expression objects.
118
     * See default values in {@see defaultExpressionBuilders()} method.
119
     *
120
     * {@see setExpressionBuilders()}
121
     * {@see defaultExpressionBuilders()}
122
     */
123
    protected array $expressionBuilders = [];
124
    protected string $separator = ' ';
125
    protected DDLQueryBuilder $ddlBuilder;
126
    protected DMLQueryBuilder $dmlBuilder;
127
128
    public function __construct(
129
        private QuoterInterface $quoter,
130
        private SchemaInterface $schema
131
    ) {
132 1960
        $this->expressionBuilders = $this->defaultExpressionBuilders();
133
        $this->conditionClasses = $this->defaultConditionClasses();
134 1960
    }
135 1960
136 1960
    public function addCheck(string $name, string $table, string $expression): string
137 1960
    {
138
        return $this->ddlBuilder->addCheck($name, $table, $expression);
139
    }
140
141
    public function addColumn(string $table, string $column, string $type): string
142
    {
143
        return $this->ddlBuilder->addColumn($table, $column, $type);
144
    }
145
146
    public function addCommentOnColumn(string $table, string $column, string $comment): string
147 1960
    {
148
        return $this->ddlBuilder->addCommentOnColumn($table, $column, $comment);
149
    }
150 1960
151
    public function addCommentOnTable(string $table, string $comment): string
152
    {
153
        return $this->ddlBuilder->addCommentOnTable($table, $comment);
154
    }
155
156
    public function addDefaultValue(string $name, string $table, string $column, mixed $value): string
157
    {
158
        return $this->ddlBuilder->addDefaultValue($name, $table, $column, $value);
159
    }
160
161
    public function addForeignKey(
162
        string $name,
163
        string $table,
164
        array|string $columns,
165
        string $refTable,
166
        array|string $refColumns,
167
        ?string $delete = null,
168
        ?string $update = null
169
    ): string {
170
        return $this->ddlBuilder->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update);
171
    }
172
173
    public function addPrimaryKey(string $name, string $table, array|string $columns): string
174 1960
    {
175
        return $this->ddlBuilder->addPrimaryKey($name, $table, $columns);
176
    }
177 1960
178
    public function addUnique(string $name, string $table, array|string $columns): string
179
    {
180
        return $this->ddlBuilder->addUnique($name, $table, $columns);
181
    }
182
183
    public function alterColumn(string $table, string $column, string $type): string
184
    {
185
        return $this->ddlBuilder->alterColumn($table, $column, $type);
186
    }
187
188
    public function batchInsert(string $table, array $columns, iterable|Generator $rows, array &$params = []): string
189
    {
190
        return $this->dmlBuilder->batchInsert($table, $columns, $rows, $params);
191
    }
192
193
    public function bindParam(mixed $value, array &$params = []): string
194
    {
195
        $phName = self::PARAM_PREFIX . count($params);
196
        /** @psalm-var mixed */
197
        $params[$phName] = $value;
198
199
        return $phName;
200
    }
201
202
    public function build(QueryInterface $query, array $params = []): array
203
    {
204
        $query = $query->prepare($this);
205
        $params = empty($params) ? $query->getParams() : array_merge($params, $query->getParams());
206
        $clauses = [
207
            $this->buildSelect($query->getSelect(), $params, $query->getDistinct(), $query->getSelectOption()),
208
            $this->buildFrom($query->getFrom(), $params),
209
            $this->buildJoin($query->getJoin(), $params),
210
            $this->buildWhere($query->getWhere(), $params),
211
            $this->buildGroupBy($query->getGroupBy(), $params),
212
            $this->buildHaving($query->getHaving(), $params),
213
        ];
214
        $sql = implode($this->separator, array_filter($clauses));
215
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
216
217
        if (!empty($query->getOrderBy())) {
218
            foreach ($query->getOrderBy() as $expression) {
219
                if ($expression instanceof ExpressionInterface) {
220
                    $this->buildExpression($expression, $params);
221
                }
222
            }
223
        }
224
225
        if (!empty($query->getGroupBy())) {
226
            foreach ($query->getGroupBy() as $expression) {
227
                if ($expression instanceof ExpressionInterface) {
228
                    $this->buildExpression($expression, $params);
229
                }
230
            }
231
        }
232
233
        $union = $this->buildUnion($query->getUnion(), $params);
234
235
        if ($union !== '') {
236 1234
            $sql = "($sql)$this->separator$union";
237
        }
238 1234
239
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
240 1234
241
        if ($with !== '') {
242
            $sql = "$with$this->separator$sql";
243 1234
        }
244 1234
245 1234
        return [$sql, $params];
246 1234
    }
247 1234
248 1234
    public function buildColumns(array|string $columns): string
249
    {
250
        if (!is_array($columns)) {
251 1234
            if (str_contains($columns, '(')) {
252
                return $columns;
253 1234
            }
254
255 1234
            $rawColumns = $columns;
256 180
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
257 180
258 4
            if ($columns === false) {
259
                throw new InvalidArgumentException("$rawColumns is not valid columns.");
260
            }
261
        }
262
263 1234
        foreach ($columns as $i => $column) {
264 12
            if ($column instanceof ExpressionInterface) {
265 12
                $columns[$i] = $this->buildExpression($column);
266 4
            } elseif (!str_contains($column, '(')) {
267
                $columns[$i] = $this->quoter->quoteColumnName($column);
268
            }
269
        }
270
271 1234
        return implode(', ', $columns);
272
    }
273 1234
274 9
    public function buildCondition(array|string|ExpressionInterface|null $condition, array &$params = []): string
275
    {
276
        if (is_array($condition)) {
277 1234
            if (empty($condition)) {
278
                return '';
279 1234
            }
280 8
281
            $condition = $this->createConditionFromArray($condition);
282
        }
283 1234
284
        if ($condition instanceof ExpressionInterface) {
285
            return $this->buildExpression($condition, $params);
286
        }
287
288
        return $condition ?? '';
289
    }
290
291
    /**
292
     * @psalm-suppress UndefinedInterfaceMethod
293
     */
294
    public function buildExpression(ExpressionInterface $expression, array &$params = []): string
295
    {
296
        $builder = $this->getExpressionBuilder($expression);
297
        return (string) $builder->build($expression, $params);
298
    }
299
300
    public function buildFrom(?array $tables, array &$params): string
301
    {
302 1376
        if (empty($tables)) {
303
            return '';
304 1376
        }
305
306 1376
        $tables = $this->quoteTableNames($tables, $params);
307
308
        return 'FROM ' . implode(', ', $tables);
309
    }
310
311
    public function buildGroupBy(array $columns, array &$params = []): string
312
    {
313
        if (empty($columns)) {
314
            return '';
315
        }
316
317
        foreach ($columns as $i => $column) {
318
            if ($column instanceof Expression) {
319
                $columns[$i] = $this->buildExpression($column);
320
                $params = array_merge($params, $column->getParams());
321
            } elseif (!str_contains($column, '(')) {
322 1376
                $columns[$i] = $this->quoter->quoteColumnName($column);
323
            }
324 1376
        }
325
326 1376
        return 'GROUP BY ' . implode(', ', $columns);
327
    }
328
329
    public function buildHaving(array|string|null $condition, array &$params = []): string
330
    {
331
        $having = $this->buildCondition($condition, $params);
332
333
        return ($having === '') ? '' : ('HAVING ' . $having);
334
    }
335
336
    public function buildJoin(array $joins, array &$params): string
337
    {
338
        if (empty($joins)) {
339
            return '';
340
        }
341 1376
342
        foreach ($joins as $i => $join) {
343
            if (!is_array($join) || !isset($join[0], $join[1])) {
344
                throw new Exception(
345 1376
                    'A join clause must be specified as an array of join type, join table, and optionally join '
346 1376
                    . 'condition.'
347
                );
348
            }
349 1376
350
            /* 0:join type, 1:join table, 2:on-condition (optional) */
351
            [$joinType, $table] = $join;
352
353
            $tables = $this->quoteTableNames((array) $table, $params);
354
355
            /** @var string $table */
356
            $table = reset($tables);
357
            $joins[$i] = "$joinType $table";
358
359
            if (isset($join[2])) {
360
                $condition = $this->buildCondition($join[2], $params);
361
                if ($condition !== '') {
362
                    $joins[$i] .= ' ON ' . $condition;
363
                }
364
            }
365
        }
366
367
        /** @psalm-var array<string> $joins */
368
        return implode($this->separator, $joins);
369
    }
370
371
    public function buildLimit(Expression|int|null $limit, Expression|int|null $offset): string
372
    {
373
        $sql = '';
374
375
        if ($this->hasLimit($limit)) {
376 209
            $sql = 'LIMIT ' . (string) $limit;
377
        }
378 209
379
        if ($this->hasOffset($offset)) {
380 197
            $sql .= ' OFFSET ' . (string) $offset;
381 197
        }
382 197
383
        return ltrim($sql);
384
    }
385
386
    public function buildOrderBy(array $columns, array &$params = []): string
387
    {
388
        if (empty($columns)) {
389
            return '';
390
        }
391
392
        $orders = [];
393
394
        foreach ($columns as $name => $direction) {
395
            if ($direction instanceof Expression) {
396
                $orders[] = $this->buildExpression($direction);
397
                $params = array_merge($params, $direction->getParams());
398 298
            } else {
399
                $orders[] = $this->quoter->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
400 298
            }
401 298
        }
402 298
403 298
        return 'ORDER BY ' . implode(', ', $orders);
404 298
    }
405 298
406
    public function buildOrderByAndLimit(
407 298
        string $sql,
408 69
        array $orderBy,
409
        Expression|int|null $limit,
410 238
        Expression|int|null $offset,
411 233
        array &$params = []
412 233
    ): string {
413
        $orderBy = $this->buildOrderBy($orderBy, $params);
414 233
        if ($orderBy !== '') {
415 51
            $sql .= $this->separator . $orderBy;
416 227
        }
417
        $limit = $this->buildLimit($limit, $offset);
418
        if ($limit !== '') {
419
            $sql .= $this->separator . $limit;
420 227
        }
421
422
        return $sql;
423
    }
424
425 283
    public function buildSelect(
426
        array $columns,
427
        array &$params,
428
        ?bool $distinct = false,
429
        string $selectOption = null
430
    ): string {
431
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
432
433
        if ($selectOption !== null) {
434
            $select .= ' ' . $selectOption;
435
        }
436
437
        if (empty($columns)) {
438
            return $select . ' *';
439
        }
440 69
441
        foreach ($columns as $i => $column) {
442
            if ($column instanceof ExpressionInterface) {
443 69
                if (is_int($i)) {
444 69
                    $columns[$i] = $this->buildExpression($column, $params);
445 69
                } else {
446
                    $columns[$i] = $this->buildExpression($column, $params) . ' AS '
447 15
                        . $this->quoter->quoteColumnName($i);
448
                }
449
            } elseif ($column instanceof QueryInterface) {
450 54
                [$sql, $params] = $this->build($column, $params);
451
                $columns[$i] = "($sql) AS " . $this->quoter->quoteColumnName((string) $i);
452 54
            } elseif (is_string($i) && $i !== $column) {
453 54
                if (!str_contains($column, '(')) {
454
                    $column = $this->quoter->quoteColumnName($column);
455 54
                }
456 54
                $columns[$i] = "$column AS " . $this->quoter->quoteColumnName($i);
457 54
            } elseif (!str_contains($column, '(')) {
458
                if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $column, $matches)) {
459
                    $columns[$i] = $this->quoter->quoteColumnName(
460
                        $matches[1]
461
                    ) . ' AS ' . $this->quoter->quoteColumnName($matches[2]);
462
                } else {
463
                    $columns[$i] = $this->quoter->quoteColumnName($column);
464
                }
465 54
            }
466
        }
467
468
        return $select . ' ' . implode(', ', $columns);
469
    }
470
471
    public function buildUnion(array $unions, array &$params): string
472
    {
473
        if (empty($unions)) {
474
            return '';
475
        }
476
477
        $result = '';
478
479
        foreach ($unions as $i => $union) {
480
            $query = $union['query'];
481
            if ($query instanceof QueryInterface) {
482
                [$unions[$i]['query'], $params] = $this->build($query, $params);
483
            }
484
485
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
486
        }
487
488
        return trim($result);
489
    }
490
491
    public function buildWhere(
492
        array|string|ConditionInterface|ExpressionInterface|null $condition,
493
        array &$params = []
494 40
    ): string {
495
        $where = $this->buildCondition($condition, $params);
496 40
        return ($where === '') ? '' : ('WHERE ' . $where);
497 4
    }
498
499
    public function buildWithQueries(array $withs, array &$params): string
500 38
    {
501
        if (empty($withs)) {
502
            return '';
503 38
        }
504 38
505
        $recursive = false;
506
        $result = [];
507
508
        foreach ($withs as $with) {
509 38
            if ($with['recursive']) {
510
                $recursive = true;
511 38
            }
512 35
513 35
            $query = $with['query'];
514 35
            if ($query instanceof QueryInterface) {
515 26
                [$with['query'], $params] = $this->build($query, $params);
516
            }
517 35
518 23
            $result[] = $with['alias'] . ' AS (' . $with['query'] . ')';
519 21
        }
520
521 2
        return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . implode(', ', $result);
522 21
    }
523 7
524 21
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
525 12
    {
526 13
        return $this->ddlBuilder->checkIntegrity($schema, $table, $check);
527 9
    }
528
529 35
    public function createConditionFromArray(array $condition): ConditionInterface
530
    {
531 35
        /** operator format: operator, operand 1, operand 2, ... */
532
        if (isset($condition[0])) {
533
            $operator = strtoupper((string) array_shift($condition));
534 38
535 3
            $className = $this->conditionClasses[$operator] ?? SimpleCondition::class;
536
537
            /** @var ConditionInterface $className */
538 35
            return $className::fromArrayDefinition($operator, $condition);
539 32
        }
540
541
        /** hash format: 'column1' => 'value1', 'column2' => 'value2', ... */
542 35
        return new HashCondition($condition);
543 35
    }
544
545
    public function createIndex(string $name, string $table, array|string $columns, bool $unique = false): string
546
    {
547
        return $this->ddlBuilder->createIndex($name, $table, $columns, $unique);
548
    }
549
550
    public function createTable(string $table, array $columns, ?string $options = null): string
551
    {
552
        return $this->ddlBuilder->createTable($table, $columns, $options);
553
    }
554
555
    public function createView(string $viewName, Query|string $subQuery): string
556
    {
557
        return $this->ddlBuilder->createView($viewName, $subQuery);
558
    }
559
560
    public function delete(string $table, array|string $condition, array &$params): string
561
    {
562
        return $this->dmlBuilder->delete($table, $condition, $params);
563
    }
564
565
    public function dropCheck(string $name, string $table): string
566
    {
567
        return $this->ddlBuilder->dropCheck($name, $table);
568
    }
569
570
    public function dropColumn(string $table, string $column): string
571
    {
572
        return $this->ddlBuilder->dropColumn($table, $column);
573
    }
574
575
    public function dropCommentFromColumn(string $table, string $column): string
576
    {
577
        return $this->ddlBuilder->dropCommentFromColumn($table, $column);
578
    }
579
580
    public function dropCommentFromTable(string $table): string
581
    {
582
        return $this->ddlBuilder->dropCommentFromTable($table);
583
    }
584
585
    public function dropDefaultValue(string $name, string $table): string
586
    {
587
        return $this->ddlBuilder->dropDefaultValue($name, $table);
588
    }
589
590
    public function dropForeignKey(string $name, string $table): string
591
    {
592
        return $this->ddlBuilder->dropForeignKey($name, $table);
593 90
    }
594
595 90
    public function dropIndex(string $name, string $table): string
596 40
    {
597
        return $this->ddlBuilder->dropIndex($name, $table);
598 50
    }
599
600
    public function dropPrimaryKey(string $name, string $table): string
601 90
    {
602
        return $this->ddlBuilder->dropPrimaryKey($name, $table);
603 90
    }
604
605 90
    public function dropTable(string $table): string
606 65
    {
607
        return $this->ddlBuilder->dropTable($table);
608
    }
609 25
610
    public function dropUnique(string $name, string $table): string
611
    {
612
        return $this->ddlBuilder->dropUnique($name, $table);
613
    }
614
615
    public function dropView(string $viewName): string
616
    {
617
        return $this->ddlBuilder->dropView($viewName);
618
    }
619
620
    public function getColumnType(ColumnSchemaBuilder|string $type): string
621
    {
622
        if ($type instanceof ColumnSchemaBuilder) {
623
            $type = $type->__toString();
624
        }
625
626
        if (isset($this->typeMap[$type])) {
627 90
            return $this->typeMap[$type];
628
        }
629 90
630
        if (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
631 90
            if (isset($this->typeMap[$matches[1]])) {
632 90
                return preg_replace(
633
                    '/\(.+\)/',
634 90
                    '(' . $matches[2] . ')',
635 88
                    $this->typeMap[$matches[1]]
636
                ) . $matches[3];
637
            }
638 90
        } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
639 89
            if (isset($this->typeMap[$matches[1]])) {
640 89
                return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
641
            }
642
        }
643
644 90
        return $type;
645
    }
646
647 90
    public function getExpressionBuilder(ExpressionInterface $expression): ExpressionBuilderInterface
648 90
    {
649 90
        $className = get_class($expression);
650 90
651 90
        if (!isset($this->expressionBuilders[$className])) {
652
            throw new InvalidArgumentException(
653 90
                'Expression of class ' . $className . ' can not be built in ' . static::class
654
            );
655 90
        }
656
657 90
        return new $this->expressionBuilders[$className]($this);
658
    }
659
660 90
    public function getQuoter(): QuoterInterface
661
    {
662
        return $this->quoter;
663 90
    }
664 90
665 90
    public function insert(string $table, Query|array $columns, array &$params = []): string
666 90
    {
667
        return $this->dmlBuilder->insert($table, $columns, $params);
668 90
    }
669 90
670
    public function insertEx(string $table, Query|array $columns, array &$params = []): string
671 90
    {
672 75
        return $this->dmlBuilder->insertEx($table, $columns, $params);
673
    }
674
675 90
    public function renameColumn(string $table, string $oldName, string $newName): string
676 90
    {
677
        return $this->ddlBuilder->renameColumn($table, $oldName, $newName);
678
    }
679
680 90
    public function renameTable(string $oldName, string $newName): string
681
    {
682
        return $this->ddlBuilder->renameTable($oldName, $newName);
683
    }
684
685
    public function resetSequence(string $tableName, array|int|string|null $value = null): string
686
    {
687
        return $this->dmlBuilder->resetSequence($tableName, $value);
688
    }
689
690
    public function selectExists(string $rawSql): string
691
    {
692
        return $this->dmlBuilder->selectExists($rawSql);
693
    }
694
695
    /**
696
     * Setter for {@see conditionClasses} property.
697
     *
698
     * @param string[] $classes map of condition aliases to condition classes. For example:
699
     *
700
     * ```php
701
     * ['LIKE' => \Yiisoft\Db\Condition\LikeCondition::class]
702
     * ```
703
     *
704
     * See {@see conditionClasses} docs for details.
705
     */
706
    public function setConditionClasses(array $classes): void
707
    {
708 94
        $this->conditionClasses = array_merge($this->conditionClasses, $classes);
709
    }
710
711
    /**
712
     * Setter for {@see expressionBuilders property.
713
     *
714 94
     * @param string[] $builders array of builders that should be merged with the pre-defined ones in property.
715 94
     *
716 94
     * See {@see expressionBuilders} docs for details.
717
     */
718 94
    public function setExpressionBuilders(array $builders): void
719
    {
720
        $this->expressionBuilders = array_merge($this->expressionBuilders, $builders);
721
    }
722
723
    /**
724
     * @param string the separator between different fragments of a SQL statement.
725
     *
726
     * Defaults to an empty space. This is mainly used by {@see build()} when generating a SQL statement.
727
     */
728
    public function setSeparator(string $separator): void
729
    {
730
        $this->separator = $separator;
731
    }
732
733
    public function truncateTable(string $table): string
734
    {
735
        return $this->ddlBuilder->truncateTable($table);
736 140
    }
737
738 140
    public function update(string $table, array $columns, array|string $condition, array &$params = []): string
739
    {
740 140
        return $this->dmlBuilder->update($table, $columns, $condition, $params);
741
    }
742 140
743
    public function upsert(
744 140
        string $table,
745
        Query|array $insertColumns,
746 140
        bool|array $updateColumns,
747 140
        array &$params = []
748 70
    ): string {
749
        return $this->dmlBuilder->upsert($table, $insertColumns, $updateColumns, $params);
750 96
    }
751
752
    /**
753 140
     * Contains array of default condition classes. Extend this method, if you want to change default condition classes
754
     * for the query builder.
755
     *
756 140
     * @return array
757
     *
758
     * See {@see conditionClasses} docs for details.
759
     */
760
    protected function defaultConditionClasses(): array
761
    {
762
        return [
763
            'NOT' => Conditions\NotCondition::class,
764
            'AND' => Conditions\AndCondition::class,
765
            'OR' => Conditions\OrCondition::class,
766
            'BETWEEN' => Conditions\BetweenCondition::class,
767
            'NOT BETWEEN' => Conditions\BetweenCondition::class,
768
            'IN' => Conditions\InCondition::class,
769
            'NOT IN' => Conditions\InCondition::class,
770
            'LIKE' => Conditions\LikeCondition::class,
771
            'NOT LIKE' => Conditions\LikeCondition::class,
772
            'OR LIKE' => Conditions\LikeCondition::class,
773
            'OR NOT LIKE' => Conditions\LikeCondition::class,
774
            'EXISTS' => Conditions\ExistsCondition::class,
775
            'NOT EXISTS' => Conditions\ExistsCondition::class,
776
        ];
777
    }
778
779
    /**
780 47
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
781
     * expression builders for this query builder.
782 47
     *
783 47
     * @return array
784
     *
785 47
     * See {@see expressionBuilders} docs for details.
786
     */
787
    protected function defaultExpressionBuilders(): array
788
    {
789
        return [
790
            Query::class => QueryExpressionBuilder::class,
791
            PdoValue::class => PdoValueBuilder::class,
792
            Expression::class => ExpressionBuilder::class,
793
            Conditions\ConjunctionCondition::class => Conditions\Builder\ConjunctionConditionBuilder::class,
794
            Conditions\NotCondition::class => Conditions\Builder\NotConditionBuilder::class,
795
            Conditions\AndCondition::class => Conditions\Builder\ConjunctionConditionBuilder::class,
796
            Conditions\OrCondition::class => Conditions\Builder\ConjunctionConditionBuilder::class,
797
            Conditions\BetweenCondition::class => Conditions\Builder\BetweenConditionBuilder::class,
798
            Conditions\InCondition::class => Conditions\Builder\InConditionBuilder::class,
799
            Conditions\LikeCondition::class => Conditions\Builder\LikeConditionBuilder::class,
800
            Conditions\ExistsCondition::class => Conditions\Builder\ExistsConditionBuilder::class,
801
            Conditions\SimpleCondition::class => Conditions\Builder\SimpleConditionBuilder::class,
802
            Conditions\HashCondition::class => Conditions\Builder\HashConditionBuilder::class,
803
            Conditions\BetweenColumnsCondition::class => Conditions\Builder\BetweenColumnsConditionBuilder::class,
804
        ];
805
    }
806
807
    /**
808
     * Extracts table alias if there is one or returns false.
809
     *
810
     * @param string $table
811
     *
812
     * @psalm-return string[]|bool
813
     */
814
    protected function extractAlias(string $table): array|bool
815
    {
816
        if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) {
817
            return $matches;
818 63
        }
819
820 63
        return false;
821
    }
822 63
823 63
    /**
824 63
     * Checks to see if the given limit is effective.
825
     *
826 5
     * @param mixed $limit the given limit.
827
     *
828
     * @return bool whether the limit is effective.
829
     */
830 63
    protected function hasLimit(mixed $limit): bool
831
    {
832 63
        return ($limit instanceof ExpressionInterface) || ctype_digit((string) $limit);
833
    }
834
835
    /**
836
     * Checks to see if the given offset is effective.
837
     *
838
     * @param mixed $offset the given offset.
839
     *
840
     * @return bool whether the offset is effective.
841
     */
842
    protected function hasOffset(mixed $offset): bool
843 2
    {
844
        return ($offset instanceof ExpressionInterface) || (ctype_digit((string)$offset) && (string)$offset !== '0');
845 2
    }
846
847
    /**
848
     * Prepare select-subquery and field names for INSERT INTO ... SELECT SQL statement.
849
     *
850
     * @param Query $columns Object, which represents select query.
851
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
852
     * in the result with the additional parameters generated during the query building process.
853
     *
854
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
855 12
     *
856
     * @return array array of column names, values and params.
857 12
     */
858
    protected function prepareInsertSelectSubQuery(Query $columns, array $params = []): array
859
    {
860
        if (empty($columns->getSelect()) || in_array('*', $columns->getSelect(), true)) {
861
            throw new InvalidArgumentException('Expected select query object with enumerated (named) parameters');
862
        }
863
864
        [$values, $params] = $this->build($columns, $params);
865
866
        $names = [];
867
        $values = ' ' . $values;
868
869
        foreach ($columns->getSelect() as $title => $field) {
870
            if (is_string($title)) {
871 11
                $names[] = $this->quoter->quoteColumnName($title);
872
            } elseif (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $field, $matches)) {
873 11
                $names[] = $this->quoter->quoteColumnName($matches[2]);
874 8
            } else {
875
                $names[] = $this->quoter->quoteColumnName($field);
876
            }
877 11
        }
878 11
879
        return [$names, $values, $params];
880
    }
881 11
882 11
    public function prepareInsertValues(string $table, QueryInterface|array $columns, array $params = []): array
883 11
    {
884
        $tableSchema = $this->schema->getTableSchema($table);
885
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
886
        $names = [];
887
        $placeholders = [];
888
        $values = ' DEFAULT VALUES';
889
890
        if ($columns instanceof QueryInterface) {
891
            [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $params);
892
        } else {
893
            foreach ($columns as $name => $value) {
894 5
                $names[] = $this->quoter->quoteColumnName($name);
895
                $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
896 5
897 5
                if ($value instanceof ExpressionInterface) {
898
                    $placeholders[] = $this->buildExpression($value, $params);
899
                } elseif ($value instanceof QueryInterface) {
900
                    [$sql, $params] = $this->build($value, $params);
901
                    $placeholders[] = "($sql)";
902
                } else {
903
                    $placeholders[] = $this->bindParam($value, $params);
904
                }
905
            }
906
        }
907 3
908
        return [$names, $placeholders, $values, $params];
909 3
    }
910
911
    public function prepareUpdateSets(string $table, array $columns, array $params = []): array
912
    {
913
        $tableSchema = $this->schema->getTableSchema($table);
914
915
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
916
917
        $sets = [];
918
919
        foreach ($columns as $name => $value) {
920
            /** @psalm-var mixed $value */
921
            $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
922
            if ($value instanceof ExpressionInterface) {
923
                $placeholder = $this->buildExpression($value, $params);
924
            } else {
925
                $placeholder = $this->bindParam($value, $params);
926 2
            }
927
928 2
            $sets[] = $this->quoter->quoteColumnName($name) . '=' . $placeholder;
929 2
        }
930 2
931
        return [$sets, $params];
932
    }
933
934
    public function prepareUpsertColumns(
935
        string $table,
936
        QueryInterface|array $insertColumns,
937
        QueryInterface|bool|array $updateColumns,
938
        array &$constraints = []
939
    ): array {
940
        if ($insertColumns instanceof QueryInterface) {
941
            [$insertNames] = $this->prepareInsertSelectSubQuery($insertColumns);
942
        } else {
943
            $insertNames = array_map([$this->quoter, 'quoteColumnName'], array_keys($insertColumns));
944
        }
945
946
        $uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
947
        $uniqueNames = array_map([$this->quoter, 'quoteColumnName'], $uniqueNames);
948
949
        if ($updateColumns !== true) {
950
            return [$uniqueNames, $insertNames, null];
951
        }
952
953
        return [$uniqueNames, $insertNames, array_diff($insertNames, $uniqueNames)];
954
    }
955
956
    /**
957
     * Quotes table names passed.
958
     *
959
     * @param array $tables
960
     * @param array $params
961
     *
962
     * @psalm-param array<array-key, array|QueryInterface|string> $tables
963
     *
964
     * @throws Exception|InvalidConfigException|NotSupportedException
965
     *
966
     * @return array
967
     */
968
    private function quoteTableNames(array $tables, array &$params): array
969
    {
970
        foreach ($tables as $i => $table) {
971
            if ($table instanceof QueryInterface) {
972
                [$sql, $params] = $this->build($table, $params);
973
                $tables[$i] = "($sql) " . $this->quoter->quoteTableName((string) $i);
974
            } elseif (is_string($table) && is_string($i)) {
975
                if (!str_contains($table, '(')) {
976 1
                    $table = $this->quoter->quoteTableName($table);
977
                }
978 1
                $tables[$i] = "$table " . $this->quoter->quoteTableName($i);
979 1
            } elseif (is_string($table) && !str_contains($table, '(')) {
980 1
                $tableWithAlias = $this->extractAlias($table);
981 1
                if (is_array($tableWithAlias)) { // with alias
982
                    $tables[$i] = $this->quoter->quoteTableName($tableWithAlias[1]) . ' '
983
                        . $this->quoter->quoteTableName($tableWithAlias[2]);
984
                } else {
985
                    $tables[$i] = $this->quoter->quoteTableName($table);
986
                }
987
            }
988
        }
989
990
        return $tables;
991
    }
992
993
    /**
994
     * Returns all column names belonging to constraints enforcing uniqueness (`PRIMARY KEY`, `UNIQUE INDEX`, etc.)
995
     * for the named table removing constraints which did not cover the specified column list.
996
     *
997
     * The column list will be unique by column names.
998
     *
999
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
1000
     * @param string[] $columns source column list.
1001
     * @param Constraint[] $constraints this parameter optionally receives a matched constraint list. The constraints
1002
     * will be unique by their column names.
1003
     *
1004
     * @throws JsonException
1005
     *
1006
     * @return array column list.
1007 9
     */
1008
    private function getTableUniqueColumnNames(string $name, array $columns, array &$constraints = []): array
1009
    {
1010
        $constraints = [];
1011
        $primaryKey = $this->schema->getTablePrimaryKey($name);
1012
1013
        if ($primaryKey !== null) {
1014
            $constraints[] = $primaryKey;
1015
        }
1016 9
1017 9
        foreach ($this->schema->getTableIndexes($name) as $constraint) {
1018 9
            if ($constraint->isUnique()) {
1019 9
                $constraints[] = $constraint;
1020 9
            }
1021
        }
1022 9
1023 6
        $constraints = array_merge($constraints, $this->schema->getTableUniques($name));
1024
1025
        /** Remove duplicates */
1026 9
        $constraints = array_combine(
1027 6
            array_map(
1028
                static function ($constraint) {
1029
                    $columns = $constraint->getColumnNames();
1030 9
                    sort($columns, SORT_STRING);
1031
1032
                    return json_encode($columns, JSON_THROW_ON_ERROR);
1033
                },
1034
                $constraints
1035
            ),
1036
            $constraints
1037
        );
1038
1039
        $columnNames = [];
1040
        $quoter = $this->quoter;
1041
1042 7
        /** Remove all constraints which do not cover the specified column list */
1043
        $constraints = array_values(
1044 7
            array_filter(
1045 7
                $constraints,
1046
                static function ($constraint) use ($quoter, $columns, &$columnNames) {
1047
                    /** @psalm-suppress UndefinedClass, UndefinedMethod */
1048
                    $constraintColumnNames = array_map([$quoter, 'quoteColumnName'], $constraint->getColumnNames());
1049
                    $result = !array_diff($constraintColumnNames, $columns);
1050
1051
                    if ($result) {
1052
                        $columnNames = array_merge($columnNames, $constraintColumnNames);
1053
                    }
1054
1055
                    return $result;
1056
                }
1057
            )
1058
        );
1059
1060
        return array_unique($columnNames);
1061
    }
1062
}
1063