Passed
Branch dev (f56f10)
by Wilmer
04:41 queued 01:34
created

QueryBuilder::batchInsert()   C

Complexity

Conditions 13
Paths 85

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 13.0056

Importance

Changes 0
Metric Value
cc 13
eloc 31
nc 85
nop 4
dl 0
loc 50
ccs 30
cts 31
cp 0.9677
crap 13.0056
rs 6.6166
c 0
b 0
f 0

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 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
    public function __construct(
133
        private QuoterInterface $quoter,
134
        private SchemaInterface $schema
135
    ) {
136
        $this->expressionBuilders = $this->defaultExpressionBuilders();
137
        $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
        return $this->ddlBuilder->addColumn($table, $column, $type);
148
    }
149
150
    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
        return $this->ddlBuilder->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update);
175
    }
176
177
    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
237
        $union = $this->buildUnion($query->getUnion(), $params);
238
239
        if ($union !== '') {
240
            $sql = "($sql)$this->separator$union";
241
        }
242
243
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
244
245
        if ($with !== '') {
246
            $sql = "$with$this->separator$sql";
247
        }
248
249
        return [$sql, $params];
250
    }
251
252
    public function buildColumns(array|string $columns): string
253
    {
254
        if (!is_array($columns)) {
255
            if (str_contains($columns, '(')) {
256
                return $columns;
257
            }
258
259
            $rawColumns = $columns;
260
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
261
262
            if ($columns === false) {
263
                throw new InvalidArgumentException("$rawColumns is not valid columns.");
264
            }
265
        }
266
267
        foreach ($columns as $i => $column) {
268
            if ($column instanceof ExpressionInterface) {
269
                $columns[$i] = $this->buildExpression($column);
270
            } elseif (!str_contains($column, '(')) {
271
                $columns[$i] = $this->quoter->quoteColumnName($column);
272
            }
273
        }
274
275
        return implode(', ', $columns);
276
    }
277
278
    public function buildCondition(array|string|ExpressionInterface|null $condition, array &$params = []): string
279
    {
280
        if (is_array($condition)) {
281
            if (empty($condition)) {
282
                return '';
283
            }
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
    {
303
        if (empty($tables)) {
304
            return '';
305
        }
306
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
            } elseif (!str_contains($column, '(')) {
323
                $columns[$i] = $this->quoter->quoteColumnName($column);
324
            }
325
        }
326
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
        }
342
343
        foreach ($joins as $i => $join) {
344
            if (!is_array($join) || !isset($join[0], $join[1])) {
345
                throw new Exception(
346
                    'A join clause must be specified as an array of join type, join table, and optionally join '
347
                    . 'condition.'
348
                );
349
            }
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
        if ($this->hasLimit($limit)) {
377
            $sql = 'LIMIT ' . (string) $limit;
378
        }
379
380
        if ($this->hasOffset($offset)) {
381
            $sql .= ' OFFSET ' . (string) $offset;
382
        }
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
                $params = array_merge($params, $direction->getParams());
399
            } else {
400
                $orders[] = $this->quoter->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
401
            }
402
        }
403
404
        return 'ORDER BY ' . implode(', ', $orders);
405
    }
406
407
    public function buildOrderByAndLimit(
408
        string $sql,
409
        array $orderBy,
410
        Expression|int|null $limit,
411
        Expression|int|null $offset,
412
        array &$params = []
413
    ): string {
414
        $orderBy = $this->buildOrderBy($orderBy, $params);
415
        if ($orderBy !== '') {
416
            $sql .= $this->separator . $orderBy;
417
        }
418
        $limit = $this->buildLimit($limit, $offset);
419
        if ($limit !== '') {
420
            $sql .= $this->separator . $limit;
421
        }
422
423
        return $sql;
424
    }
425
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
        }
441
442
        foreach ($columns as $i => $column) {
443
            if ($column instanceof ExpressionInterface) {
444
                if (is_int($i)) {
445
                    $columns[$i] = $this->buildExpression($column, $params);
446
                } else {
447
                    $columns[$i] = $this->buildExpression($column, $params) . ' AS '
448
                        . $this->quoter->quoteColumnName($i);
449
                }
450
            } elseif ($column instanceof QueryInterface) {
451
                [$sql, $params] = $this->build($column, $params);
452
                $columns[$i] = "($sql) AS " . $this->quoter->quoteColumnName((string) $i);
453
            } elseif (is_string($i) && $i !== $column) {
454
                if (!str_contains($column, '(')) {
455
                    $column = $this->quoter->quoteColumnName($column);
456
                }
457
                $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
                }
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 QueryInterface) {
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
        array &$params = []
495
    ): string {
496
        $where = $this->buildCondition($condition, $params);
497
        return ($where === '') ? '' : ('WHERE ' . $where);
498
    }
499
500
    public function buildWithQueries(array $withs, array &$params): string
501
    {
502
        if (empty($withs)) {
503
            return '';
504
        }
505
506
        $recursive = false;
507
        $result = [];
508
509
        foreach ($withs as $with) {
510
            if ($with['recursive']) {
511
                $recursive = true;
512
            }
513
514
            $query = $with['query'];
515
            if ($query instanceof QueryInterface) {
516
                [$with['query'], $params] = $this->build($query, $params);
517
            }
518
519
            $result[] = $with['alias'] . ' AS (' . $with['query'] . ')';
520
        }
521
522
        return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . implode(', ', $result);
523
    }
524
525
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
526
    {
527
        return $this->ddlBuilder->checkIntegrity($schema, $table, $check);
528
    }
529
530
    public function createConditionFromArray(array $condition): ConditionInterface
531
    {
532
        /** operator format: operator, operand 1, operand 2, ... */
533
        if (isset($condition[0])) {
534
            $operator = strtoupper((string) array_shift($condition));
535
536
            $className = $this->conditionClasses[$operator] ?? SimpleCondition::class;
537
538
            /** @var ConditionInterface $className */
539
            return $className::fromArrayDefinition($operator, $condition);
540
        }
541
542
        /** hash format: 'column1' => 'value1', 'column2' => 'value2', ... */
543
        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
        return $this->ddlBuilder->dropForeignKey($name, $table);
594
    }
595
596
    public function dropIndex(string $name, string $table): string
597
    {
598
        return $this->ddlBuilder->dropIndex($name, $table);
599
    }
600
601
    public function dropPrimaryKey(string $name, string $table): string
602
    {
603
        return $this->ddlBuilder->dropPrimaryKey($name, $table);
604
    }
605
606
    public function dropTable(string $table): string
607
    {
608
        return $this->ddlBuilder->dropTable($table);
609
    }
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
        if (isset($this->typeMap[$type])) {
628
            return $this->typeMap[$type];
629
        }
630
631
        if (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
632
            if (isset($this->typeMap[$matches[1]])) {
633
                return preg_replace(
634
                    '/\(.+\)/',
635
                    '(' . $matches[2] . ')',
636
                    $this->typeMap[$matches[1]]
637
                ) . $matches[3];
638
            }
639
        } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
640
            if (isset($this->typeMap[$matches[1]])) {
641
                return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
642
            }
643
        }
644
645
        return $type;
646
    }
647
648
    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
    {
650
        $className = get_class($expression);
651
652
        if (!isset($this->expressionBuilders[$className])) {
653
            foreach (array_reverse($this->expressionBuilders) as $expressionClass => $builderClass) {
654
                if (is_subclass_of($expression, $expressionClass)) {
655
                    $this->expressionBuilders[$className] = $builderClass;
656
                    break;
657
                }
658
            }
659
660
            if (!isset($this->expressionBuilders[$className])) {
661
                throw new InvalidArgumentException(
662
                    'Expression of class ' . $className . ' can not be built in ' . static::class
663
                );
664
            }
665
        }
666
667
        if ($this->expressionBuilders[$className] === __CLASS__) {
668
            return $this;
669
        }
670
671
        if (!is_object($this->expressionBuilders[$className])) {
672
            $this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this);
673
        }
674
675
        return $this->expressionBuilders[$className];
676
    }
677
678
    public function getQuoter(): QuoterInterface
679
    {
680
        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
    /**
709
     * Setter for {@see conditionClasses} property.
710
     *
711
     * @param string[] $classes map of condition aliases to condition classes. For example:
712
     *
713
     * ```php
714
     * ['LIKE' => \Yiisoft\Db\Condition\LikeCondition::class]
715
     * ```
716
     *
717
     * See {@see conditionClasses} docs for details.
718
     */
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
    /**
737
     * @param string the separator between different fragments of a SQL statement.
738
     *
739
     * Defaults to an empty space. This is mainly used by {@see build()} when generating a SQL statement.
740
     */
741
    public function setSeparator(string $separator): void
742
    {
743
        $this->separator = $separator;
744
    }
745
746
    public function truncateTable(string $table): string
747
    {
748
        return $this->ddlBuilder->truncateTable($table);
749
    }
750
751
    public function update(string $table, array $columns, array|string $condition, array &$params = []): string
752
    {
753
        return $this->dmlBuilder->update($table, $columns, $condition, $params);
754
    }
755
756
    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
            'NOT BETWEEN' => Conditions\BetweenCondition::class,
781
            'IN' => Conditions\InCondition::class,
782
            'NOT IN' => Conditions\InCondition::class,
783
            'LIKE' => Conditions\LikeCondition::class,
784
            'NOT LIKE' => Conditions\LikeCondition::class,
785
            '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
    }
819
820
    /**
821
     * Extracts table alias if there is one or returns false.
822
     *
823
     * @param string $table
824
     *
825
     * @return array|bool
826
     */
827
    protected function extractAlias(string $table): array|bool
828
    {
829
        if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) {
830
            return $matches;
831
        }
832
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
    protected function hasLimit(mixed $limit): bool
844
    {
845
        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
    protected function hasOffset(mixed $offset): bool
856
    {
857
        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
    protected function prepareInsertSelectSubQuery(Query $columns, array $params = []): array
872
    {
873
        if (
874
            !is_array($columns->getSelect())
875
            || empty($columns->getSelect())
876
            || in_array('*', $columns->getSelect(), true)
877
        ) {
878
            throw new InvalidArgumentException('Expected select query object with enumerated (named) parameters');
879
        }
880
881
        [$values, $params] = $this->build($columns, $params);
882
883
        $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
        }
895
896
        return [$names, $values, $params];
897
    }
898
899
    public function prepareInsertValues(string $table, QueryInterface|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
        if ($columns instanceof QueryInterface) {
908
            [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $params);
909
        } 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 QueryInterface) {
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
    }
927
928
    public function prepareUpdateSets(string $table, array $columns, array $params = []): array
929
    {
930
        $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
        QueryInterface|array $insertColumns,
954
        QueryInterface|bool|array $updateColumns,
955
        array &$constraints = []
956
    ): array {
957
        if ($insertColumns instanceof QueryInterface) {
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
     * @param array $tables
977
     * @param array $params
978
     *
979
     * @psalm-param array<array-key, array|QueryInterface|string> $tables
980
     *
981
     * @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 QueryInterface) {
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
        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
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
1017
     * @param string[] $columns source column list.
1018
     * @param Constraint[] $constraints this parameter optionally receives a matched constraint list. The constraints
1019
     * will be unique by their column names.
1020
     *
1021
     * @throws JsonException
1022
     *
1023
     * @return array column list.
1024
     */
1025
    private function getTableUniqueColumnNames(string $name, array $columns, array &$constraints = []): array
1026
    {
1027
        if (!$this->schema instanceof ConstraintSchemaInterface) {
1028
            return [];
1029
        }
1030
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
        }
1043
1044
        $constraints = array_merge($constraints, $this->schema->getTableUniques($name));
1045
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
            array_filter(
1066
                $constraints,
1067
                static function ($constraint) use ($quoter, $columns, &$columnNames) {
1068
                    /** @psalm-suppress UndefinedClass, UndefinedMethod */
1069
                    $constraintColumnNames = array_map([$quoter, 'quoteColumnName'], $constraint->getColumnNames());
1070
                    $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
        return array_unique($columnNames);
1082
    }
1083
}
1084