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