Passed
Pull Request — master (#216)
by Wilmer
12:01
created

QueryBuilder::batchInsert()   C

Complexity

Conditions 13
Paths 85

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 13.0069

Importance

Changes 0
Metric Value
cc 13
eloc 31
c 0
b 0
f 0
nc 85
nop 4
dl 0
loc 50
ccs 28
cts 29
cp 0.9655
crap 13.0069
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<array-key, 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 1931
133
    public function __construct(Connection $db)
134 1931
    {
135 1931
        $this->db = $db;
136 1931
        $this->expressionBuilders = $this->defaultExpressionBuilders();
137 1931
        $this->conditionClasses = $this->defaultConditionClasses();
138
    }
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 1931
     */
148
    protected function defaultConditionClasses(): array
149
    {
150 1931
        return [
151
            '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 1931
     */
175
    protected function defaultExpressionBuilders(): array
176
    {
177 1931
        return [
178
            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 1219
     *
235
     * @psalm-return array{0: string, 1: array}
236 1219
     */
237
    public function build(Query $query, array $params = []): array
238 1219
    {
239
        $query = $query->prepare($this);
240
241 1219
        $params = empty($params) ? $query->getParams() : array_merge($params, $query->getParams());
242 1219
243 1219
        $clauses = [
244 1219
            $this->buildSelect($query->getSelect(), $params, $query->getDistinct(), $query->getSelectOption()),
245 1219
            $this->buildFrom($query->getFrom(), $params),
246 1219
            $this->buildJoin($query->getJoin(), $params),
247
            $this->buildWhere($query->getWhere(), $params),
248
            $this->buildGroupBy($query->getGroupBy(), $params),
249 1219
            $this->buildHaving($query->getHaving(), $params),
250
        ];
251 1219
252
        $sql = implode($this->separator, array_filter($clauses));
253 1219
254 180
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
255 180
256 4
        if (!empty($query->getOrderBy())) {
257
            foreach ($query->getOrderBy() as $expression) {
258
                if ($expression instanceof ExpressionInterface) {
259
                    $this->buildExpression($expression, $params);
260
                }
261 1219
            }
262 12
        }
263 12
264 4
        if (!empty($query->getGroupBy())) {
265
            foreach ($query->getGroupBy() as $expression) {
266
                if ($expression instanceof ExpressionInterface) {
267
                    $this->buildExpression($expression, $params);
268
                }
269 1219
            }
270
        }
271 1219
272 9
        $union = $this->buildUnion($query->getUnion(), $params);
273
274
        if ($union !== '') {
275 1219
            $sql = "($sql){$this->separator}$union";
276
        }
277 1219
278 8
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
279
280
        if ($with !== '') {
281 1219
            $sql = "$with{$this->separator}$sql";
282
        }
283
284
        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 1368
     * {@see ExpressionBuilderInterface}
301
     * {@see expressionBuilders}
302 1368
     */
303
    public function buildExpression(ExpressionInterface $expression, array &$params = []): string
304 1368
    {
305
        $builder = $this->getExpressionBuilder($expression);
306
307
        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 1368
     *
321
     * {@see expressionBuilders}
322 1368
     */
323
    public function getExpressionBuilder(ExpressionInterface $expression)
324 1368
    {
325
        $className = get_class($expression);
326
327
        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 1368
            }
340
        }
341
342
        if ($this->expressionBuilders[$className] === __CLASS__) {
343 1368
            return $this;
344 1368
        }
345
346
        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 1368
            $this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this);
348
        }
349
350
        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 197
     *
375
     * @return string the INSERT SQL.
376 197
     */
377
    public function insert(string $table, $columns, array &$params = []): string
378 185
    {
379 185
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
380 185
381
        return 'INSERT INTO ' . $this->db->quoteTableName($table)
382
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
383
            . (!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|JsonException|NotSupportedException
396 282
     *
397
     * @return array array of column names, placeholders, values and params.
398 282
     */
399 282
    protected function prepareInsertValues(string $table, $columns, array $params = []): array
400 282
    {
401 282
        $schema = $this->db->getSchema();
402 282
        $tableSchema = $schema->getTableSchema($table);
403 282
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
404
        $names = [];
405 282
        $placeholders = [];
406 69
        $values = ' DEFAULT VALUES';
407
408 222
        if ($columns instanceof Query) {
409 217
            [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
410 217
        } else {
411
            foreach ($columns as $name => $value) {
412 217
                $names[] = $schema->quoteColumnName($name);
413 43
                $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
414 211
415
                if ($value instanceof ExpressionInterface) {
416
                    $placeholders[] = $this->buildExpression($value, $params);
417
                } elseif ($value instanceof Query) {
418 211
                    [$sql, $params] = $this->build($value, $params);
419
                    $placeholders[] = "($sql)";
420
                } else {
421
                    $placeholders[] = $this->bindParam($value, $params);
422
                }
423 267
            }
424
        }
425
426
        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 69
     *
439
     * @return array array of column names, values and params.
440
     */
441 69
    protected function prepareInsertSelectSubQuery(Query $columns, Schema $schema, array $params = []): array
442 69
    {
443 69
        if (
444
            !is_array($columns->getSelect())
0 ignored issues
show
introduced by
The condition is_array($columns->getSelect()) is always true.
Loading history...
445 15
            || empty($columns->getSelect())
446
            || in_array('*', $columns->getSelect(), true)
447
        ) {
448 54
            throw new InvalidArgumentException('Expected select query object with enumerated (named) parameters');
449
        }
450 54
451 54
        [$values, $params] = $this->build($columns, $params);
452
453 54
        $names = [];
454 54
        $values = ' ' . $values;
455 54
456
        foreach ($columns->getSelect() as $title => $field) {
457
            if (is_string($title)) {
458
                $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 54
            }
464
        }
465
466
        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|JsonException
492 40
     *
493
     * @return string the batch INSERT SQL statement.
494 40
     */
495 4
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
496
    {
497
        if (empty($rows)) {
498 38
            return '';
499
        }
500
501 38
        $schema = $this->db->getSchema();
502 38
503
504
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
505
            $columnSchemas = $tableSchema->getColumns();
506
        } else {
507 38
            $columnSchemas = [];
508
        }
509 38
510 35
        $values = [];
511 35
512 35
        foreach ($rows as $row) {
513 26
            $vs = [];
514
            foreach ($row as $i => $value) {
515 35
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
516 23
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
517 21
                }
518
                if (is_string($value)) {
519 2
                    $value = $schema->quoteValue($value);
520 21
                } elseif (is_float($value)) {
521 7
                    /* ensure type cast always has . as decimal separator in all locales */
522 21
                    $value = NumericHelper::normalize((string) $value);
523 12
                } elseif ($value === false) {
524 13
                    $value = 0;
525 9
                } elseif ($value === null) {
526
                    $value = 'NULL';
527 35
                } elseif ($value instanceof ExpressionInterface) {
528
                    $value = $this->buildExpression($value, $params);
529 35
                }
530
                $vs[] = $value;
531
            }
532 38
            $values[] = '(' . implode(', ', $vs) . ')';
533 3
        }
534
535
        if (empty($values)) {
536 35
            return '';
537 32
        }
538
539
        foreach ($columns as $i => $name) {
540 35
            $columns[$i] = $schema->quoteColumnName($name);
541 35
        }
542
543
        return 'INSERT INTO ' . $schema->quoteTableName($table)
544
            . ' (' . 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 89
     *
592
     * @return array
593 89
     */
594 40
    protected function prepareUpsertColumns(string $table, $insertColumns, $updateColumns, array &$constraints = []): array
595
    {
596
        if ($insertColumns instanceof Query) {
597 49
            [$insertNames] = $this->prepareInsertSelectSubQuery($insertColumns, $this->db->getSchema());
598
        } else {
599
            /** @psalm-suppress UndefinedMethod */
600 89
            $insertNames = array_map([$this->db, 'quoteColumnName'], array_keys($insertColumns));
601
        }
602
603 89
        $uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
604
605 89
        /** @psalm-suppress UndefinedMethod */
606 64
        $uniqueNames = array_map([$this->db, 'quoteColumnName'], $uniqueNames);
607
608
        if ($updateColumns !== true) {
609 25
            return [$uniqueNames, $insertNames, null];
610
        }
611
612
        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 89
     *
628
     * @return array column list.
629 89
     */
630
    private function getTableUniqueColumnNames(string $name, array $columns, array &$constraints = []): array
631 89
    {
632
        $schema = $this->db->getSchema();
633
634
        if (!$schema instanceof ConstraintFinderInterface) {
635 89
            return [];
636 89
        }
637
638 89
        $constraints = [];
639 88
        $primaryKey = $schema->getTablePrimaryKey($name);
640
641
        if ($primaryKey !== null) {
642 89
            $constraints[] = $primaryKey;
643 88
        }
644 88
645
        foreach ($schema->getTableIndexes($name) as $constraint) {
646
            if ($constraint->isUnique()) {
647
                $constraints[] = $constraint;
648 89
            }
649
        }
650
651 89
        $constraints = array_merge($constraints, $schema->getTableUniques($name));
652 89
653 89
        /** Remove duplicates */
654 89
        $constraints = array_combine(
655 89
            array_map(
656
                static function ($constraint) {
657 89
                    $columns = $constraint->getColumnNames();
658
                    sort($columns, SORT_STRING);
659 89
660
                    return json_encode($columns, JSON_THROW_ON_ERROR);
661 89
                },
662
                $constraints
663
            ),
664 89
            $constraints
665
        );
666
667 89
        $columnNames = [];
668 89
669 89
        /** Remove all constraints which do not cover the specified column list */
670 89
        $constraints = array_values(
671
            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 74
                    $result = !array_diff($constraintColumnNames, $columns);
677
678
                    if ($result) {
679 89
                        $columnNames = array_merge($columnNames, $constraintColumnNames);
680 89
                    }
681
682
                    return $result;
683
                }
684 89
            )
685
        );
686
687
        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 94
     *
711
     * @throws Exception|InvalidArgumentException|JsonException
712 94
     *
713 94
     * @return string the UPDATE SQL.
714 94
     */
715
    public function update(string $table, array $columns, $condition, array &$params = []): string
716 94
    {
717
        /**
718
         * @psalm-var array<array-key, mixed> $lines
719
         * @psalm-var array<array-key, mixed> $params
720
         */
721
        [$lines, $params] = $this->prepareUpdateSets($table, $columns, $params);
722
        $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
723
        $where = $this->buildWhere($condition, $params);
724
725
        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 139
     * @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 139
     * DB command later.
735
     *
736 139
     * @psalm-param array<string, ExpressionInterface|string> $columns
737
     *
738 139
     * @throws Exception|InvalidArgumentException|JsonException
739
     *
740 139
     * @return array `SET` parts for an `UPDATE` SQL statement (the first array element) and params (the second array
741 139
     * element).
742 139
     */
743 69
    protected function prepareUpdateSets(string $table, array $columns, array $params = []): array
744
    {
745 96
        $tableSchema = $this->db->getTableSchema($table);
746
747
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
748 139
749
        $sets = [];
750
751 139
        foreach ($columns as $name => $value) {
752
            /** @psalm-var mixed $value */
753
            $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
754
            if ($value instanceof ExpressionInterface) {
755
                $placeholder = $this->buildExpression($value, $params);
756
            } else {
757
                $placeholder = $this->bindParam($value, $params);
758
            }
759
760
            $sets[] = $this->db->quoteColumnName($name) . '=' . $placeholder;
761
        }
762
763
        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 41
     * The method will properly escape the table and column names.
776
     *
777 41
     * @param string $table the table where the data will be deleted from.
778 41
     * @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 41
     * @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
    public function delete(string $table, $condition, array &$params): string
788
    {
789
        $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
790
        $where = $this->buildWhere($condition, $params);
791
792
        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 60
     *  'id' => 'pk',
812
     *  'name' => 'string',
813 60
     *  'age' => 'integer',
814 60
     * ]);
815 60
     * ```
816 60
     *
817
     * @param string $table the name of the table to be created. The name will be properly quoted by the method.
818 5
     * @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 60
     *
823
     * @return string the SQL statement for creating a new DB table.
824 60
     */
825
    public function createTable(string $table, array $columns, ?string $options = null): string
826
    {
827
        $cols = [];
828
829
        foreach ($columns as $name => $type) {
830
            if (is_string($name)) {
831
                $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
832
            } else {
833
                $cols[] = "\t" . $type;
834
            }
835 2
        }
836
837 2
        $sql = 'CREATE TABLE ' . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)";
838
839
        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 9
     *
848
     * @return string the SQL statement for renaming a DB table.
849 9
     */
850
    public function renameTable(string $oldName, string $newName): string
851
    {
852
        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 11
     */
862
    public function dropTable(string $table): string
863 11
    {
864 8
        return 'DROP TABLE ' . $this->db->quoteTableName($table);
865
    }
866
867 11
    /**
868 11
     * 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 11
     * @param string $table the table that the primary key constraint will be added to.
872 11
     * @param array|string $columns comma separated string or array of columns that the primary key will consist of.
873 11
     *
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
    public function addPrimaryKey(string $name, string $table, $columns): string
879
    {
880
        if (is_string($columns)) {
881
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
882
        }
883
884 5
        foreach ($columns as $i => $col) {
885
            $columns[$i] = $this->db->quoteColumnName($col);
886 5
        }
887 5
888
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
889
            . $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
890
            . 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 3
     * @param string $table the table that the primary key constraint will be removed from.
898
     *
899 3
     * @return string the SQL statement for removing a primary key constraint from an existing table.
900
     */
901
    public function dropPrimaryKey(string $name, string $table): string
902
    {
903
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
904
            . ' 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
    public function truncateTable(string $table): string
915
    {
916 2
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table);
917
    }
918 2
919 2
    /**
920 2
     * 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
    public function addColumn(string $table, string $column, string $type): string
934
    {
935
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
936
            . ' ADD ' . $this->db->quoteColumnName($column) . ' '
937
            . $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 1
            . ' RENAME COLUMN ' . $this->db->quoteColumnName($oldName)
967
            . ' TO ' . $this->db->quoteColumnName($newName);
968 1
    }
969 1
970 1
    /**
971 1
     * 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
    public function alterColumn(string $table, string $column, string $type): string
984
    {
985
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
986
            . $this->db->quoteColumnName($column) . ' '
987
            . $this->db->quoteColumnName($column) . ' '
988
            . $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 9
     *
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 9
     * SET DEFAULT, SET NULL.
1004 9
     * @param string|null $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
1005 9
     * SET DEFAULT, SET NULL.
1006 9
     *
1007 9
     * @psalm-param array<array-key,string>|string $columns
1008
     * @psalm-param array<array-key,string>|string $refColumns
1009 9
     *
1010 6
     * @throws Exception|InvalidArgumentException
1011
     *
1012
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
1013 9
     */
1014 6
    public function addForeignKey(
1015
        string $name,
1016
        string $table,
1017 9
        $columns,
1018
        string $refTable,
1019
        $refColumns,
1020
        ?string $delete = null,
1021
        ?string $update = null
1022
    ): string {
1023
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
1024
            . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
1025
            . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
1026
            . ' REFERENCES ' . $this->db->quoteTableName($refTable)
1027
            . ' (' . $this->buildColumns($refColumns) . ')';
1028
1029 7
        if ($delete !== null) {
1030
            $sql .= ' ON DELETE ' . $delete;
1031 7
        }
1032 7
1033
        if ($update !== null) {
1034
            $sql .= ' ON UPDATE ' . $update;
1035
        }
1036
1037
        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
    public function dropForeignKey(string $name, string $table): string
1050 11
    {
1051
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1052 11
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1053 11
    }
1054 11
1055 11
    /**
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 6
     * @psalm-param array<array-key, ExpressionInterface|string>|string $columns
1067
     *
1068 6
     * @throws Exception|InvalidArgumentException
1069
     *
1070
     * @return string the SQL statement for creating a new index.
1071
     */
1072
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
1073
    {
1074
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
1075
            . $this->db->quoteTableName($name) . ' ON '
1076
            . $this->db->quoteTableName($table)
1077
            . ' (' . $this->buildColumns($columns) . ')';
1078
    }
1079
1080
    /**
1081
     * Builds a SQL statement for dropping an index.
1082 12
     *
1083
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
1084 12
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
1085 8
     *
1086
     * @return string the SQL statement for dropping an index.
1087 12
     */
1088 12
    public function dropIndex(string $name, string $table): string
1089
    {
1090
        return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
1091 12
    }
1092 12
1093 12
    /**
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 6
    public function addUnique(string $name, string $table, $columns): string
1107
    {
1108 6
        if (is_string($columns)) {
1109 6
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1110
        }
1111
1112
        foreach ($columns as $i => $col) {
1113
            $columns[$i] = $this->db->quoteColumnName($col);
1114
        }
1115
1116
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1117
            . $this->db->quoteColumnName($name) . ' UNIQUE ('
1118
            . implode(', ', $columns) . ')';
1119
    }
1120
1121
    /**
1122 6
     * Creates a SQL command for dropping an unique constraint.
1123
     *
1124 6
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
1125 6
     * 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
    public function dropUnique(string $name, string $table): string
1132
    {
1133
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1134
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1135
    }
1136
1137
    /**
1138 6
     * Creates a SQL command for adding a check constraint to an existing table.
1139
     *
1140 6
     * @param string $name the name of the check constraint. The name will be properly quoted by the method.
1141 6
     * @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
    public function addCheck(string $name, string $table, string $expression): string
1148
    {
1149
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1150
            . $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
    public function dropCheck(string $name, string $table): string
1164
    {
1165
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1166
            . ' 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 3
     *
1236
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
1237 3
     *
1238 3
     * @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 2
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1253
     * method.
1254 2
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1255
     *
1256
     * @throws Exception|InvalidConfigException
1257
     *
1258
     * @return string the SQL statement for adding comment on column.
1259
     */
1260
    public function addCommentOnColumn(string $table, string $column, string $comment): string
1261
    {
1262
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column)
1263
            . ' IS ' . $this->db->quoteValue($comment);
1264
    }
1265
1266
    /**
1267 1
     * Builds a SQL command for adding comment to table.
1268
     *
1269 1
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1270 1
     * 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|InvalidConfigException
1274
     *
1275
     * @return string the SQL statement for adding comment on table.
1276
     */
1277
    public function addCommentOnTable(string $table, string $comment): string
1278
    {
1279
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS ' . $this->db->quoteValue($comment);
1280
    }
1281 1
1282
    /**
1283 1
     * 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
    public function dropCommentFromColumn(string $table, string $column): string
1293
    {
1294
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column)
1295
            . ' IS NULL';
1296
    }
1297
1298 5
    /**
1299
     * Builds a SQL command for adding comment to table.
1300 5
     *
1301 5
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1302 5
     * method.
1303
     *
1304 5
     * @return string the SQL statement for adding comment on column.
1305 5
     */
1306 5
    public function dropCommentFromTable(string $table): string
1307
    {
1308 5
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS NULL';
1309
    }
1310
1311 5
    /**
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 5
     * @return string the `CREATE VIEW` SQL statement.
1322
     */
1323 5
    public function createView(string $viewName, $subQuery): string
1324
    {
1325
        if ($subQuery instanceof Query) {
1326
            /** @psalm-var array<array-key, int|string> $params */
1327
            [$rawQuery, $params] = $this->build($subQuery);
1328
1329
            foreach ($params as $key => $value) {
1330
                $params[$key] = $this->db->quoteValue($value);
1331
            }
1332
1333
            $subQuery = strtr($rawQuery, $params);
1334
        }
1335
1336
        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
    public function dropView(string $viewName): string
1347
    {
1348
        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 66
     * - `decimal`: decimal number type, will be converted into "decimal"
1373
     * - `datetime`: datetime type, will be converted into "datetime"
1374 66
     * - `timestamp`: timestamp type, will be converted into "timestamp"
1375 11
     * - `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 66
     * - `binary`: binary data type, will be converted into "blob"
1379 41
     *
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 40
     *
1383 20
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
1384 17
     *
1385
     * For some of the abstract types you can also specify a length or precision constraint by appending it in round
1386 17
     * brackets directly to the type.
1387 17
     *
1388 20
     * 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 33
     *
1391 32
     * If a type cannot be found in {@see typeMap}, it will be returned without any change.
1392 32
     *
1393
     * @param ColumnSchemaBuilder|string $type abstract column type.
1394
     *
1395
     * @return string physical column type.
1396 5
     */
1397
    public function getColumnType($type): string
1398
    {
1399
        if ($type instanceof ColumnSchemaBuilder) {
1400
            $type = $type->__toString();
1401
        }
1402
1403
        if (isset($this->typeMap[$type])) {
1404
            return $this->typeMap[$type];
1405
        }
1406
1407
        if (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
1408
            if (isset($this->typeMap[$matches[1]])) {
1409 1507
                return preg_replace(
1410
                    '/\(.+\)/',
1411
                    '(' . $matches[2] . ')',
1412
                    $this->typeMap[$matches[1]]
1413
                ) . $matches[3];
1414
            }
1415 1507
        } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
1416
            if (isset($this->typeMap[$matches[1]])) {
1417 1507
                return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
1418
            }
1419
        }
1420
1421 1507
        return $type;
1422 1286
    }
1423
1424
    /**
1425 434
     * @param array $columns
1426 434
     * @param array $params the binding parameters to be populated.
1427 73
     * @param bool|null $distinct
1428 10
     * @param string|null $selectOption
1429
     *
1430 73
     * @psalm-param array<array-key,ExpressionInterface|Query|string> $columns
1431
     *
1432 416
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
1433
     *
1434
     * @return string the SELECT clause built from {@see Query::$select}.
1435 416
     */
1436 19
    public function buildSelect(
1437 14
        array $columns,
1438
        array &$params,
1439 19
        ?bool $distinct = false,
1440 412
        string $selectOption = null
1441 317
    ): string {
1442
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
1443
1444
        if ($selectOption !== null) {
1445
            $select .= ' ' . $selectOption;
1446 317
        }
1447
1448
        if (empty($columns)) {
1449
            return $select . ' *';
1450
        }
1451 434
1452
        foreach ($columns as $i => $column) {
1453
            if ($column instanceof ExpressionInterface) {
1454
                if (is_int($i)) {
1455
                    $columns[$i] = $this->buildExpression($column, $params);
1456
                } else {
1457
                    $columns[$i] = $this->buildExpression($column, $params) . ' AS ' . $this->db->quoteColumnName($i);
1458
                }
1459
            } elseif ($column instanceof Query) {
1460
                [$sql, $params] = $this->build($column, $params);
1461
                $columns[$i] = "($sql) AS " . $this->db->quoteColumnName((string) $i);
1462 1527
            } elseif (is_string($i) && $i !== $column) {
1463
                if (strpos($column, '(') === false) {
1464 1527
                    $column = $this->db->quoteColumnName($column);
1465 668
                }
1466
                $columns[$i] = "$column AS " . $this->db->quoteColumnName($i);
1467
            } elseif (strpos($column, '(') === false) {
1468 921
                if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $column, $matches)) {
1469
                    $columns[$i] = $this->db->quoteColumnName(
1470 921
                        $matches[1]
1471
                    ) . ' AS ' . $this->db->quoteColumnName($matches[2]);
1472
                } else {
1473
                    $columns[$i] = $this->db->quoteColumnName($column);
1474
                }
1475
            }
1476
        }
1477
1478
        return $select . ' ' . implode(', ', $columns);
1479
    }
1480
1481 1507
    /**
1482
     * @param array|null $tables
1483 1507
     * @param array $params the binding parameters to be populated.
1484 1502
     *
1485
     * @psalm-param array<array-key,array|Query|string> $tables
1486
     *
1487 110
     * @throws Exception|InvalidConfigException|NotSupportedException
1488 110
     *
1489
     * @return string the FROM clause built from {@see Query::$from}.
1490
     */
1491
    public function buildFrom(?array $tables, array &$params): string
1492
    {
1493
        if (empty($tables)) {
1494
            return '';
1495
        }
1496 110
1497
        $tables = $this->quoteTableNames($tables, $params);
1498 110
1499 110
        return 'FROM ' . implode(', ', $tables);
1500 110
    }
1501
1502 110
    /**
1503 110
     * @param array $joins
1504 110
     * @param array $params the binding parameters to be populated.
1505 110
     *
1506
     * @psalm-param array<
1507
     *   array-key,
1508
     *   array{
1509
     *     0?:string,
1510 110
     *     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
    public function buildJoin(array $joins, array &$params): string
1520
    {
1521
        if (empty($joins)) {
1522
            return '';
1523 921
        }
1524
1525 921
        foreach ($joins as $i => $join) {
1526 921
            if (!is_array($join) || !isset($join[0], $join[1])) {
1527 16
                throw new Exception(
1528 16
                    'A join clause must be specified as an array of join type, join table, and optionally join '
1529 921
                    . 'condition.'
1530 58
                );
1531 40
            }
1532
1533 58
            /* 0:join type, 1:join table, 2:on-condition (optional) */
1534 898
            [$joinType, $table] = $join;
1535 887
1536 887
            $tables = $this->quoteTableNames((array) $table, $params);
1537 50
1538 50
            /** @var string $table */
1539
            $table = reset($tables);
1540 847
            $joins[$i] = "$joinType $table";
1541
1542
            if (isset($join[2])) {
1543
                $condition = $this->buildCondition($join[2], $params);
1544
                if ($condition !== '') {
1545
                    $joins[$i] .= ' ON ' . $condition;
1546 921
                }
1547
            }
1548
        }
1549
1550
        return implode($this->separator, $joins);
1551
    }
1552
1553
    /**
1554
     * Quotes table names passed.
1555
     *
1556
     * @param array $tables
1557 1533
     * @param array $params
1558
     *
1559 1533
     * @psalm-param array<array-key,array|Query|string> $tables
1560
     *
1561 1533
     * @throws Exception|InvalidConfigException|NotSupportedException
1562
     *
1563
     * @return array
1564
     */
1565
    private function quoteTableNames(array $tables, array &$params): array
1566
    {
1567
        foreach ($tables as $i => $table) {
1568
            if ($table instanceof Query) {
1569
                [$sql, $params] = $this->build($table, $params);
1570
                $tables[$i] = "($sql) " . $this->db->quoteTableName((string) $i);
1571
            } elseif (is_string($table) && is_string($i)) {
1572 1507
                if (strpos($table, '(') === false) {
1573
                    $table = $this->db->quoteTableName($table);
1574 1507
                }
1575 1497
                $tables[$i] = "$table " . $this->db->quoteTableName($i);
1576
            } elseif (is_string($table) && strpos($table, '(') === false) {
1577 15
                $tableWithAlias = $this->extractAlias($table);
1578 15
                if (is_array($tableWithAlias)) { // with alias
1579 5
                    $tables[$i] = $this->db->quoteTableName($tableWithAlias[1]) . ' '
1580 5
                        . $this->db->quoteTableName($tableWithAlias[2]);
1581 15
                } else {
1582 15
                    $tables[$i] = $this->db->quoteTableName($table);
1583
                }
1584
            }
1585
        }
1586 15
1587
        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 1507
     */
1598
    public function buildWhere($condition, array &$params = []): string
1599 1507
    {
1600
        $where = $this->buildCondition($condition, $params);
1601 1507
1602
        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
    public function buildGroupBy(array $columns, array &$params = []): string
1616
    {
1617
        if (empty($columns)) {
1618 940
            return '';
1619
        }
1620
1621
        foreach ($columns as $i => $column) {
1622
            if ($column instanceof Expression) {
1623
                $columns[$i] = $this->buildExpression($column);
1624
                $params = array_merge($params, $column->getParams());
1625 940
            } elseif (strpos($column, '(') === false) {
1626 940
                $columns[$i] = $this->db->quoteColumnName($column);
1627 138
            }
1628
        }
1629 940
1630 940
        return 'GROUP BY ' . implode(', ', $columns);
1631 36
    }
1632
1633
    /**
1634 940
     * @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
    public function buildHaving($condition, array &$params = []): string
1642
    {
1643
        $having = $this->buildCondition($condition, $params);
1644
1645 1519
        return ($having === '') ? '' : ('HAVING ' . $having);
1646
    }
1647 1519
1648 1464
    /**
1649
     * Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
1650
     *
1651 229
     * @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 229
     * parameter.
1654 229
     * @psalm-param array<string, Expression|int|string> $orderBy
1655 5
     *
1656 5
     * @param Expression|int|null $limit the limit number. See {@see Query::limit} for more details.
1657
     * @param Expression|int|null $offset the offset number. See {@see Query::offset} for more details.
1658 229
     * @param array $params the binding parameters to be populated.
1659
     *
1660
     * @throws Exception|InvalidArgumentException
1661
     *
1662 229
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any).
1663
     */
1664
    public function buildOrderByAndLimit(
1665
        string $sql,
1666
        array $orderBy,
1667
        $limit,
1668
        $offset,
1669
        array &$params = []
1670
    ): string {
1671 346
        $orderBy = $this->buildOrderBy($orderBy, $params);
1672
        if ($orderBy !== '') {
1673 346
            $sql .= $this->separator . $orderBy;
1674
        }
1675 346
        $limit = $this->buildLimit($limit, $offset);
1676 11
        if ($limit !== '') {
1677
            $sql .= $this->separator . $limit;
1678
        }
1679 346
1680 3
        return $sql;
1681
    }
1682
1683 346
    /**
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 1216
    public function buildOrderBy(array $columns, array &$params = []): string
1694
    {
1695 1216
        if (empty($columns)) {
1696
            return '';
1697
        }
1698
1699
        $orders = [];
1700
1701
        foreach ($columns as $name => $direction) {
1702
            if ($direction instanceof Expression) {
1703
                $orders[] = $this->buildExpression($direction);
1704
                $params = array_merge($params, $direction->getParams());
1705 1216
            } else {
1706
                $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
1707 1216
            }
1708
        }
1709
1710
        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 1219
     */
1719
    public function buildLimit($limit, $offset): string
1720 1219
    {
1721 1219
        $sql = '';
1722
1723
        if ($this->hasLimit($limit)) {
1724 9
            $sql = 'LIMIT ' . (string) $limit;
1725
        }
1726 9
1727 9
        if ($this->hasOffset($offset)) {
1728 9
            $sql .= ' OFFSET ' . (string) $offset;
1729 9
        }
1730
1731
        return ltrim($sql);
1732 9
    }
1733
1734
    /**
1735 9
     * 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
    protected function hasLimit($limit): bool
1742
    {
1743
        return ($limit instanceof ExpressionInterface) || ctype_digit((string) $limit);
1744
    }
1745
1746
    /**
1747
     * Checks to see if the given offset is effective.
1748
     *
1749 41
     * @param mixed $offset the given offset.
1750
     *
1751 41
     * @return bool whether the offset is effective.
1752 31
     */
1753
    protected function hasOffset($offset): bool
1754
    {
1755
        return ($offset instanceof ExpressionInterface) || (ctype_digit((string)$offset) && (string)$offset !== '0');
1756 31
    }
1757 31
1758
    /**
1759 31
     * @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 41
     *
1764 41
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
1765
     *
1766 41
     * @return string the UNION clause built from {@see Query::$union}.
1767 41
     */
1768
    public function buildUnion(array $unions, array &$params): string
1769
    {
1770
        if (empty($unions)) {
1771 41
            return '';
1772
        }
1773
1774
        $result = '';
1775
1776
        foreach ($unions as $i => $union) {
1777
            $query = $union['query'];
1778
            if ($query instanceof Query) {
1779
                [$unions[$i]['query'], $params] = $this->build($query, $params);
1780
            }
1781
1782
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
1783
        }
1784
1785 1537
        return trim($result);
1786
    }
1787 1537
1788 1172
    /**
1789 5
     * Processes columns and properly quotes them if necessary.
1790
     *
1791
     * It will join all columns into a string with comma as separators.
1792 1172
     *
1793
     * @param array|string $columns the columns to be processed.
1794
     *
1795 1537
     * @psalm-param array<array-key, ExpressionInterface|string>|string $columns
1796 1277
     *
1797
     * @throws Exception|InvalidArgumentException
1798
     *
1799 1513
     * @return string the processing result.
1800
     */
1801
    public function buildColumns($columns): string
1802
    {
1803
        if (!is_array($columns)) {
1804
            if (strpos($columns, '(') !== false) {
1805
                return $columns;
1806
            }
1807
1808
            $rawColumns = $columns;
1809
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1810
1811
            if ($columns === false) {
1812
                throw new InvalidArgumentException("$rawColumns is not valid columns.");
1813 1172
            }
1814
        }
1815
        foreach ($columns as $i => $column) {
1816 1172
            if ($column instanceof ExpressionInterface) {
1817 820
                $columns[$i] = $this->buildExpression($column);
1818
            } elseif (strpos($column, '(') === false) {
1819 820
                $columns[$i] = $this->db->quoteColumnName($column);
1820
            }
1821
        }
1822 820
1823
        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 619
    /**
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 9
     */
1837
    public function buildCondition($condition, array &$params = []): string
1838 9
    {
1839
        if (is_array($condition)) {
1840
            if (empty($condition)) {
1841
                return '';
1842
            }
1843
1844
            $condition = $this->createConditionFromArray($condition);
1845
        }
1846
1847
        if ($condition instanceof ExpressionInterface) {
1848
            return $this->buildExpression($condition, $params);
1849 1186
        }
1850
1851 1186
        return $condition ?? '';
1852 1186
    }
1853
1854 1186
    /**
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 887
     */
1865
    public function createConditionFromArray(array $condition): ConditionInterface
1866 887
    {
1867 50
        /** operator format: operator, operand 1, operand 2, ... */
1868
        if (isset($condition[0])) {
1869
            $operator = strtoupper((string) array_shift($condition));
1870 847
1871
            $className = $this->conditionClasses[$operator] ?? SimpleCondition::class;
1872
1873
            /** @var ConditionInterface $className */
1874
            return $className::fromArrayDefinition($operator, $condition);
1875
        }
1876
1877
        /** hash format: 'column1' => 'value1', 'column2' => 'value2', ... */
1878
        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
    public function selectExists(string $rawSql): string
1889
    {
1890
        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 1507
     * @return string the placeholder name in $params array.
1900
     */
1901 1507
    public function bindParam($value, array &$params = []): string
1902 1507
    {
1903
        $phName = self::PARAM_PREFIX . count($params);
1904
1905 10
        /** @psalm-var mixed */
1906 10
        $params[$phName] = $value;
1907
1908 10
        return $phName;
1909 10
    }
1910 5
1911
    /**
1912
     * Extracts table alias if there is one or returns false.
1913 10
     *
1914 10
     * @param $table
1915 10
     *
1916
     * @return array|bool
1917
     *
1918 10
     * @psalm-return array<array-key, string>|bool
1919
     */
1920
    protected function extractAlias(string $table)
1921 10
    {
1922
        if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) {
1923
            return $matches;
1924 1332
        }
1925
1926 1332
        return false;
1927
    }
1928
1929
    /**
1930
     * @psalm-param array<array-key, array{query:string|Query, alias:string, recursive:bool}> $withs
1931
     */
1932
    public function buildWithQueries(array $withs, array &$params): string
1933
    {
1934 5
        if (empty($withs)) {
1935
            return '';
1936 5
        }
1937 5
1938
        $recursive = false;
1939
        $result = [];
1940
1941
        foreach ($withs as $i => $with) {
1942
            if ($with['recursive']) {
1943
                $recursive = true;
1944
            }
1945
1946
            $query = $with['query'];
1947
            if ($query instanceof Query) {
1948
                [$with['query'], $params] = $this->build($query, $params);
1949
            }
1950
1951
            $result[] = $with['alias'] . ' AS (' . $with['query'] . ')';
1952
        }
1953
1954
        return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . implode(', ', $result);
1955
    }
1956
1957
    public function getDb(): Connection
1958
    {
1959
        return $this->db;
1960
    }
1961
1962
    /**
1963
     * @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...
1964
     *
1965
     * Defaults to an empty space. This is mainly used by {@see build()} when generating a SQL statement.
1966
     */
1967
    public function setSeparator(string $separator): void
1968
    {
1969
        $this->separator = $separator;
1970
    }
1971
}
1972