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

QueryBuilder   F

Complexity

Total Complexity 204

Size/Duplication

Total Lines 1893
Duplicated Lines 0 %

Test Coverage

Coverage 89.51%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 461
dl 0
loc 1893
ccs 435
cts 486
cp 0.8951
rs 2
c 3
b 0
f 0
wmc 204

How to fix   Complexity   

Complex Class

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

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