Passed
Push — master ( dabdd0...83db8c )
by Wilmer
10:25
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\Connection\Connection;
10
use Yiisoft\Db\Constraint\Constraint;
11
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
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\Schema;
27
use Yiisoft\Strings\NumericHelper;
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 strpos;
56
use function strtoupper;
57
use function strtr;
58
use function trim;
59
60
/**
61
 * QueryBuilder builds a SELECT SQL statement based on the specification given as a {@see Query} object.
62
 *
63
 * SQL statements are created from {@see Query} objects using the {@see build()}-method.
64
 *
65
 * QueryBuilder is also used by {@see Command} to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE.
66
 *
67
 * For more details and usage information on QueryBuilder:
68
 * {@see [guide article on query builders](guide:db-query-builder)}.
69
 *
70
 * @property string[] $conditionClasses Map of condition aliases to condition classes. This property is write-only.
71
 *
72
 * For example:
73
 * ```php
74
 *     ['LIKE' => \Yiisoft\Db\Condition\LikeCondition::class]
75
 * ```
76
 * @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones in
77
 * {@see expressionBuilders} property. This property is write-only.
78
 */
79
class QueryBuilder
80
{
81
    /**
82
     * The prefix for automatically generated query binding parameters.
83
     */
84
    public const PARAM_PREFIX = ':qp';
85
86
    /**
87
     * @var array the abstract column types mapped to physical column types.
88
     * This is mainly used to support creating/modifying tables using DB-independent data type specifications.
89
     * Child classes should override this property to declare supported type mappings.
90
     *
91
     * @psalm-var array<string, string>
92
     */
93
    protected array $typeMap = [];
94
95
    /**
96
     * @var array map of condition aliases to condition classes. For example:
97
     *
98
     * ```php
99
     * return [
100
     *     'LIKE' => \Yiisoft\Db\Condition\LikeCondition::class,
101
     * ];
102
     * ```
103
     *
104
     * This property is used by {@see createConditionFromArray} method.
105
     * See default condition classes list in {@see defaultConditionClasses()} method.
106
     *
107
     * In case you want to add custom conditions support, use the {@see setConditionClasses()} method.
108
     *
109
     * @see setConditonClasses()
110
     * @see defaultConditionClasses()
111
     */
112
    protected array $conditionClasses = [];
113
114
    /**
115
     * @var ExpressionBuilderInterface[]|string[] maps expression class to expression builder class.
116
     * For example:
117
     *
118
     * ```php
119
     * [
120
     *    Expression::class => ExpressionBuilder::class
121
     * ]
122
     * ```
123
     * This property is mainly used by {@see buildExpression()} to build SQL expressions form expression objects.
124
     * See default values in {@see defaultExpressionBuilders()} method.
125
     *
126
     * @see setExpressionBuilders()
127
     * @see defaultExpressionBuilders()
128
     */
129
    protected array $expressionBuilders = [];
130
    protected string $separator = ' ';
131
    private Connection $db;
132
133 1935
    public function __construct(Connection $db)
134
    {
135 1935
        $this->db = $db;
136 1935
        $this->expressionBuilders = $this->defaultExpressionBuilders();
137 1935
        $this->conditionClasses = $this->defaultConditionClasses();
138 1935
    }
139
140
    /**
141
     * Contains array of default condition classes. Extend this method, if you want to change default condition classes
142
     * for the query builder.
143
     *
144
     * @return array
145
     *
146
     * See {@see conditionClasses} docs for details.
147
     */
148 1935
    protected function defaultConditionClasses(): array
149
    {
150
        return [
151 1935
            'NOT' => Conditions\NotCondition::class,
152
            'AND' => Conditions\AndCondition::class,
153
            'OR' => Conditions\OrCondition::class,
154
            'BETWEEN' => Conditions\BetweenCondition::class,
155
            'NOT BETWEEN' => Conditions\BetweenCondition::class,
156
            'IN' => Conditions\InCondition::class,
157
            'NOT IN' => Conditions\InCondition::class,
158
            'LIKE' => Conditions\LikeCondition::class,
159
            'NOT LIKE' => Conditions\LikeCondition::class,
160
            'OR LIKE' => Conditions\LikeCondition::class,
161
            'OR NOT LIKE' => Conditions\LikeCondition::class,
162
            'EXISTS' => Conditions\ExistsCondition::class,
163
            'NOT EXISTS' => Conditions\ExistsCondition::class,
164
        ];
165
    }
166
167
    /**
168
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
169
     * expression builders for this query builder.
170
     *
171
     * @return array
172
     *
173
     * See {@see expressionBuilders} docs for details.
174
     */
175 1935
    protected function defaultExpressionBuilders(): array
176
    {
177
        return [
178 1935
            Query::class => QueryExpressionBuilder::class,
179
            PdoValue::class => PdoValueBuilder::class,
180
            Expression::class => ExpressionBuilder::class,
181
            Conditions\ConjunctionCondition::class => Conditions\ConjunctionConditionBuilder::class,
182
            Conditions\NotCondition::class => Conditions\NotConditionBuilder::class,
183
            Conditions\AndCondition::class => Conditions\ConjunctionConditionBuilder::class,
184
            Conditions\OrCondition::class => Conditions\ConjunctionConditionBuilder::class,
185
            Conditions\BetweenCondition::class => Conditions\BetweenConditionBuilder::class,
186
            Conditions\InCondition::class => Conditions\InConditionBuilder::class,
187
            Conditions\LikeCondition::class => Conditions\LikeConditionBuilder::class,
188
            Conditions\ExistsCondition::class => Conditions\ExistsConditionBuilder::class,
189
            Conditions\SimpleCondition::class => Conditions\SimpleConditionBuilder::class,
190
            Conditions\HashCondition::class => Conditions\HashConditionBuilder::class,
191
            Conditions\BetweenColumnsCondition::class => Conditions\BetweenColumnsConditionBuilder::class,
192
        ];
193
    }
194
195
    /**
196
     * Setter for {@see expressionBuilders property.
197
     *
198
     * @param string[] $builders array of builders that should be merged with the pre-defined ones in property.
199
     *
200
     * See {@see expressionBuilders} docs for details.
201
     */
202
    public function setExpressionBuilders(array $builders): void
203
    {
204
        $this->expressionBuilders = array_merge($this->expressionBuilders, $builders);
205
    }
206
207
    /**
208
     * Setter for {@see conditionClasses} property.
209
     *
210
     * @param string[] $classes map of condition aliases to condition classes. For example:
211
     *
212
     * ```php
213
     * ['LIKE' => \Yiisoft\Db\Condition\LikeCondition::class]
214
     * ```
215
     *
216
     * See {@see conditionClasses} docs for details.
217
     */
218
    public function setConditionClasses(array $classes): void
219
    {
220
        $this->conditionClasses = array_merge($this->conditionClasses, $classes);
221
    }
222
223
    /**
224
     * Generates a SELECT SQL statement from a {@see Query} object.
225
     *
226
     * @param Query $query the {@see Query} object from which the SQL statement will be generated.
227
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
228
     * in the result with the additional parameters generated during the query building process.
229
     *
230
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
231
     *
232
     * @return array the generated SQL statement (the first array element) and the corresponding parameters to be bound
233
     * to the SQL statement (the second array element). The parameters returned include those provided in `$params`.
234
     *
235
     * @psalm-return array{0: string, 1: array}
236
     */
237 1220
    public function build(Query $query, array $params = []): array
238
    {
239 1220
        $query = $query->prepare($this);
240
241 1220
        $params = empty($params) ? $query->getParams() : array_merge($params, $query->getParams());
242
243
        $clauses = [
244 1220
            $this->buildSelect($query->getSelect(), $params, $query->getDistinct(), $query->getSelectOption()),
245 1220
            $this->buildFrom($query->getFrom(), $params),
246 1220
            $this->buildJoin($query->getJoin(), $params),
247 1220
            $this->buildWhere($query->getWhere(), $params),
248 1220
            $this->buildGroupBy($query->getGroupBy(), $params),
249 1220
            $this->buildHaving($query->getHaving(), $params),
250
        ];
251
252 1220
        $sql = implode($this->separator, array_filter($clauses));
253
254 1220
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
255
256 1220
        if (!empty($query->getOrderBy())) {
257 180
            foreach ($query->getOrderBy() as $expression) {
258 180
                if ($expression instanceof ExpressionInterface) {
259 4
                    $this->buildExpression($expression, $params);
260
                }
261
            }
262
        }
263
264 1220
        if (!empty($query->getGroupBy())) {
265 12
            foreach ($query->getGroupBy() as $expression) {
266 12
                if ($expression instanceof ExpressionInterface) {
267 4
                    $this->buildExpression($expression, $params);
268
                }
269
            }
270
        }
271
272 1220
        $union = $this->buildUnion($query->getUnion(), $params);
273
274 1220
        if ($union !== '') {
275 9
            $sql = "($sql){$this->separator}$union";
276
        }
277
278 1220
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
279
280 1220
        if ($with !== '') {
281 8
            $sql = "$with{$this->separator}$sql";
282
        }
283
284 1220
        return [$sql, $params];
285
    }
286
287
    /**
288
     * Builds given $expression.
289
     *
290
     * @param ExpressionInterface $expression the expression to be built
291
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
292
     * in the result with the additional parameters generated during the expression building process.
293
     *
294
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException when $expression building
295
     * is not supported by this QueryBuilder.
296
     *
297
     * @return string the SQL statement that will not be neither quoted nor encoded before passing to DBMS.
298
     *
299
     * @see ExpressionInterface
300
     * @see ExpressionBuilderInterface
301
     * @see expressionBuilders
302
     */
303 1369
    public function buildExpression(ExpressionInterface $expression, array &$params = []): string
304
    {
305 1369
        $builder = $this->getExpressionBuilder($expression);
306
307 1369
        return (string) $builder->build($expression, $params);
308
    }
309
310
    /**
311
     * Gets object of {@see ExpressionBuilderInterface} that is suitable for $expression.
312
     *
313
     * Uses {@see expressionBuilders} array to find a suitable builder class.
314
     *
315
     * @param ExpressionInterface $expression
316
     *
317
     * @throws InvalidArgumentException when $expression building is not supported by this QueryBuilder.
318
     *
319
     * @return ExpressionBuilderInterface|QueryBuilder|string
320
     *
321
     * @see expressionBuilders
322
     */
323 1369
    public function getExpressionBuilder(ExpressionInterface $expression)
324
    {
325 1369
        $className = get_class($expression);
326
327 1369
        if (!isset($this->expressionBuilders[$className])) {
328
            foreach (array_reverse($this->expressionBuilders) as $expressionClass => $builderClass) {
329
                if (is_subclass_of($expression, $expressionClass)) {
330
                    $this->expressionBuilders[$className] = $builderClass;
331
                    break;
332
                }
333
            }
334
335
            if (!isset($this->expressionBuilders[$className])) {
336
                throw new InvalidArgumentException(
337
                    'Expression of class ' . $className . ' can not be built in ' . static::class
338
                );
339
            }
340
        }
341
342 1369
        if ($this->expressionBuilders[$className] === __CLASS__) {
343
            return $this;
344
        }
345
346 1369
        if (!is_object($this->expressionBuilders[$className])) {
0 ignored issues
show
introduced by
The condition is_object($this->expressionBuilders[$className]) is always false.
Loading history...
347 1369
            $this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this);
348
        }
349
350 1369
        return $this->expressionBuilders[$className];
351
    }
352
353
    /**
354
     * Creates an INSERT SQL statement.
355
     *
356
     * For example,.
357
     *
358
     * ```php
359
     * $sql = $queryBuilder->insert('user', [
360
     *     'name' => 'Sam',
361
     *     'age' => 30,
362
     * ], $params);
363
     * ```
364
     *
365
     * The method will properly escape the table and column names.
366
     *
367
     * @param string $table the table that new rows will be inserted into.
368
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
369
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of {@see Query}.
370
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
371
     * DB command later.
372
     *
373
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
374
     *
375
     * @return string the INSERT SQL.
376
     */
377 199
    public function insert(string $table, $columns, array &$params = []): string
378
    {
379 199
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
380
381 187
        return 'INSERT INTO ' . $this->db->quoteTableName($table)
382 187
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
383 187
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
384
    }
385
386
    /**
387
     * Prepares a `VALUES` part for an `INSERT` SQL statement.
388
     *
389
     * @param string $table the table that new rows will be inserted into.
390
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
391
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement.
392
     * @param array $params the binding parameters that will be generated by this method.
393
     * They should be bound to the DB command later.
394
     *
395
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
396
     *
397
     * @return array array of column names, placeholders, values and params.
398
     */
399 284
    protected function prepareInsertValues(string $table, $columns, array $params = []): array
400
    {
401 284
        $schema = $this->db->getSchema();
402 284
        $tableSchema = $schema->getTableSchema($table);
403 284
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
404 284
        $names = [];
405 284
        $placeholders = [];
406 284
        $values = ' DEFAULT VALUES';
407
408 284
        if ($columns instanceof Query) {
409 69
            [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
410
        } else {
411 224
            foreach ($columns as $name => $value) {
412 219
                $names[] = $schema->quoteColumnName($name);
413 219
                $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
414
415 219
                if ($value instanceof ExpressionInterface) {
416 44
                    $placeholders[] = $this->buildExpression($value, $params);
417 213
                } elseif ($value instanceof Query) {
418
                    [$sql, $params] = $this->build($value, $params);
419
                    $placeholders[] = "($sql)";
420
                } else {
421 213
                    $placeholders[] = $this->bindParam($value, $params);
422
                }
423
            }
424
        }
425
426 269
        return [$names, $placeholders, $values, $params];
427
    }
428
429
    /**
430
     * Prepare select-subquery and field names for INSERT INTO ... SELECT SQL statement.
431
     *
432
     * @param Query $columns Object, which represents select query.
433
     * @param Schema $schema Schema object to quote column name.
434
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
435
     * in the result with the additional parameters generated during the query building process.
436
     *
437
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
438
     *
439
     * @return array array of column names, values and params.
440
     */
441 69
    protected function prepareInsertSelectSubQuery(Query $columns, Schema $schema, array $params = []): array
442
    {
443
        if (
444 69
            !is_array($columns->getSelect())
0 ignored issues
show
introduced by
The condition is_array($columns->getSelect()) is always true.
Loading history...
445 69
            || empty($columns->getSelect())
446 69
            || in_array('*', $columns->getSelect(), true)
447
        ) {
448 15
            throw new InvalidArgumentException('Expected select query object with enumerated (named) parameters');
449
        }
450
451 54
        [$values, $params] = $this->build($columns, $params);
452
453 54
        $names = [];
454 54
        $values = ' ' . $values;
455
456 54
        foreach ($columns->getSelect() as $title => $field) {
457 54
            if (is_string($title)) {
458 54
                $names[] = $schema->quoteColumnName($title);
459
            } elseif (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $field, $matches)) {
460
                $names[] = $schema->quoteColumnName($matches[2]);
461
            } else {
462
                $names[] = $schema->quoteColumnName($field);
463
            }
464
        }
465
466 54
        return [$names, $values, $params];
467
    }
468
469
    /**
470
     * Generates a batch INSERT SQL statement.
471
     *
472
     * For example,
473
     *
474
     * ```php
475
     * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
476
     *     ['Tom', 30],
477
     *     ['Jane', 20],
478
     *     ['Linda', 25],
479
     * ]);
480
     * ```
481
     *
482
     * Note that the values in each row must match the corresponding column names.
483
     *
484
     * The method will properly escape the column names, and quote the values to be inserted.
485
     *
486
     * @param string $table the table that new rows will be inserted into.
487
     * @param array $columns the column names.
488
     * @param array|Generator $rows the rows to be batch inserted into the table.
489
     * @param array $params the binding parameters. This parameter exists.
490
     *
491
     * @throws Exception|InvalidArgumentException
492
     *
493
     * @return string the batch INSERT SQL statement.
494
     */
495 40
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
496
    {
497 40
        if (empty($rows)) {
498 4
            return '';
499
        }
500
501 38
        $schema = $this->db->getSchema();
502
503
504 38
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
505 38
            $columnSchemas = $tableSchema->getColumns();
506
        } else {
507
            $columnSchemas = [];
508
        }
509
510 38
        $values = [];
511
512 38
        foreach ($rows as $row) {
513 35
            $vs = [];
514 35
            foreach ($row as $i => $value) {
515 35
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
516 26
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
517
                }
518 35
                if (is_string($value)) {
519 23
                    $value = $schema->quoteValue($value);
520 21
                } elseif (is_float($value)) {
521
                    /* ensure type cast always has . as decimal separator in all locales */
522 2
                    $value = NumericHelper::normalize((string) $value);
523 21
                } elseif ($value === false) {
524 7
                    $value = 0;
525 21
                } elseif ($value === null) {
526 12
                    $value = 'NULL';
527 13
                } elseif ($value instanceof ExpressionInterface) {
528 9
                    $value = $this->buildExpression($value, $params);
529
                }
530 35
                $vs[] = $value;
531
            }
532 35
            $values[] = '(' . implode(', ', $vs) . ')';
533
        }
534
535 38
        if (empty($values)) {
536 3
            return '';
537
        }
538
539 35
        foreach ($columns as $i => $name) {
540 32
            $columns[$i] = $schema->quoteColumnName($name);
541
        }
542
543 35
        return 'INSERT INTO ' . $schema->quoteTableName($table)
544 35
            . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
545
    }
546
547
    /**
548
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
549
     * constraints), or update them if they do.
550
     *
551
     * For example,
552
     *
553
     * ```php
554
     * $sql = $queryBuilder->upsert('pages', [
555
     *     'name' => 'Front page',
556
     *     'url' => 'http://example.com/', // url is unique
557
     *     'visits' => 0,
558
     * ], [
559
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
560
     * ], $params);
561
     * ```
562
     *
563
     * The method will properly escape the table and column names.
564
     *
565
     * @param string $table the table that new rows will be inserted into/updated in.
566
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
567
     * of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
568
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
569
     * If `true` is passed, the column data will be updated to match the insert column data.
570
     * If `false` is passed, no update will be performed if the column data already exists.
571
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the DB
572
     * command later.
573
     *
574
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
575
     *
576
     * @return string the resulting SQL.
577
     */
578
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
579
    {
580
        throw new NotSupportedException($this->db->getDriverName() . ' does not support upsert statements.');
581
    }
582
583
    /**
584
     * @param string $table
585
     * @param array|Query $insertColumns
586
     * @param array|bool $updateColumns
587
     * @param Constraint[] $constraints this parameter recieves a matched constraint list.
588
     * The constraints will be unique by their column names.
589
     *
590
     * @throws Exception|JsonException
591
     *
592
     * @return array
593
     */
594 89
    protected function prepareUpsertColumns(string $table, $insertColumns, $updateColumns, array &$constraints = []): array
595
    {
596 89
        if ($insertColumns instanceof Query) {
597 40
            [$insertNames] = $this->prepareInsertSelectSubQuery($insertColumns, $this->db->getSchema());
598
        } else {
599
            /** @psalm-suppress UndefinedMethod */
600 49
            $insertNames = array_map([$this->db, 'quoteColumnName'], array_keys($insertColumns));
601
        }
602
603 89
        $uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
604
605
        /** @psalm-suppress UndefinedMethod */
606 89
        $uniqueNames = array_map([$this->db, 'quoteColumnName'], $uniqueNames);
607
608 89
        if ($updateColumns !== true) {
609 64
            return [$uniqueNames, $insertNames, null];
610
        }
611
612 25
        return [$uniqueNames, $insertNames, array_diff($insertNames, $uniqueNames)];
613
    }
614
615
    /**
616
     * Returns all column names belonging to constraints enforcing uniqueness (`PRIMARY KEY`, `UNIQUE INDEX`, etc.)
617
     * for the named table removing constraints which did not cover the specified column list.
618
     *
619
     * The column list will be unique by column names.
620
     *
621
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
622
     * @param string[] $columns source column list.
623
     * @param Constraint[] $constraints this parameter optionally recieves a matched constraint list. The constraints
624
     * will be unique by their column names.
625
     *
626
     * @throws JsonException
627
     *
628
     * @return array column list.
629
     */
630 89
    private function getTableUniqueColumnNames(string $name, array $columns, array &$constraints = []): array
631
    {
632 89
        $schema = $this->db->getSchema();
633
634 89
        if (!$schema instanceof ConstraintFinderInterface) {
635
            return [];
636
        }
637
638 89
        $constraints = [];
639 89
        $primaryKey = $schema->getTablePrimaryKey($name);
640
641 89
        if ($primaryKey !== null) {
642 88
            $constraints[] = $primaryKey;
643
        }
644
645 89
        foreach ($schema->getTableIndexes($name) as $constraint) {
646 88
            if ($constraint->isUnique()) {
647 88
                $constraints[] = $constraint;
648
            }
649
        }
650
651 89
        $constraints = array_merge($constraints, $schema->getTableUniques($name));
652
653
        /** Remove duplicates */
654 89
        $constraints = array_combine(
655 89
            array_map(
656 89
                static function ($constraint) {
657 89
                    $columns = $constraint->getColumnNames();
658 89
                    sort($columns, SORT_STRING);
659
660 89
                    return json_encode($columns, JSON_THROW_ON_ERROR);
661
                },
662 89
                $constraints
663
            ),
664 89
            $constraints
665
        );
666
667 89
        $columnNames = [];
668
669
        /** Remove all constraints which do not cover the specified column list */
670 89
        $constraints = array_values(
671 89
            array_filter(
672 89
                $constraints,
673 89
                static function ($constraint) use ($schema, $columns, &$columnNames) {
674
                    /** @psalm-suppress UndefinedClass, UndefinedMethod */
675 89
                    $constraintColumnNames = array_map([$schema, 'quoteColumnName'], $constraint->getColumnNames());
676 89
                    $result = !array_diff($constraintColumnNames, $columns);
677
678 89
                    if ($result) {
679 74
                        $columnNames = array_merge($columnNames, $constraintColumnNames);
680
                    }
681
682 89
                    return $result;
683 89
                }
684
            )
685
        );
686
687 89
        return array_unique($columnNames);
688
    }
689
690
    /**
691
     * Creates an UPDATE SQL statement.
692
     *
693
     * For example,
694
     *
695
     * ```php
696
     * $params = [];
697
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
698
     * ```
699
     *
700
     * The method will properly escape the table and column names.
701
     *
702
     * @param string $table the table to be updated.
703
     * @param array $columns the column data (name => value) to be updated.
704
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
705
     * {@see Query::where()} on how to specify condition.
706
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
707
     * DB command later.
708
     *
709
     * @psalm-param array<string, ExpressionInterface|string> $columns
710
     *
711
     * @throws Exception|InvalidArgumentException
712
     *
713
     * @return string the UPDATE SQL.
714
     */
715 94
    public function update(string $table, array $columns, $condition, array &$params = []): string
716
    {
717
        /**
718
         * @psalm-var array<array-key, mixed> $lines
719
         * @psalm-var array<array-key, mixed> $params
720
         */
721 94
        [$lines, $params] = $this->prepareUpdateSets($table, $columns, $params);
722 94
        $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
723 94
        $where = $this->buildWhere($condition, $params);
724
725 94
        return ($where === '') ? $sql : ($sql . ' ' . $where);
726
    }
727
728
    /**
729
     * Prepares a `SET` parts for an `UPDATE` SQL statement.
730
     *
731
     * @param string $table the table to be updated.
732
     * @param array $columns the column data (name => value) to be updated.
733
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
734
     * DB command later.
735
     *
736
     * @psalm-param array<string, ExpressionInterface|string> $columns
737
     *
738
     * @throws Exception|InvalidArgumentException
739
     *
740
     * @return array `SET` parts for an `UPDATE` SQL statement (the first array element) and params (the second array
741
     * element).
742
     */
743 139
    protected function prepareUpdateSets(string $table, array $columns, array $params = []): array
744
    {
745 139
        $tableSchema = $this->db->getTableSchema($table);
746
747 139
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
748
749 139
        $sets = [];
750
751 139
        foreach ($columns as $name => $value) {
752
            /** @psalm-var mixed $value */
753 139
            $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
754 139
            if ($value instanceof ExpressionInterface) {
755 69
                $placeholder = $this->buildExpression($value, $params);
756
            } else {
757 96
                $placeholder = $this->bindParam($value, $params);
758
            }
759
760 139
            $sets[] = $this->db->quoteColumnName($name) . '=' . $placeholder;
761
        }
762
763 139
        return [$sets, $params];
764
    }
765
766
    /**
767
     * Creates a DELETE SQL statement.
768
     *
769
     * For example,
770
     *
771
     * ```php
772
     * $sql = $queryBuilder->delete('user', 'status = 0');
773
     * ```
774
     *
775
     * The method will properly escape the table and column names.
776
     *
777
     * @param string $table the table where the data will be deleted from.
778
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
779
     * {@see Query::where()} on how to specify condition.
780
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
781
     * DB command later.
782
     *
783
     * @throws Exception|InvalidArgumentException
784
     *
785
     * @return string the DELETE SQL.
786
     */
787 41
    public function delete(string $table, $condition, array &$params): string
788
    {
789 41
        $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
790 41
        $where = $this->buildWhere($condition, $params);
791
792 41
        return ($where === '') ? $sql : ($sql . ' ' . $where);
793
    }
794
795
    /**
796
     * Builds a SQL statement for creating a new DB table.
797
     *
798
     * The columns in the new  table should be specified as name-definition pairs (e.g. 'name' => 'string'), where name
799
     * stands for a column name which will be properly quoted by the method, and definition stands for the column type
800
     * which can contain an abstract DB type.
801
     *
802
     * The {@see getColumnType()} method will be invoked to convert any abstract type into a physical one.
803
     *
804
     * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly inserted
805
     * into the generated SQL.
806
     *
807
     * For example,
808
     *
809
     * ```php
810
     * $sql = $queryBuilder->createTable('user', [
811
     *  'id' => 'pk',
812
     *  'name' => 'string',
813
     *  'age' => 'integer',
814
     * ]);
815
     * ```
816
     *
817
     * @param string $table the name of the table to be created. The name will be properly quoted by the method.
818
     * @param array $columns the columns (name => definition) in the new table.
819
     * @param string|null $options additional SQL fragment that will be appended to the generated SQL.
820
     *
821
     * @psalm-param array<array-key, ColumnSchemaBuilder|string> $columns
822
     *
823
     * @return string the SQL statement for creating a new DB table.
824
     */
825 61
    public function createTable(string $table, array $columns, ?string $options = null): string
826
    {
827 61
        $cols = [];
828
829 61
        foreach ($columns as $name => $type) {
830 61
            if (is_string($name)) {
831 61
                $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
832
            } else {
833 5
                $cols[] = "\t" . $type;
834
            }
835
        }
836
837 61
        $sql = 'CREATE TABLE ' . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)";
838
839 61
        return ($options === null) ? $sql : ($sql . ' ' . $options);
840
    }
841
842
    /**
843
     * Builds a SQL statement for renaming a DB table.
844
     *
845
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
846
     * @param string $newName the new table name. The name will be properly quoted by the method.
847
     *
848
     * @return string the SQL statement for renaming a DB table.
849
     */
850 2
    public function renameTable(string $oldName, string $newName): string
851
    {
852 2
        return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName);
853
    }
854
855
    /**
856
     * Builds a SQL statement for dropping a DB table.
857
     *
858
     * @param string $table the table to be dropped. The name will be properly quoted by the method.
859
     *
860
     * @return string the SQL statement for dropping a DB table.
861
     */
862 10
    public function dropTable(string $table): string
863
    {
864 10
        return 'DROP TABLE ' . $this->db->quoteTableName($table);
865
    }
866
867
    /**
868
     * Builds a SQL statement for adding a primary key constraint to an existing table.
869
     *
870
     * @param string $name the name of the primary key constraint.
871
     * @param string $table the table that the primary key constraint will be added to.
872
     * @param array|string $columns comma separated string or array of columns that the primary key will consist of.
873
     *
874
     * @psalm-param array<array-key, string>|string $columns
875
     *
876
     * @return string the SQL statement for adding a primary key constraint to an existing table.
877
     */
878 11
    public function addPrimaryKey(string $name, string $table, $columns): string
879
    {
880 11
        if (is_string($columns)) {
881 8
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
882
        }
883
884 11
        foreach ($columns as $i => $col) {
885 11
            $columns[$i] = $this->db->quoteColumnName($col);
886
        }
887
888 11
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
889 11
            . $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
890 11
            . implode(', ', $columns) . ')';
891
    }
892
893
    /**
894
     * Builds a SQL statement for removing a primary key constraint to an existing table.
895
     *
896
     * @param string $name the name of the primary key constraint to be removed.
897
     * @param string $table the table that the primary key constraint will be removed from.
898
     *
899
     * @return string the SQL statement for removing a primary key constraint from an existing table.
900
     */
901 5
    public function dropPrimaryKey(string $name, string $table): string
902
    {
903 5
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
904 5
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
905
    }
906
907
    /**
908
     * Builds a SQL statement for truncating a DB table.
909
     *
910
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
911
     *
912
     * @return string the SQL statement for truncating a DB table.
913
     */
914 3
    public function truncateTable(string $table): string
915
    {
916 3
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table);
917
    }
918
919
    /**
920
     * Builds a SQL statement for adding a new DB column.
921
     *
922
     * @param string $table the table that the new column will be added to. The table name will be properly quoted by
923
     * the method.
924
     * @param string $column the name of the new column. The name will be properly quoted by the method.
925
     * @param string $type the column type. The {@see getColumnType()} method will be invoked to convert abstract column
926
     * type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
927
     * generated SQL.
928
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
929
     * 'varchar(255) not null'.
930
     *
931
     * @return string the SQL statement for adding a new column.
932
     */
933 2
    public function addColumn(string $table, string $column, string $type): string
934
    {
935 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
936 2
            . ' ADD ' . $this->db->quoteColumnName($column) . ' '
937 2
            . $this->getColumnType($type);
938
    }
939
940
    /**
941
     * Builds a SQL statement for dropping a DB column.
942
     *
943
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
944
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
945
     *
946
     * @return string the SQL statement for dropping a DB column.
947
     */
948
    public function dropColumn(string $table, string $column): string
949
    {
950
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
951
            . ' DROP COLUMN ' . $this->db->quoteColumnName($column);
952
    }
953
954
    /**
955
     * Builds a SQL statement for renaming a column.
956
     *
957
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
958
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
959
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
960
     *
961
     * @return string the SQL statement for renaming a DB column.
962
     */
963
    public function renameColumn(string $table, string $oldName, string $newName): string
964
    {
965
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
966
            . ' RENAME COLUMN ' . $this->db->quoteColumnName($oldName)
967
            . ' TO ' . $this->db->quoteColumnName($newName);
968
    }
969
970
    /**
971
     * Builds a SQL statement for changing the definition of a column.
972
     *
973
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
974
     * method.
975
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
976
     * @param string $type the new column type. The {@see getColumnType()} method will be invoked to convert abstract
977
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
978
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
979
     * will become 'varchar(255) not null'.
980
     *
981
     * @return string the SQL statement for changing the definition of a column.
982
     */
983 1
    public function alterColumn(string $table, string $column, string $type): string
984
    {
985 1
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
986 1
            . $this->db->quoteColumnName($column) . ' '
987 1
            . $this->db->quoteColumnName($column) . ' '
988 1
            . $this->getColumnType($type);
989
    }
990
991
    /**
992
     * Builds a SQL statement for adding a foreign key constraint to an existing table. The method will properly quote
993
     * the table and column names.
994
     *
995
     * @param string $name the name of the foreign key constraint.
996
     * @param string $table the table that the foreign key constraint will be added to.
997
     * @param array|string $columns the name of the column to that the constraint will be added on. If there are
998
     * multiple columns, separate them with commas or use an array to represent them.
999
     * @param string $refTable the table that the foreign key references to.
1000
     * @param array|string $refColumns the name of the column that the foreign key references to. If there are multiple
1001
     * columns, separate them with commas or use an array to represent them.
1002
     * @param string|null $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
1003
     * SET DEFAULT, SET NULL.
1004
     * @param string|null $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
1005
     * SET DEFAULT, SET NULL.
1006
     *
1007
     * @psalm-param array<array-key, string>|string $columns
1008
     * @psalm-param array<array-key, string>|string $refColumns
1009
     *
1010
     * @throws Exception|InvalidArgumentException
1011
     *
1012
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
1013
     */
1014 9
    public function addForeignKey(
1015
        string $name,
1016
        string $table,
1017
        $columns,
1018
        string $refTable,
1019
        $refColumns,
1020
        ?string $delete = null,
1021
        ?string $update = null
1022
    ): string {
1023 9
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
1024 9
            . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
1025 9
            . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
1026 9
            . ' REFERENCES ' . $this->db->quoteTableName($refTable)
1027 9
            . ' (' . $this->buildColumns($refColumns) . ')';
1028
1029 9
        if ($delete !== null) {
1030 6
            $sql .= ' ON DELETE ' . $delete;
1031
        }
1032
1033 9
        if ($update !== null) {
1034 6
            $sql .= ' ON UPDATE ' . $update;
1035
        }
1036
1037 9
        return $sql;
1038
    }
1039
1040
    /**
1041
     * Builds a SQL statement for dropping a foreign key constraint.
1042
     *
1043
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by
1044
     * the method.
1045
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
1046
     *
1047
     * @return string the SQL statement for dropping a foreign key constraint.
1048
     */
1049 7
    public function dropForeignKey(string $name, string $table): string
1050
    {
1051 7
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1052 7
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1053
    }
1054
1055
    /**
1056
     * Builds a SQL statement for creating a new index.
1057
     *
1058
     * @param string $name the name of the index. The name will be properly quoted by the method.
1059
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
1060
     * the method.
1061
     * @param array|string $columns the column(s) that should be included in the index. If there are multiple columns,
1062
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
1063
     * method, unless a parenthesis is found in the name.
1064
     * @param bool $unique whether to add UNIQUE constraint on the created index.
1065
     *
1066
     * @psalm-param array<array-key, ExpressionInterface|string>|string $columns
1067
     *
1068
     * @throws Exception|InvalidArgumentException
1069
     *
1070
     * @return string the SQL statement for creating a new index.
1071
     */
1072 11
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
1073
    {
1074 11
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
1075 11
            . $this->db->quoteTableName($name) . ' ON '
1076 11
            . $this->db->quoteTableName($table)
1077 11
            . ' (' . $this->buildColumns($columns) . ')';
1078
    }
1079
1080
    /**
1081
     * Builds a SQL statement for dropping an index.
1082
     *
1083
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
1084
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
1085
     *
1086
     * @return string the SQL statement for dropping an index.
1087
     */
1088 6
    public function dropIndex(string $name, string $table): string
1089
    {
1090 6
        return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
1091
    }
1092
1093
    /**
1094
     * Creates a SQL command for adding an unique constraint to an existing table.
1095
     *
1096
     * @param string $name the name of the unique constraint. The name will be properly quoted by the method.
1097
     * @param string $table the table that the unique constraint will be added to. The name will be properly quoted by
1098
     * the method.
1099
     * @param array|string $columns the name of the column to that the constraint will be added on. If there are
1100
     * multiple columns, separate them with commas. The name will be properly quoted by the method.
1101
     *
1102
     * @psalm-param array<array-key, string>|string $columns
1103
     *
1104
     * @return string the SQL statement for adding an unique constraint to an existing table.
1105
     */
1106 12
    public function addUnique(string $name, string $table, $columns): string
1107
    {
1108 12
        if (is_string($columns)) {
1109 8
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1110
        }
1111
1112 12
        foreach ($columns as $i => $col) {
1113 12
            $columns[$i] = $this->db->quoteColumnName($col);
1114
        }
1115
1116 12
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1117 12
            . $this->db->quoteColumnName($name) . ' UNIQUE ('
1118 12
            . implode(', ', $columns) . ')';
1119
    }
1120
1121
    /**
1122
     * Creates a SQL command for dropping an unique constraint.
1123
     *
1124
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
1125
     * method.
1126
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by the
1127
     * method.
1128
     *
1129
     * @return string the SQL statement for dropping an unique constraint.
1130
     */
1131 6
    public function dropUnique(string $name, string $table): string
1132
    {
1133 6
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1134 6
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1135
    }
1136
1137
    /**
1138
     * Creates a SQL command for adding a check constraint to an existing table.
1139
     *
1140
     * @param string $name the name of the check constraint. The name will be properly quoted by the method.
1141
     * @param string $table the table that the check constraint will be added to. The name will be properly quoted by
1142
     * the method.
1143
     * @param string $expression the SQL of the `CHECK` constraint.
1144
     *
1145
     * @return string the SQL statement for adding a check constraint to an existing table.
1146
     */
1147 6
    public function addCheck(string $name, string $table, string $expression): string
1148
    {
1149 6
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1150 6
            . $this->db->quoteColumnName($name) . ' CHECK (' . $this->db->quoteSql($expression) . ')';
1151
    }
1152
1153
    /**
1154
     * Creates a SQL command for dropping a check constraint.
1155
     *
1156
     * @param string $name the name of the check constraint to be dropped. The name will be properly quoted by the
1157
     * method.
1158
     * @param string $table the table whose check constraint is to be dropped. The name will be properly quoted by the
1159
     * method.
1160
     *
1161
     * @return string the SQL statement for dropping a check constraint.
1162
     */
1163 6
    public function dropCheck(string $name, string $table): string
1164
    {
1165 6
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1166 6
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1167
    }
1168
1169
    /**
1170
     * Creates a SQL command for adding a default value constraint to an existing table.
1171
     *
1172
     * @param string $name the name of the default value constraint.
1173
     * The name will be properly quoted by the method.
1174
     * @param string $table the table that the default value constraint will be added to.
1175
     * The name will be properly quoted by the method.
1176
     * @param string $column the name of the column to that the constraint will be added on.
1177
     * The name will be properly quoted by the method.
1178
     * @param mixed $value default value.
1179
     *
1180
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
1181
     *
1182
     * @return string the SQL statement for adding a default value constraint to an existing table.
1183
     */
1184
    public function addDefaultValue(string $name, string $table, string $column, $value): string
1185
    {
1186
        throw new NotSupportedException(
1187
            $this->db->getDriverName() . ' does not support adding default value constraints.'
1188
        );
1189
    }
1190
1191
    /**
1192
     * Creates a SQL command for dropping a default value constraint.
1193
     *
1194
     * @param string $name the name of the default value constraint to be dropped.
1195
     * The name will be properly quoted by the method.
1196
     * @param string $table the table whose default value constraint is to be dropped.
1197
     * The name will be properly quoted by the method.
1198
     *
1199
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
1200
     *
1201
     * @return string the SQL statement for dropping a default value constraint.
1202
     */
1203
    public function dropDefaultValue(string $name, string $table): string
1204
    {
1205
        throw new NotSupportedException(
1206
            $this->db->getDriverName() . ' does not support dropping default value constraints.'
1207
        );
1208
    }
1209
1210
    /**
1211
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
1212
     *
1213
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
1214
     * or 1.
1215
     *
1216
     * @param string $tableName the name of the table whose primary key sequence will be reset.
1217
     * @param array|string|null $value the value for the primary key of the next new row inserted. If this is not set,
1218
     * the next new row's primary key will have a value 1.
1219
     *
1220
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
1221
     *
1222
     * @return string the SQL statement for resetting sequence.
1223
     */
1224
    public function resetSequence(string $tableName, $value = null): string
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1224
    public function resetSequence(/** @scrutinizer ignore-unused */ string $tableName, $value = null): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1225
    {
1226
        throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.');
1227
    }
1228
1229
    /**
1230
     * Builds a SQL statement for enabling or disabling integrity check.
1231
     *
1232
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
1233
     * @param string $table the table name. Defaults to empty string, meaning that no table will be changed.
1234
     * @param bool $check whether to turn on or off the integrity check.
1235
     *
1236
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
1237
     *
1238
     * @return string the SQL statement for checking integrity.
1239
     */
1240
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
0 ignored issues
show
Unused Code introduced by
The parameter $check is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1240
    public function checkIntegrity(string $schema = '', string $table = '', /** @scrutinizer ignore-unused */ bool $check = true): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1241
    {
1242
        throw new NotSupportedException(
1243
            $this->db->getDriverName() . ' does not support enabling/disabling integrity check.'
1244
        );
1245
    }
1246
1247
    /**
1248
     * Builds a SQL command for adding comment to column.
1249
     *
1250
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1251
     * method.
1252
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1253
     * method.
1254
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1255
     *
1256
     * @throws Exception
1257
     *
1258
     * @return string the SQL statement for adding comment on column.
1259
     */
1260 3
    public function addCommentOnColumn(string $table, string $column, string $comment): string
1261
    {
1262 3
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column)
1263 3
            . ' IS ' . $this->db->quoteValue($comment);
1264
    }
1265
1266
    /**
1267
     * Builds a SQL command for adding comment to table.
1268
     *
1269
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1270
     * method.
1271
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1272
     *
1273
     * @throws Exception
1274
     *
1275
     * @return string the SQL statement for adding comment on table.
1276
     */
1277 2
    public function addCommentOnTable(string $table, string $comment): string
1278
    {
1279 2
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS ' . $this->db->quoteValue($comment);
1280
    }
1281
1282
    /**
1283
     * Builds a SQL command for adding comment to column.
1284
     *
1285
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1286
     * method.
1287
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1288
     * method.
1289
     *
1290
     * @return string the SQL statement for adding comment on column.
1291
     */
1292 1
    public function dropCommentFromColumn(string $table, string $column): string
1293
    {
1294 1
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column)
1295 1
            . ' IS NULL';
1296
    }
1297
1298
    /**
1299
     * Builds a SQL command for adding comment to table.
1300
     *
1301
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1302
     * method.
1303
     *
1304
     * @return string the SQL statement for adding comment on column.
1305
     */
1306 1
    public function dropCommentFromTable(string $table): string
1307
    {
1308 1
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS NULL';
1309
    }
1310
1311
    /**
1312
     * Creates a SQL View.
1313
     *
1314
     * @param string $viewName the name of the view to be created.
1315
     * @param Query|string $subQuery the select statement which defines the view.
1316
     *
1317
     * This can be either a string or a {@see Query} object.
1318
     *
1319
     * @throws Exception|InvalidConfigException|NotSupportedException
1320
     *
1321
     * @return string the `CREATE VIEW` SQL statement.
1322
     */
1323 5
    public function createView(string $viewName, $subQuery): string
1324
    {
1325 5
        if ($subQuery instanceof Query) {
1326
            /** @psalm-var array<array-key, int|string> $params */
1327 5
            [$rawQuery, $params] = $this->build($subQuery);
1328
1329 5
            foreach ($params as $key => $value) {
1330 5
                $params[$key] = $this->db->quoteValue($value);
1331
            }
1332
1333 5
            $subQuery = strtr($rawQuery, $params);
1334
        }
1335
1336 5
        return 'CREATE VIEW ' . $this->db->quoteTableName($viewName) . ' AS ' . $subQuery;
1337
    }
1338
1339
    /**
1340
     * Drops a SQL View.
1341
     *
1342
     * @param string $viewName the name of the view to be dropped.
1343
     *
1344
     * @return string the `DROP VIEW` SQL statement.
1345
     */
1346 5
    public function dropView(string $viewName): string
1347
    {
1348 5
        return 'DROP VIEW ' . $this->db->quoteTableName($viewName);
1349
    }
1350
1351
    /**
1352
     * Converts an abstract column type into a physical column type.
1353
     *
1354
     * The conversion is done using the type map specified in {@see typeMap}.
1355
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
1356
     * physical types):
1357
     *
1358
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY
1359
     *    KEY"
1360
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT
1361
     *    PRIMARY KEY"
1362
     * - `upk`: an unsigned auto-incremental primary key type, will be converted into "int(10) UNSIGNED NOT NULL
1363
     *    AUTO_INCREMENT PRIMARY KEY"
1364
     * - `char`: char type, will be converted into "char(1)"
1365
     * - `string`: string type, will be converted into "varchar(255)"
1366
     * - `text`: a long string type, will be converted into "text"
1367
     * - `smallint`: a small integer type, will be converted into "smallint(6)"
1368
     * - `integer`: integer type, will be converted into "int(11)"
1369
     * - `bigint`: a big integer type, will be converted into "bigint(20)"
1370
     * - `boolean`: boolean type, will be converted into "tinyint(1)"
1371
     * - `float``: float number type, will be converted into "float"
1372
     * - `decimal`: decimal number type, will be converted into "decimal"
1373
     * - `datetime`: datetime type, will be converted into "datetime"
1374
     * - `timestamp`: timestamp type, will be converted into "timestamp"
1375
     * - `time`: time type, will be converted into "time"
1376
     * - `date`: date type, will be converted into "date"
1377
     * - `money`: money type, will be converted into "decimal(19,4)"
1378
     * - `binary`: binary data type, will be converted into "blob"
1379
     *
1380
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only the first
1381
     * part will be converted, and the rest of the parts will be appended to the converted result.
1382
     *
1383
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
1384
     *
1385
     * For some of the abstract types you can also specify a length or precision constraint by appending it in round
1386
     * brackets directly to the type.
1387
     *
1388
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. If the underlying DBMS does
1389
     * not support these kind of constraints for a type it will be ignored.
1390
     *
1391
     * If a type cannot be found in {@see typeMap}, it will be returned without any change.
1392
     *
1393
     * @param ColumnSchemaBuilder|string $type abstract column type.
1394
     *
1395
     * @return string physical column type.
1396
     */
1397 67
    public function getColumnType($type): string
1398
    {
1399 67
        if ($type instanceof ColumnSchemaBuilder) {
1400 11
            $type = $type->__toString();
1401
        }
1402
1403 67
        if (isset($this->typeMap[$type])) {
1404 42
            return $this->typeMap[$type];
1405
        }
1406
1407 40
        if (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
1408 20
            if (isset($this->typeMap[$matches[1]])) {
1409 17
                return preg_replace(
1410
                    '/\(.+\)/',
1411 17
                    '(' . $matches[2] . ')',
1412 17
                    $this->typeMap[$matches[1]]
1413 20
                ) . $matches[3];
1414
            }
1415 33
        } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
1416 32
            if (isset($this->typeMap[$matches[1]])) {
1417 32
                return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
1418
            }
1419
        }
1420
1421 5
        return $type;
1422
    }
1423
1424
    /**
1425
     * @param array $columns
1426
     * @param array $params the binding parameters to be populated.
1427
     * @param bool|null $distinct
1428
     * @param string|null $selectOption
1429
     *
1430
     * @psalm-param array<array-key, ExpressionInterface|Query|string> $columns
1431
     *
1432
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
1433
     *
1434
     * @return string the SELECT clause built from {@see Query::$select}.
1435
     */
1436 1508
    public function buildSelect(
1437
        array $columns,
1438
        array &$params,
1439
        ?bool $distinct = false,
1440
        string $selectOption = null
1441
    ): string {
1442 1508
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
1443
1444 1508
        if ($selectOption !== null) {
1445
            $select .= ' ' . $selectOption;
1446
        }
1447
1448 1508
        if (empty($columns)) {
1449 1287
            return $select . ' *';
1450
        }
1451
1452 434
        foreach ($columns as $i => $column) {
1453 434
            if ($column instanceof ExpressionInterface) {
1454 73
                if (is_int($i)) {
1455 10
                    $columns[$i] = $this->buildExpression($column, $params);
1456
                } else {
1457 73
                    $columns[$i] = $this->buildExpression($column, $params) . ' AS ' . $this->db->quoteColumnName($i);
1458
                }
1459 416
            } elseif ($column instanceof Query) {
1460
                [$sql, $params] = $this->build($column, $params);
1461
                $columns[$i] = "($sql) AS " . $this->db->quoteColumnName((string) $i);
1462 416
            } elseif (is_string($i) && $i !== $column) {
1463 19
                if (strpos($column, '(') === false) {
1464 14
                    $column = $this->db->quoteColumnName($column);
1465
                }
1466 19
                $columns[$i] = "$column AS " . $this->db->quoteColumnName($i);
1467 412
            } elseif (strpos($column, '(') === false) {
1468 317
                if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $column, $matches)) {
1469
                    $columns[$i] = $this->db->quoteColumnName(
1470
                        $matches[1]
1471
                    ) . ' AS ' . $this->db->quoteColumnName($matches[2]);
1472
                } else {
1473 317
                    $columns[$i] = $this->db->quoteColumnName($column);
1474
                }
1475
            }
1476
        }
1477
1478 434
        return $select . ' ' . implode(', ', $columns);
1479
    }
1480
1481
    /**
1482
     * @param array|null $tables
1483
     * @param array $params the binding parameters to be populated.
1484
     *
1485
     * @psalm-param array<array-key, array|Query|string> $tables
1486
     *
1487
     * @throws Exception|InvalidConfigException|NotSupportedException
1488
     *
1489
     * @return string the FROM clause built from {@see Query::$from}.
1490
     */
1491 1528
    public function buildFrom(?array $tables, array &$params): string
1492
    {
1493 1528
        if (empty($tables)) {
1494 668
            return '';
1495
        }
1496
1497 922
        $tables = $this->quoteTableNames($tables, $params);
1498
1499 922
        return 'FROM ' . implode(', ', $tables);
1500
    }
1501
1502
    /**
1503
     * @param array $joins
1504
     * @param array $params the binding parameters to be populated.
1505
     *
1506
     * @psalm-param array<
1507
     *   array-key,
1508
     *   array{
1509
     *     0?:string,
1510
     *     1?:array<array-key, Query|string>|string,
1511
     *     2?:array|ExpressionInterface|string|null
1512
     *   }|null
1513
     * > $joins
1514
     *
1515
     * @throws Exception if the $joins parameter is not in proper format.
1516
     *
1517
     * @return string the JOIN clause built from {@see Query::$join}.
1518
     */
1519 1508
    public function buildJoin(array $joins, array &$params): string
1520
    {
1521 1508
        if (empty($joins)) {
1522 1503
            return '';
1523
        }
1524
1525 110
        foreach ($joins as $i => $join) {
1526 110
            if (!is_array($join) || !isset($join[0], $join[1])) {
1527
                throw new Exception(
1528
                    'A join clause must be specified as an array of join type, join table, and optionally join '
1529
                    . 'condition.'
1530
                );
1531
            }
1532
1533
            /* 0:join type, 1:join table, 2:on-condition (optional) */
1534 110
            [$joinType, $table] = $join;
1535
1536 110
            $tables = $this->quoteTableNames((array) $table, $params);
1537
1538
            /** @var string $table */
1539 110
            $table = reset($tables);
1540 110
            $joins[$i] = "$joinType $table";
1541
1542 110
            if (isset($join[2])) {
1543 110
                $condition = $this->buildCondition($join[2], $params);
1544 110
                if ($condition !== '') {
1545 110
                    $joins[$i] .= ' ON ' . $condition;
1546
                }
1547
            }
1548
        }
1549
1550 110
        return implode($this->separator, $joins);
1551
    }
1552
1553
    /**
1554
     * Quotes table names passed.
1555
     *
1556
     * @param array $tables
1557
     * @param array $params
1558
     *
1559
     * @psalm-param array<array-key, array|Query|string> $tables
1560
     *
1561
     * @throws Exception|InvalidConfigException|NotSupportedException
1562
     *
1563
     * @return array
1564
     */
1565 922
    private function quoteTableNames(array $tables, array &$params): array
1566
    {
1567 922
        foreach ($tables as $i => $table) {
1568 922
            if ($table instanceof Query) {
1569 16
                [$sql, $params] = $this->build($table, $params);
1570 16
                $tables[$i] = "($sql) " . $this->db->quoteTableName((string) $i);
1571 922
            } elseif (is_string($table) && is_string($i)) {
1572 58
                if (strpos($table, '(') === false) {
1573 40
                    $table = $this->db->quoteTableName($table);
1574
                }
1575 58
                $tables[$i] = "$table " . $this->db->quoteTableName($i);
1576 899
            } elseif (is_string($table) && strpos($table, '(') === false) {
1577 888
                $tableWithAlias = $this->extractAlias($table);
1578 888
                if (is_array($tableWithAlias)) { // with alias
1579 50
                    $tables[$i] = $this->db->quoteTableName($tableWithAlias[1]) . ' '
1580 50
                        . $this->db->quoteTableName($tableWithAlias[2]);
1581
                } else {
1582 848
                    $tables[$i] = $this->db->quoteTableName($table);
1583
                }
1584
            }
1585
        }
1586
1587 922
        return $tables;
1588
    }
1589
1590
    /**
1591
     * @param array|string $condition
1592
     * @param array $params the binding parameters to be populated.
1593
     *
1594
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
1595
     *
1596
     * @return string the WHERE clause built from {@see Query::$where}.
1597
     */
1598 1534
    public function buildWhere($condition, array &$params = []): string
1599
    {
1600 1534
        $where = $this->buildCondition($condition, $params);
1601
1602 1534
        return ($where === '') ? '' : ('WHERE ' . $where);
1603
    }
1604
1605
    /**
1606
     * @param array $columns
1607
     * @psalm-param array<string, Expression|string> $columns
1608
     *
1609
     * @param array $params the binding parameters to be populated
1610
     *
1611
     * @throws Exception|InvalidArgumentException
1612
     *
1613
     * @return string the GROUP BY clause
1614
     */
1615 1508
    public function buildGroupBy(array $columns, array &$params = []): string
1616
    {
1617 1508
        if (empty($columns)) {
1618 1498
            return '';
1619
        }
1620
1621 15
        foreach ($columns as $i => $column) {
1622 15
            if ($column instanceof Expression) {
1623 5
                $columns[$i] = $this->buildExpression($column);
1624 5
                $params = array_merge($params, $column->getParams());
1625 15
            } elseif (strpos($column, '(') === false) {
1626 15
                $columns[$i] = $this->db->quoteColumnName($column);
1627
            }
1628
        }
1629
1630 15
        return 'GROUP BY ' . implode(', ', $columns);
1631
    }
1632
1633
    /**
1634
     * @param array|string $condition
1635
     * @param array $params the binding parameters to be populated.
1636
     *
1637
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
1638
     *
1639
     * @return string the HAVING clause built from {@see Query::$having}.
1640
     */
1641 1508
    public function buildHaving($condition, array &$params = []): string
1642
    {
1643 1508
        $having = $this->buildCondition($condition, $params);
1644
1645 1508
        return ($having === '') ? '' : ('HAVING ' . $having);
1646
    }
1647
1648
    /**
1649
     * Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
1650
     *
1651
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET).
1652
     * @param array $orderBy the order by columns. See {@see Query::orderBy} for more details on how to specify this
1653
     * parameter.
1654
     * @param Expression|int|null $limit the limit number. See {@see Query::limit} for more details.
1655
     * @param Expression|int|null $offset the offset number. See {@see Query::offset} for more details.
1656
     * @param array $params the binding parameters to be populated.
1657
     *
1658
     * @psalm-param array<string, Expression|int|string> $orderBy
1659
     *
1660
     * @throws Exception|InvalidArgumentException
1661
     *
1662
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any).
1663
     */
1664 941
    public function buildOrderByAndLimit(
1665
        string $sql,
1666
        array $orderBy,
1667
        $limit,
1668
        $offset,
1669
        array &$params = []
1670
    ): string {
1671 941
        $orderBy = $this->buildOrderBy($orderBy, $params);
1672 941
        if ($orderBy !== '') {
1673 138
            $sql .= $this->separator . $orderBy;
1674
        }
1675 941
        $limit = $this->buildLimit($limit, $offset);
1676 941
        if ($limit !== '') {
1677 36
            $sql .= $this->separator . $limit;
1678
        }
1679
1680 941
        return $sql;
1681
    }
1682
1683
    /**
1684
     * @param array $columns
1685
     * @param array $params the binding parameters to be populated
1686
     *
1687
     * @psalm-param array<string, Expression|int|string> $columns
1688
     *
1689
     * @throws Exception|InvalidArgumentException
1690
     *
1691
     * @return string the ORDER BY clause built from {@see Query::$orderBy}.
1692
     */
1693 1520
    public function buildOrderBy(array $columns, array &$params = []): string
1694
    {
1695 1520
        if (empty($columns)) {
1696 1465
            return '';
1697
        }
1698
1699 229
        $orders = [];
1700
1701 229
        foreach ($columns as $name => $direction) {
1702 229
            if ($direction instanceof Expression) {
1703 5
                $orders[] = $this->buildExpression($direction);
1704 5
                $params = array_merge($params, $direction->getParams());
1705
            } else {
1706 229
                $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
1707
            }
1708
        }
1709
1710 229
        return 'ORDER BY ' . implode(', ', $orders);
1711
    }
1712
1713
    /**
1714
     * @param Expression|int|null $limit
1715
     * @param Expression|int|null $offset
1716
     *
1717
     * @return string the LIMIT and OFFSET clauses.
1718
     */
1719 347
    public function buildLimit($limit, $offset): string
1720
    {
1721 347
        $sql = '';
1722
1723 347
        if ($this->hasLimit($limit)) {
1724 11
            $sql = 'LIMIT ' . (string) $limit;
1725
        }
1726
1727 347
        if ($this->hasOffset($offset)) {
1728 3
            $sql .= ' OFFSET ' . (string) $offset;
1729
        }
1730
1731 347
        return ltrim($sql);
1732
    }
1733
1734
    /**
1735
     * Checks to see if the given limit is effective.
1736
     *
1737
     * @param mixed $limit the given limit.
1738
     *
1739
     * @return bool whether the limit is effective.
1740
     */
1741 1217
    protected function hasLimit($limit): bool
1742
    {
1743 1217
        return ($limit instanceof ExpressionInterface) || ctype_digit((string) $limit);
1744
    }
1745
1746
    /**
1747
     * Checks to see if the given offset is effective.
1748
     *
1749
     * @param mixed $offset the given offset.
1750
     *
1751
     * @return bool whether the offset is effective.
1752
     */
1753 1217
    protected function hasOffset($offset): bool
1754
    {
1755 1217
        return ($offset instanceof ExpressionInterface) || (ctype_digit((string)$offset) && (string)$offset !== '0');
1756
    }
1757
1758
    /**
1759
     * @param array $unions
1760
     * @param array $params the binding parameters to be populated
1761
     *
1762
     * @psalm-param array<array{query:Query|string, all:bool}> $unions
1763
     *
1764
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
1765
     *
1766
     * @return string the UNION clause built from {@see Query::$union}.
1767
     */
1768 1220
    public function buildUnion(array $unions, array &$params): string
1769
    {
1770 1220
        if (empty($unions)) {
1771 1220
            return '';
1772
        }
1773
1774 9
        $result = '';
1775
1776 9
        foreach ($unions as $i => $union) {
1777 9
            $query = $union['query'];
1778 9
            if ($query instanceof Query) {
1779 9
                [$unions[$i]['query'], $params] = $this->build($query, $params);
1780
            }
1781
1782 9
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
1783
        }
1784
1785 9
        return trim($result);
1786
    }
1787
1788
    /**
1789
     * Processes columns and properly quotes them if necessary.
1790
     *
1791
     * It will join all columns into a string with comma as separators.
1792
     *
1793
     * @param array|string $columns the columns to be processed.
1794
     *
1795
     * @psalm-param array<array-key, ExpressionInterface|string>|string $columns
1796
     *
1797
     * @throws Exception|InvalidArgumentException
1798
     *
1799
     * @return string the processing result.
1800
     */
1801 42
    public function buildColumns($columns): string
1802
    {
1803 42
        if (!is_array($columns)) {
1804 32
            if (strpos($columns, '(') !== false) {
1805
                return $columns;
1806
            }
1807
1808 32
            $rawColumns = $columns;
1809 32
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1810
1811 32
            if ($columns === false) {
1812
                throw new InvalidArgumentException("$rawColumns is not valid columns.");
1813
            }
1814
        }
1815 42
        foreach ($columns as $i => $column) {
1816 42
            if ($column instanceof ExpressionInterface) {
1817
                $columns[$i] = $this->buildExpression($column);
1818 42
            } elseif (strpos($column, '(') === false) {
1819 42
                $columns[$i] = $this->db->quoteColumnName($column);
1820
            }
1821
        }
1822
1823 42
        return implode(', ', $columns);
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type string; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1823
        return implode(', ', /** @scrutinizer ignore-type */ $columns);
Loading history...
1824
    }
1825
1826
    /**
1827
     * Parses the condition specification and generates the corresponding SQL expression.
1828
     *
1829
     * @param array|ExpressionInterface|string|null $condition the condition specification.
1830
     * Please refer to {@see Query::where()} on how to specify a condition.
1831
     * @param array $params the binding parameters to be populated.
1832
     *
1833
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
1834
     *
1835
     * @return string the generated SQL expression.
1836
     */
1837 1538
    public function buildCondition($condition, array &$params = []): string
1838
    {
1839 1538
        if (is_array($condition)) {
1840 1172
            if (empty($condition)) {
1841 5
                return '';
1842
            }
1843
1844 1172
            $condition = $this->createConditionFromArray($condition);
1845
        }
1846
1847 1538
        if ($condition instanceof ExpressionInterface) {
1848 1277
            return $this->buildExpression($condition, $params);
1849
        }
1850
1851 1514
        return $condition ?? '';
1852
    }
1853
1854
    /**
1855
     * Transforms $condition defined in array format (as described in {@see Query::where()} to instance of
1856
     *
1857
     * @param array|string $condition.
1858
     *
1859
     * @throws InvalidArgumentException
1860
     *
1861
     * @return ConditionInterface
1862
     *
1863
     * {@see ConditionInterface|ConditionInterface} according to {@see conditionClasses} map.
1864
     */
1865 1172
    public function createConditionFromArray(array $condition): ConditionInterface
1866
    {
1867
        /** operator format: operator, operand 1, operand 2, ... */
1868 1172
        if (isset($condition[0])) {
1869 820
            $operator = strtoupper((string) array_shift($condition));
1870
1871 820
            $className = $this->conditionClasses[$operator] ?? SimpleCondition::class;
1872
1873
            /** @var ConditionInterface $className */
1874 820
            return $className::fromArrayDefinition($operator, $condition);
1875
        }
1876
1877
        /** hash format: 'column1' => 'value1', 'column2' => 'value2', ... */
1878 619
        return new HashCondition($condition);
1879
    }
1880
1881
    /**
1882
     * Creates a SELECT EXISTS() SQL statement.
1883
     *
1884
     * @param string $rawSql the subquery in a raw form to select from.
1885
     *
1886
     * @return string the SELECT EXISTS() SQL statement.
1887
     */
1888 9
    public function selectExists(string $rawSql): string
1889
    {
1890 9
        return 'SELECT EXISTS(' . $rawSql . ')';
1891
    }
1892
1893
    /**
1894
     * Helper method to add $value to $params array using {@see PARAM_PREFIX}.
1895
     *
1896
     * @param mixed $value
1897
     * @param array $params passed by reference.
1898
     *
1899
     * @return string the placeholder name in $params array.
1900
     */
1901 1188
    public function bindParam($value, array &$params = []): string
1902
    {
1903 1188
        $phName = self::PARAM_PREFIX . count($params);
1904
1905
        /** @psalm-var mixed */
1906 1188
        $params[$phName] = $value;
1907
1908 1188
        return $phName;
1909
    }
1910
1911
    /**
1912
     * Extracts table alias if there is one or returns false.
1913
     *
1914
     * @param $table
1915
     *
1916
     * @return array|bool
1917
     *
1918
     * @psalm-return array<array-key, string>|bool
1919
     */
1920 888
    protected function extractAlias(string $table)
1921
    {
1922 888
        if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) {
1923 50
            return $matches;
1924
        }
1925
1926 848
        return false;
1927
    }
1928
1929
    /**
1930
     * @param array $withs
1931
     * @param array $params
1932
     *
1933
     * @psalm-param array<array-key, array{query:string|Query, alias:string, recursive:bool}> $withs
1934
     *
1935
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
1936
     *
1937
     * @return string
1938
     */
1939 1508
    public function buildWithQueries(array $withs, array &$params): string
1940
    {
1941 1508
        if (empty($withs)) {
1942 1508
            return '';
1943
        }
1944
1945 10
        $recursive = false;
1946 10
        $result = [];
1947
1948 10
        foreach ($withs as $i => $with) {
1949 10
            if ($with['recursive']) {
1950 5
                $recursive = true;
1951
            }
1952
1953 10
            $query = $with['query'];
1954 10
            if ($query instanceof Query) {
1955 10
                [$with['query'], $params] = $this->build($query, $params);
1956
            }
1957
1958 10
            $result[] = $with['alias'] . ' AS (' . $with['query'] . ')';
1959
        }
1960
1961 10
        return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . implode(', ', $result);
1962
    }
1963
1964 1336
    public function getDb(): Connection
1965
    {
1966 1336
        return $this->db;
1967
    }
1968
1969
    /**
1970
     * @param string the separator between different fragments of a SQL statement.
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Query\the was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1971
     *
1972
     * Defaults to an empty space. This is mainly used by {@see build()} when generating a SQL statement.
1973
     */
1974 5
    public function setSeparator(string $separator): void
1975
    {
1976 5
        $this->separator = $separator;
1977 5
    }
1978
}
1979