Passed
Branch dev (f56f10)
by Wilmer
04:41 queued 01:34
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 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