Passed
Push — master ( f4eca7...1ebc33 )
by Def
10:04
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\ConnectionInterface;
10
use Yiisoft\Db\Constraint\Constraint;
11
use Yiisoft\Db\Exception\Exception;
12
use Yiisoft\Db\Exception\InvalidArgumentException;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
use Yiisoft\Db\Expression\Expression;
16
use Yiisoft\Db\Expression\ExpressionBuilder;
17
use Yiisoft\Db\Expression\ExpressionBuilderInterface;
18
use Yiisoft\Db\Expression\ExpressionInterface;
19
use Yiisoft\Db\Pdo\PdoValue;
20
use Yiisoft\Db\Pdo\PdoValueBuilder;
21
use Yiisoft\Db\Query\Conditions\ConditionInterface;
22
use Yiisoft\Db\Query\Conditions\HashCondition;
23
use Yiisoft\Db\Query\Conditions\SimpleCondition;
24
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
25
use Yiisoft\Db\Schema\Schema;
26
use Yiisoft\Strings\NumericHelper;
27
28
use function array_combine;
29
use function array_diff;
30
use function array_filter;
31
use function array_keys;
32
use function array_map;
33
use function array_merge;
34
use function array_reverse;
35
use function array_shift;
36
use function array_unique;
37
use function array_values;
38
use function count;
39
use function ctype_digit;
40
use function get_class;
41
use function implode;
42
use function in_array;
43
use function is_array;
44
use function is_int;
45
use function is_object;
46
use function is_string;
47
use function is_subclass_of;
48
use function json_encode;
49
use function ltrim;
50
use function preg_match;
51
use function preg_replace;
52
use function preg_split;
53
use function reset;
54
use function strpos;
55
use function strtoupper;
56
use function strtr;
57
use function trim;
58
59
/**
60
 * QueryBuilder builds a SELECT SQL statement based on the specification given as a {@see Query} object.
61
 *
62
 * SQL statements are created from {@see Query} objects using the {@see build()}-method.
63
 *
64
 * QueryBuilder is also used by {@see Command} to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE.
65
 *
66
 * For more details and usage information on QueryBuilder:
67
 * {@see [guide article on query builders](guide:db-query-builder)}.
68
 *
69
 * @property string[] $conditionClasses Map of condition aliases to condition classes. This property is write-only.
70
 *
71
 * For example:
72
 * ```php
73
 *     ['LIKE' => \Yiisoft\Db\Condition\LikeCondition::class]
74
 * ```
75
 * @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones in
76
 * {@see expressionBuilders} property. This property is write-only.
77
 */
78
class QueryBuilder
79
{
80
    /**
81
     * The prefix for automatically generated query binding parameters.
82
     */
83
    public const PARAM_PREFIX = ':qp';
84
85
    /**
86
     * @var array the abstract column types mapped to physical column types.
87
     * This is mainly used to support creating/modifying tables using DB-independent data type specifications.
88
     * Child classes should override this property to declare supported type mappings.
89
     *
90
     * @psalm-var array<string, string>
91
     */
92
    protected array $typeMap = [];
93
94
    /**
95
     * @var array map of condition aliases to condition classes. For example:
96
     *
97
     * ```php
98
     * return [
99
     *     'LIKE' => \Yiisoft\Db\Condition\LikeCondition::class,
100
     * ];
101
     * ```
102
     *
103
     * This property is used by {@see createConditionFromArray} method.
104
     * See default condition classes list in {@see defaultConditionClasses()} method.
105
     *
106
     * In case you want to add custom conditions support, use the {@see setConditionClasses()} method.
107
     *
108
     * @see setConditonClasses()
109
     * @see defaultConditionClasses()
110
     */
111
    protected array $conditionClasses = [];
112
113
    /**
114
     * @var ExpressionBuilderInterface[]|string[] maps expression class to expression builder class.
115
     * For example:
116
     *
117
     * ```php
118
     * [
119
     *    Expression::class => ExpressionBuilder::class
120
     * ]
121
     * ```
122
     * This property is mainly used by {@see buildExpression()} to build SQL expressions form expression objects.
123
     * See default values in {@see defaultExpressionBuilders()} method.
124
     *
125
     * @see setExpressionBuilders()
126
     * @see defaultExpressionBuilders()
127
     */
128
    protected array $expressionBuilders = [];
129
    protected string $separator = ' ';
130
    private ConnectionInterface $db;
131
132 1960
    public function __construct(ConnectionInterface $db)
133
    {
134 1960
        $this->db = $db;
135 1960
        $this->expressionBuilders = $this->defaultExpressionBuilders();
136 1960
        $this->conditionClasses = $this->defaultConditionClasses();
137 1960
    }
138
139
    /**
140
     * Contains array of default condition classes. Extend this method, if you want to change default condition classes
141
     * for the query builder.
142
     *
143
     * @return array
144
     *
145
     * See {@see conditionClasses} docs for details.
146
     */
147 1960
    protected function defaultConditionClasses(): array
148
    {
149
        return [
150 1960
            'NOT' => Conditions\NotCondition::class,
151
            'AND' => Conditions\AndCondition::class,
152
            'OR' => Conditions\OrCondition::class,
153
            'BETWEEN' => Conditions\BetweenCondition::class,
154
            'NOT BETWEEN' => Conditions\BetweenCondition::class,
155
            'IN' => Conditions\InCondition::class,
156
            'NOT IN' => Conditions\InCondition::class,
157
            'LIKE' => Conditions\LikeCondition::class,
158
            'NOT LIKE' => Conditions\LikeCondition::class,
159
            'OR LIKE' => Conditions\LikeCondition::class,
160
            'OR NOT LIKE' => Conditions\LikeCondition::class,
161
            'EXISTS' => Conditions\ExistsCondition::class,
162
            'NOT EXISTS' => Conditions\ExistsCondition::class,
163
        ];
164
    }
165
166
    /**
167
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
168
     * expression builders for this query builder.
169
     *
170
     * @return array
171
     *
172
     * See {@see expressionBuilders} docs for details.
173
     */
174 1960
    protected function defaultExpressionBuilders(): array
175
    {
176
        return [
177 1960
            Query::class => QueryExpressionBuilder::class,
178
            PdoValue::class => PdoValueBuilder::class,
179
            Expression::class => ExpressionBuilder::class,
180
            Conditions\ConjunctionCondition::class => Conditions\ConjunctionConditionBuilder::class,
181
            Conditions\NotCondition::class => Conditions\NotConditionBuilder::class,
182
            Conditions\AndCondition::class => Conditions\ConjunctionConditionBuilder::class,
183
            Conditions\OrCondition::class => Conditions\ConjunctionConditionBuilder::class,
184
            Conditions\BetweenCondition::class => Conditions\BetweenConditionBuilder::class,
185
            Conditions\InCondition::class => Conditions\InConditionBuilder::class,
186
            Conditions\LikeCondition::class => Conditions\LikeConditionBuilder::class,
187
            Conditions\ExistsCondition::class => Conditions\ExistsConditionBuilder::class,
188
            Conditions\SimpleCondition::class => Conditions\SimpleConditionBuilder::class,
189
            Conditions\HashCondition::class => Conditions\HashConditionBuilder::class,
190
            Conditions\BetweenColumnsCondition::class => Conditions\BetweenColumnsConditionBuilder::class,
191
        ];
192
    }
193
194
    /**
195
     * Setter for {@see expressionBuilders property.
196
     *
197
     * @param string[] $builders array of builders that should be merged with the pre-defined ones in property.
198
     *
199
     * See {@see expressionBuilders} docs for details.
200
     */
201
    public function setExpressionBuilders(array $builders): void
202
    {
203
        $this->expressionBuilders = array_merge($this->expressionBuilders, $builders);
204
    }
205
206
    /**
207
     * Setter for {@see conditionClasses} property.
208
     *
209
     * @param string[] $classes map of condition aliases to condition classes. For example:
210
     *
211
     * ```php
212
     * ['LIKE' => \Yiisoft\Db\Condition\LikeCondition::class]
213
     * ```
214
     *
215
     * See {@see conditionClasses} docs for details.
216
     */
217
    public function setConditionClasses(array $classes): void
218
    {
219
        $this->conditionClasses = array_merge($this->conditionClasses, $classes);
220
    }
221
222
    /**
223
     * Generates a SELECT SQL statement from a {@see Query} object.
224
     *
225
     * @param Query $query the {@see Query} object from which the SQL statement will be generated.
226
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
227
     * in the result with the additional parameters generated during the query building process.
228
     *
229
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
230
     *
231
     * @return array the generated SQL statement (the first array element) and the corresponding parameters to be bound
232
     * to the SQL statement (the second array element). The parameters returned include those provided in `$params`.
233
     *
234
     * @psalm-return array{0: string, 1: array}
235
     */
236 1234
    public function build(Query $query, array $params = []): array
237
    {
238 1234
        $query = $query->prepare($this);
239
240 1234
        $params = empty($params) ? $query->getParams() : array_merge($params, $query->getParams());
241
242
        $clauses = [
243 1234
            $this->buildSelect($query->getSelect(), $params, $query->getDistinct(), $query->getSelectOption()),
244 1234
            $this->buildFrom($query->getFrom(), $params),
245 1234
            $this->buildJoin($query->getJoin(), $params),
246 1234
            $this->buildWhere($query->getWhere(), $params),
247 1234
            $this->buildGroupBy($query->getGroupBy(), $params),
248 1234
            $this->buildHaving($query->getHaving(), $params),
249
        ];
250
251 1234
        $sql = implode($this->separator, array_filter($clauses));
252
253 1234
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
254
255 1234
        if (!empty($query->getOrderBy())) {
256 180
            foreach ($query->getOrderBy() as $expression) {
257 180
                if ($expression instanceof ExpressionInterface) {
258 4
                    $this->buildExpression($expression, $params);
259
                }
260
            }
261
        }
262
263 1234
        if (!empty($query->getGroupBy())) {
264 12
            foreach ($query->getGroupBy() as $expression) {
265 12
                if ($expression instanceof ExpressionInterface) {
266 4
                    $this->buildExpression($expression, $params);
267
                }
268
            }
269
        }
270
271 1234
        $union = $this->buildUnion($query->getUnion(), $params);
272
273 1234
        if ($union !== '') {
274 9
            $sql = "($sql){$this->separator}$union";
275
        }
276
277 1234
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
278
279 1234
        if ($with !== '') {
280 8
            $sql = "$with{$this->separator}$sql";
281
        }
282
283 1234
        return [$sql, $params];
284
    }
285
286
    /**
287
     * Builds given $expression.
288
     *
289
     * @param ExpressionInterface $expression the expression to be built
290
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
291
     * in the result with the additional parameters generated during the expression building process.
292
     *
293
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException when $expression building
294
     * is not supported by this QueryBuilder.
295
     *
296
     * @return string the SQL statement that will not be neither quoted nor encoded before passing to DBMS.
297
     *
298
     * @see ExpressionInterface
299
     * @see ExpressionBuilderInterface
300
     * @see expressionBuilders
301
     */
302 1376
    public function buildExpression(ExpressionInterface $expression, array &$params = []): string
303
    {
304 1376
        $builder = $this->getExpressionBuilder($expression);
305
306 1376
        return (string) $builder->build($expression, $params);
307
    }
308
309
    /**
310
     * Gets object of {@see ExpressionBuilderInterface} that is suitable for $expression.
311
     *
312
     * Uses {@see expressionBuilders} array to find a suitable builder class.
313
     *
314
     * @param ExpressionInterface $expression
315
     *
316
     * @throws InvalidArgumentException when $expression building is not supported by this QueryBuilder.
317
     *
318
     * @return ExpressionBuilderInterface|QueryBuilder|string
319
     *
320
     * @see expressionBuilders
321
     */
322 1376
    public function getExpressionBuilder(ExpressionInterface $expression)
323
    {
324 1376
        $className = get_class($expression);
325
326 1376
        if (!isset($this->expressionBuilders[$className])) {
327
            foreach (array_reverse($this->expressionBuilders) as $expressionClass => $builderClass) {
328
                if (is_subclass_of($expression, $expressionClass)) {
329
                    $this->expressionBuilders[$className] = $builderClass;
330
                    break;
331
                }
332
            }
333
334
            if (!isset($this->expressionBuilders[$className])) {
335
                throw new InvalidArgumentException(
336
                    'Expression of class ' . $className . ' can not be built in ' . static::class
337
                );
338
            }
339
        }
340
341 1376
        if ($this->expressionBuilders[$className] === __CLASS__) {
342
            return $this;
343
        }
344
345 1376
        if (!is_object($this->expressionBuilders[$className])) {
0 ignored issues
show
introduced by
The condition is_object($this->expressionBuilders[$className]) is always false.
Loading history...
346 1376
            $this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this);
347
        }
348
349 1376
        return $this->expressionBuilders[$className];
350
    }
351
352
    /**
353
     * Creates an INSERT SQL statement.
354
     *
355
     * For example,.
356
     *
357
     * ```php
358
     * $sql = $queryBuilder->insert('user', [
359
     *     'name' => 'Sam',
360
     *     'age' => 30,
361
     * ], $params);
362
     * ```
363
     *
364
     * The method will properly escape the table and column names.
365
     *
366
     * @param string $table the table that new rows will be inserted into.
367
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
368
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of {@see Query}.
369
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
370
     * DB command later.
371
     *
372
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
373
     *
374
     * @return string the INSERT SQL.
375
     */
376 209
    public function insert(string $table, $columns, array &$params = []): string
377
    {
378 209
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
379
380 197
        return 'INSERT INTO ' . $this->db->quoteTableName($table)
0 ignored issues
show
Bug introduced by
The method quoteTableName() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

380
        return 'INSERT INTO ' . $this->db->/** @scrutinizer ignore-call */ quoteTableName($table)
Loading history...
381 197
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
382 197
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
383
    }
384
385
    /**
386
     * Prepares a `VALUES` part for an `INSERT` SQL statement.
387
     *
388
     * @param string $table the table that new rows will be inserted into.
389
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
390
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement.
391
     * @param array $params the binding parameters that will be generated by this method.
392
     * They should be bound to the DB command later.
393
     *
394
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
395
     *
396
     * @return array array of column names, placeholders, values and params.
397
     */
398 298
    protected function prepareInsertValues(string $table, $columns, array $params = []): array
399
    {
400 298
        $schema = $this->db->getSchema();
401 298
        $tableSchema = $schema->getTableSchema($table);
402 298
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
403 298
        $names = [];
404 298
        $placeholders = [];
405 298
        $values = ' DEFAULT VALUES';
406
407 298
        if ($columns instanceof Query) {
408 69
            [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
409
        } else {
410 238
            foreach ($columns as $name => $value) {
411 233
                $names[] = $schema->quoteColumnName($name);
412 233
                $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
413
414 233
                if ($value instanceof ExpressionInterface) {
415 51
                    $placeholders[] = $this->buildExpression($value, $params);
416 227
                } elseif ($value instanceof Query) {
417
                    [$sql, $params] = $this->build($value, $params);
418
                    $placeholders[] = "($sql)";
419
                } else {
420 227
                    $placeholders[] = $this->bindParam($value, $params);
421
                }
422
            }
423
        }
424
425 283
        return [$names, $placeholders, $values, $params];
426
    }
427
428
    /**
429
     * Prepare select-subquery and field names for INSERT INTO ... SELECT SQL statement.
430
     *
431
     * @param Query $columns Object, which represents select query.
432
     * @param Schema $schema Schema object to quote column name.
433
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
434
     * in the result with the additional parameters generated during the query building process.
435
     *
436
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
437
     *
438
     * @return array array of column names, values and params.
439
     */
440 69
    protected function prepareInsertSelectSubQuery(Query $columns, Schema $schema, array $params = []): array
441
    {
442
        if (
443 69
            !is_array($columns->getSelect())
0 ignored issues
show
introduced by
The condition is_array($columns->getSelect()) is always true.
Loading history...
444 69
            || empty($columns->getSelect())
445 69
            || in_array('*', $columns->getSelect(), true)
446
        ) {
447 15
            throw new InvalidArgumentException('Expected select query object with enumerated (named) parameters');
448
        }
449
450 54
        [$values, $params] = $this->build($columns, $params);
451
452 54
        $names = [];
453 54
        $values = ' ' . $values;
454
455 54
        foreach ($columns->getSelect() as $title => $field) {
456 54
            if (is_string($title)) {
457 54
                $names[] = $schema->quoteColumnName($title);
458
            } elseif (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $field, $matches)) {
459
                $names[] = $schema->quoteColumnName($matches[2]);
460
            } else {
461
                $names[] = $schema->quoteColumnName($field);
462
            }
463
        }
464
465 54
        return [$names, $values, $params];
466
    }
467
468
    /**
469
     * Generates a batch INSERT SQL statement.
470
     *
471
     * For example,
472
     *
473
     * ```php
474
     * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
475
     *     ['Tom', 30],
476
     *     ['Jane', 20],
477
     *     ['Linda', 25],
478
     * ]);
479
     * ```
480
     *
481
     * Note that the values in each row must match the corresponding column names.
482
     *
483
     * The method will properly escape the column names, and quote the values to be inserted.
484
     *
485
     * @param string $table the table that new rows will be inserted into.
486
     * @param array $columns the column names.
487
     * @param array|Generator $rows the rows to be batch inserted into the table.
488
     * @param array $params the binding parameters. This parameter exists.
489
     *
490
     * @throws Exception|InvalidArgumentException
491
     *
492
     * @return string the batch INSERT SQL statement.
493
     */
494 40
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
495
    {
496 40
        if (empty($rows)) {
497 4
            return '';
498
        }
499
500 38
        $schema = $this->db->getSchema();
501
502
503 38
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
504 38
            $columnSchemas = $tableSchema->getColumns();
505
        } else {
506
            $columnSchemas = [];
507
        }
508
509 38
        $values = [];
510
511 38
        foreach ($rows as $row) {
512 35
            $vs = [];
513 35
            foreach ($row as $i => $value) {
514 35
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
515 26
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
516
                }
517 35
                if (is_string($value)) {
518 23
                    $value = $schema->quoteValue($value);
519 21
                } elseif (is_float($value)) {
520
                    /* ensure type cast always has . as decimal separator in all locales */
521 2
                    $value = NumericHelper::normalize((string) $value);
522 21
                } elseif ($value === false) {
523 7
                    $value = 0;
524 21
                } elseif ($value === null) {
525 12
                    $value = 'NULL';
526 13
                } elseif ($value instanceof ExpressionInterface) {
527 9
                    $value = $this->buildExpression($value, $params);
528
                }
529 35
                $vs[] = $value;
530
            }
531 35
            $values[] = '(' . implode(', ', $vs) . ')';
532
        }
533
534 38
        if (empty($values)) {
535 3
            return '';
536
        }
537
538 35
        foreach ($columns as $i => $name) {
539 32
            $columns[$i] = $schema->quoteColumnName($name);
540
        }
541
542 35
        return 'INSERT INTO ' . $schema->quoteTableName($table)
543 35
            . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
544
    }
545
546
    /**
547
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
548
     * constraints), or update them if they do.
549
     *
550
     * For example,
551
     *
552
     * ```php
553
     * $sql = $queryBuilder->upsert('pages', [
554
     *     'name' => 'Front page',
555
     *     'url' => 'http://example.com/', // url is unique
556
     *     'visits' => 0,
557
     * ], [
558
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
559
     * ], $params);
560
     * ```
561
     *
562
     * The method will properly escape the table and column names.
563
     *
564
     * @param string $table the table that new rows will be inserted into/updated in.
565
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
566
     * of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
567
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
568
     * If `true` is passed, the column data will be updated to match the insert column data.
569
     * If `false` is passed, no update will be performed if the column data already exists.
570
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the DB
571
     * command later.
572
     *
573
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
574
     *
575
     * @return string the resulting SQL.
576
     */
577
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
0 ignored issues
show
Unused Code introduced by
The parameter $table 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

577
    public function upsert(/** @scrutinizer ignore-unused */ string $table, $insertColumns, $updateColumns, array &$params): 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...
Unused Code introduced by
The parameter $params 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

577
    public function upsert(string $table, $insertColumns, $updateColumns, /** @scrutinizer ignore-unused */ array &$params): 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...
Unused Code introduced by
The parameter $updateColumns 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

577
    public function upsert(string $table, $insertColumns, /** @scrutinizer ignore-unused */ $updateColumns, array &$params): 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...
Unused Code introduced by
The parameter $insertColumns 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

577
    public function upsert(string $table, /** @scrutinizer ignore-unused */ $insertColumns, $updateColumns, array &$params): 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...
578
    {
579
        throw new NotSupportedException($this->db->getDriverName() . ' does not support upsert statements.');
580
    }
581
582
    /**
583
     * @param string $table
584
     * @param array|Query $insertColumns
585
     * @param array|bool $updateColumns
586
     * @param Constraint[] $constraints this parameter recieves a matched constraint list.
587
     * The constraints will be unique by their column names.
588
     *
589
     * @throws Exception|JsonException
590
     *
591
     * @return array
592
     */
593 90
    protected function prepareUpsertColumns(string $table, $insertColumns, $updateColumns, array &$constraints = []): array
594
    {
595 90
        if ($insertColumns instanceof Query) {
596 40
            [$insertNames] = $this->prepareInsertSelectSubQuery($insertColumns, $this->db->getSchema());
597
        } else {
598 50
            $insertNames = array_map([$this->db, 'quoteColumnName'], array_keys($insertColumns));
599
        }
600
601 90
        $uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
602
603 90
        $uniqueNames = array_map([$this->db, 'quoteColumnName'], $uniqueNames);
604
605 90
        if ($updateColumns !== true) {
606 65
            return [$uniqueNames, $insertNames, null];
607
        }
608
609 25
        return [$uniqueNames, $insertNames, array_diff($insertNames, $uniqueNames)];
610
    }
611
612
    /**
613
     * Returns all column names belonging to constraints enforcing uniqueness (`PRIMARY KEY`, `UNIQUE INDEX`, etc.)
614
     * for the named table removing constraints which did not cover the specified column list.
615
     *
616
     * The column list will be unique by column names.
617
     *
618
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
619
     * @param string[] $columns source column list.
620
     * @param Constraint[] $constraints this parameter optionally recieves a matched constraint list. The constraints
621
     * will be unique by their column names.
622
     *
623
     * @throws JsonException
624
     *
625
     * @return array column list.
626
     */
627 90
    private function getTableUniqueColumnNames(string $name, array $columns, array &$constraints = []): array
628
    {
629 90
        $schema = $this->db->getSchema();
630
631 90
        $constraints = [];
632 90
        $primaryKey = $schema->getTablePrimaryKey($name);
633
634 90
        if ($primaryKey !== null) {
635 88
            $constraints[] = $primaryKey;
636
        }
637
638 90
        foreach ($schema->getTableIndexes($name) as $constraint) {
639 89
            if ($constraint->isUnique()) {
640 89
                $constraints[] = $constraint;
641
            }
642
        }
643
644 90
        $constraints = array_merge($constraints, $schema->getTableUniques($name));
645
646
        /** Remove duplicates */
647 90
        $constraints = array_combine(
648 90
            array_map(
649 90
                static function ($constraint) {
650 90
                    $columns = $constraint->getColumnNames();
651 90
                    sort($columns, SORT_STRING);
652
653 90
                    return json_encode($columns, JSON_THROW_ON_ERROR);
654
                },
655 90
                $constraints
656
            ),
657 90
            $constraints
658
        );
659
660 90
        $columnNames = [];
661
662
        /** Remove all constraints which do not cover the specified column list */
663 90
        $constraints = array_values(
664 90
            array_filter(
665 90
                $constraints,
666 90
                static function ($constraint) use ($schema, $columns, &$columnNames) {
667
                    /** @psalm-suppress UndefinedClass, UndefinedMethod */
668 90
                    $constraintColumnNames = array_map([$schema, 'quoteColumnName'], $constraint->getColumnNames());
669 90
                    $result = !array_diff($constraintColumnNames, $columns);
670
671 90
                    if ($result) {
672 75
                        $columnNames = array_merge($columnNames, $constraintColumnNames);
673
                    }
674
675 90
                    return $result;
676 90
                }
677
            )
678
        );
679
680 90
        return array_unique($columnNames);
681
    }
682
683
    /**
684
     * Creates an UPDATE SQL statement.
685
     *
686
     * For example,
687
     *
688
     * ```php
689
     * $params = [];
690
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
691
     * ```
692
     *
693
     * The method will properly escape the table and column names.
694
     *
695
     * @param string $table the table to be updated.
696
     * @param array $columns the column data (name => value) to be updated.
697
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
698
     * {@see Query::where()} on how to specify condition.
699
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
700
     * DB command later.
701
     *
702
     * @psalm-param array<string, ExpressionInterface|string> $columns
703
     *
704
     * @throws Exception|InvalidArgumentException
705
     *
706
     * @return string the UPDATE SQL.
707
     */
708 94
    public function update(string $table, array $columns, $condition, array &$params = []): string
709
    {
710
        /**
711
         * @psalm-var array<array-key, mixed> $lines
712
         * @psalm-var array<array-key, mixed> $params
713
         */
714 94
        [$lines, $params] = $this->prepareUpdateSets($table, $columns, $params);
715 94
        $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
716 94
        $where = $this->buildWhere($condition, $params);
717
718 94
        return ($where === '') ? $sql : ($sql . ' ' . $where);
719
    }
720
721
    /**
722
     * Prepares a `SET` parts for an `UPDATE` SQL statement.
723
     *
724
     * @param string $table the table to be updated.
725
     * @param array $columns the column data (name => value) to be updated.
726
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
727
     * DB command later.
728
     *
729
     * @psalm-param array<string, ExpressionInterface|string> $columns
730
     *
731
     * @throws Exception|InvalidArgumentException
732
     *
733
     * @return array `SET` parts for an `UPDATE` SQL statement (the first array element) and params (the second array
734
     * element).
735
     */
736 140
    protected function prepareUpdateSets(string $table, array $columns, array $params = []): array
737
    {
738 140
        $tableSchema = $this->db->getTableSchema($table);
739
740 140
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
741
742 140
        $sets = [];
743
744 140
        foreach ($columns as $name => $value) {
745
            /** @psalm-var mixed $value */
746 140
            $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
747 140
            if ($value instanceof ExpressionInterface) {
748 70
                $placeholder = $this->buildExpression($value, $params);
749
            } else {
750 96
                $placeholder = $this->bindParam($value, $params);
751
            }
752
753 140
            $sets[] = $this->db->quoteColumnName($name) . '=' . $placeholder;
754
        }
755
756 140
        return [$sets, $params];
757
    }
758
759
    /**
760
     * Creates a DELETE SQL statement.
761
     *
762
     * For example,
763
     *
764
     * ```php
765
     * $sql = $queryBuilder->delete('user', 'status = 0');
766
     * ```
767
     *
768
     * The method will properly escape the table and column names.
769
     *
770
     * @param string $table the table where the data will be deleted from.
771
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
772
     * {@see Query::where()} on how to specify condition.
773
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
774
     * DB command later.
775
     *
776
     * @throws Exception|InvalidArgumentException
777
     *
778
     * @return string the DELETE SQL.
779
     */
780 47
    public function delete(string $table, $condition, array &$params): string
781
    {
782 47
        $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
783 47
        $where = $this->buildWhere($condition, $params);
784
785 47
        return ($where === '') ? $sql : ($sql . ' ' . $where);
786
    }
787
788
    /**
789
     * Builds a SQL statement for creating a new DB table.
790
     *
791
     * The columns in the new  table should be specified as name-definition pairs (e.g. 'name' => 'string'), where name
792
     * stands for a column name which will be properly quoted by the method, and definition stands for the column type
793
     * which can contain an abstract DB type.
794
     *
795
     * The {@see getColumnType()} method will be invoked to convert any abstract type into a physical one.
796
     *
797
     * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly inserted
798
     * into the generated SQL.
799
     *
800
     * For example,
801
     *
802
     * ```php
803
     * $sql = $queryBuilder->createTable('user', [
804
     *  'id' => 'pk',
805
     *  'name' => 'string',
806
     *  'age' => 'integer',
807
     * ]);
808
     * ```
809
     *
810
     * @param string $table the name of the table to be created. The name will be properly quoted by the method.
811
     * @param array $columns the columns (name => definition) in the new table.
812
     * @param string|null $options additional SQL fragment that will be appended to the generated SQL.
813
     *
814
     * @psalm-param array<array-key, ColumnSchemaBuilder|string> $columns
815
     *
816
     * @return string the SQL statement for creating a new DB table.
817
     */
818 63
    public function createTable(string $table, array $columns, ?string $options = null): string
819
    {
820 63
        $cols = [];
821
822 63
        foreach ($columns as $name => $type) {
823 63
            if (is_string($name)) {
824 63
                $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
825
            } else {
826 5
                $cols[] = "\t" . $type;
827
            }
828
        }
829
830 63
        $sql = 'CREATE TABLE ' . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)";
831
832 63
        return ($options === null) ? $sql : ($sql . ' ' . $options);
833
    }
834
835
    /**
836
     * Builds a SQL statement for renaming a DB table.
837
     *
838
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
839
     * @param string $newName the new table name. The name will be properly quoted by the method.
840
     *
841
     * @return string the SQL statement for renaming a DB table.
842
     */
843 2
    public function renameTable(string $oldName, string $newName): string
844
    {
845 2
        return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName);
846
    }
847
848
    /**
849
     * Builds a SQL statement for dropping a DB table.
850
     *
851
     * @param string $table the table to be dropped. The name will be properly quoted by the method.
852
     *
853
     * @return string the SQL statement for dropping a DB table.
854
     */
855 12
    public function dropTable(string $table): string
856
    {
857 12
        return 'DROP TABLE ' . $this->db->quoteTableName($table);
858
    }
859
860
    /**
861
     * Builds a SQL statement for adding a primary key constraint to an existing table.
862
     *
863
     * @param string $name the name of the primary key constraint.
864
     * @param string $table the table that the primary key constraint will be added to.
865
     * @param array|string $columns comma separated string or array of columns that the primary key will consist of.
866
     *
867
     * @psalm-param array<array-key, string>|string $columns
868
     *
869
     * @return string the SQL statement for adding a primary key constraint to an existing table.
870
     */
871 11
    public function addPrimaryKey(string $name, string $table, $columns): string
872
    {
873 11
        if (is_string($columns)) {
874 8
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
875
        }
876
877 11
        foreach ($columns as $i => $col) {
878 11
            $columns[$i] = $this->db->quoteColumnName($col);
879
        }
880
881 11
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
882 11
            . $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
883 11
            . implode(', ', $columns) . ')';
884
    }
885
886
    /**
887
     * Builds a SQL statement for removing a primary key constraint to an existing table.
888
     *
889
     * @param string $name the name of the primary key constraint to be removed.
890
     * @param string $table the table that the primary key constraint will be removed from.
891
     *
892
     * @return string the SQL statement for removing a primary key constraint from an existing table.
893
     */
894 5
    public function dropPrimaryKey(string $name, string $table): string
895
    {
896 5
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
897 5
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
898
    }
899
900
    /**
901
     * Builds a SQL statement for truncating a DB table.
902
     *
903
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
904
     *
905
     * @return string the SQL statement for truncating a DB table.
906
     */
907 3
    public function truncateTable(string $table): string
908
    {
909 3
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table);
910
    }
911
912
    /**
913
     * Builds a SQL statement for adding a new DB column.
914
     *
915
     * @param string $table the table that the new column will be added to. The table name will be properly quoted by
916
     * the method.
917
     * @param string $column the name of the new column. The name will be properly quoted by the method.
918
     * @param string $type the column type. The {@see getColumnType()} method will be invoked to convert abstract column
919
     * type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
920
     * generated SQL.
921
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
922
     * 'varchar(255) not null'.
923
     *
924
     * @return string the SQL statement for adding a new column.
925
     */
926 2
    public function addColumn(string $table, string $column, string $type): string
927
    {
928 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
929 2
            . ' ADD ' . $this->db->quoteColumnName($column) . ' '
930 2
            . $this->getColumnType($type);
931
    }
932
933
    /**
934
     * Builds a SQL statement for dropping a DB column.
935
     *
936
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
937
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
938
     *
939
     * @return string the SQL statement for dropping a DB column.
940
     */
941
    public function dropColumn(string $table, string $column): string
942
    {
943
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
944
            . ' DROP COLUMN ' . $this->db->quoteColumnName($column);
945
    }
946
947
    /**
948
     * Builds a SQL statement for renaming a column.
949
     *
950
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
951
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
952
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
953
     *
954
     * @return string the SQL statement for renaming a DB column.
955
     */
956
    public function renameColumn(string $table, string $oldName, string $newName): string
957
    {
958
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
959
            . ' RENAME COLUMN ' . $this->db->quoteColumnName($oldName)
960
            . ' TO ' . $this->db->quoteColumnName($newName);
961
    }
962
963
    /**
964
     * Builds a SQL statement for changing the definition of a column.
965
     *
966
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
967
     * method.
968
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
969
     * @param string $type the new column type. The {@see getColumnType()} method will be invoked to convert abstract
970
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
971
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
972
     * will become 'varchar(255) not null'.
973
     *
974
     * @return string the SQL statement for changing the definition of a column.
975
     */
976 1
    public function alterColumn(string $table, string $column, string $type): string
977
    {
978 1
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
979 1
            . $this->db->quoteColumnName($column) . ' '
980 1
            . $this->db->quoteColumnName($column) . ' '
981 1
            . $this->getColumnType($type);
982
    }
983
984
    /**
985
     * Builds a SQL statement for adding a foreign key constraint to an existing table. The method will properly quote
986
     * the table and column names.
987
     *
988
     * @param string $name the name of the foreign key constraint.
989
     * @param string $table the table that the foreign key constraint will be added to.
990
     * @param array|string $columns the name of the column to that the constraint will be added on. If there are
991
     * multiple columns, separate them with commas or use an array to represent them.
992
     * @param string $refTable the table that the foreign key references to.
993
     * @param array|string $refColumns the name of the column that the foreign key references to. If there are multiple
994
     * columns, separate them with commas or use an array to represent them.
995
     * @param string|null $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
996
     * SET DEFAULT, SET NULL.
997
     * @param string|null $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
998
     * SET DEFAULT, SET NULL.
999
     *
1000
     * @psalm-param array<array-key, string>|string $columns
1001
     * @psalm-param array<array-key, string>|string $refColumns
1002
     *
1003
     * @throws Exception|InvalidArgumentException
1004
     *
1005
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
1006
     */
1007 9
    public function addForeignKey(
1008
        string $name,
1009
        string $table,
1010
        $columns,
1011
        string $refTable,
1012
        $refColumns,
1013
        ?string $delete = null,
1014
        ?string $update = null
1015
    ): string {
1016 9
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
1017 9
            . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
1018 9
            . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
1019 9
            . ' REFERENCES ' . $this->db->quoteTableName($refTable)
1020 9
            . ' (' . $this->buildColumns($refColumns) . ')';
1021
1022 9
        if ($delete !== null) {
1023 6
            $sql .= ' ON DELETE ' . $delete;
1024
        }
1025
1026 9
        if ($update !== null) {
1027 6
            $sql .= ' ON UPDATE ' . $update;
1028
        }
1029
1030 9
        return $sql;
1031
    }
1032
1033
    /**
1034
     * Builds a SQL statement for dropping a foreign key constraint.
1035
     *
1036
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by
1037
     * the method.
1038
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
1039
     *
1040
     * @return string the SQL statement for dropping a foreign key constraint.
1041
     */
1042 7
    public function dropForeignKey(string $name, string $table): string
1043
    {
1044 7
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1045 7
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1046
    }
1047
1048
    /**
1049
     * Builds a SQL statement for creating a new index.
1050
     *
1051
     * @param string $name the name of the index. The name will be properly quoted by the method.
1052
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
1053
     * the method.
1054
     * @param array|string $columns the column(s) that should be included in the index. If there are multiple columns,
1055
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
1056
     * method, unless a parenthesis is found in the name.
1057
     * @param bool $unique whether to add UNIQUE constraint on the created index.
1058
     *
1059
     * @psalm-param array<array-key, ExpressionInterface|string>|string $columns
1060
     *
1061
     * @throws Exception|InvalidArgumentException
1062
     *
1063
     * @return string the SQL statement for creating a new index.
1064
     */
1065 11
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
1066
    {
1067 11
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
1068 11
            . $this->db->quoteTableName($name) . ' ON '
1069 11
            . $this->db->quoteTableName($table)
1070 11
            . ' (' . $this->buildColumns($columns) . ')';
1071
    }
1072
1073
    /**
1074
     * Builds a SQL statement for dropping an index.
1075
     *
1076
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
1077
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
1078
     *
1079
     * @return string the SQL statement for dropping an index.
1080
     */
1081 6
    public function dropIndex(string $name, string $table): string
1082
    {
1083 6
        return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
1084
    }
1085
1086
    /**
1087
     * Creates a SQL command for adding an unique constraint to an existing table.
1088
     *
1089
     * @param string $name the name of the unique constraint. The name will be properly quoted by the method.
1090
     * @param string $table the table that the unique constraint will be added to. The name will be properly quoted by
1091
     * the method.
1092
     * @param array|string $columns the name of the column to that the constraint will be added on. If there are
1093
     * multiple columns, separate them with commas. The name will be properly quoted by the method.
1094
     *
1095
     * @psalm-param array<array-key, string>|string $columns
1096
     *
1097
     * @return string the SQL statement for adding an unique constraint to an existing table.
1098
     */
1099 12
    public function addUnique(string $name, string $table, $columns): string
1100
    {
1101 12
        if (is_string($columns)) {
1102 8
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1103
        }
1104
1105 12
        foreach ($columns as $i => $col) {
1106 12
            $columns[$i] = $this->db->quoteColumnName($col);
1107
        }
1108
1109 12
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1110 12
            . $this->db->quoteColumnName($name) . ' UNIQUE ('
1111 12
            . implode(', ', $columns) . ')';
1112
    }
1113
1114
    /**
1115
     * Creates a SQL command for dropping an unique constraint.
1116
     *
1117
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
1118
     * method.
1119
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by the
1120
     * method.
1121
     *
1122
     * @return string the SQL statement for dropping an unique constraint.
1123
     */
1124 6
    public function dropUnique(string $name, string $table): string
1125
    {
1126 6
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1127 6
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1128
    }
1129
1130
    /**
1131
     * Creates a SQL command for adding a check constraint to an existing table.
1132
     *
1133
     * @param string $name the name of the check constraint. The name will be properly quoted by the method.
1134
     * @param string $table the table that the check constraint will be added to. The name will be properly quoted by
1135
     * the method.
1136
     * @param string $expression the SQL of the `CHECK` constraint.
1137
     *
1138
     * @return string the SQL statement for adding a check constraint to an existing table.
1139
     */
1140 6
    public function addCheck(string $name, string $table, string $expression): string
1141
    {
1142 6
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1143 6
            . $this->db->quoteColumnName($name) . ' CHECK (' . $this->db->quoteSql($expression) . ')';
0 ignored issues
show
Bug introduced by
The method quoteSql() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

1143
            . $this->db->quoteColumnName($name) . ' CHECK (' . $this->db->/** @scrutinizer ignore-call */ quoteSql($expression) . ')';
Loading history...
1144
    }
1145
1146
    /**
1147
     * Creates a SQL command for dropping a check constraint.
1148
     *
1149
     * @param string $name the name of the check constraint to be dropped. The name will be properly quoted by the
1150
     * method.
1151
     * @param string $table the table whose check constraint is to be dropped. The name will be properly quoted by the
1152
     * method.
1153
     *
1154
     * @return string the SQL statement for dropping a check constraint.
1155
     */
1156 6
    public function dropCheck(string $name, string $table): string
1157
    {
1158 6
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1159 6
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1160
    }
1161
1162
    /**
1163
     * Creates a SQL command for adding a default value constraint to an existing table.
1164
     *
1165
     * @param string $name the name of the default value constraint.
1166
     * The name will be properly quoted by the method.
1167
     * @param string $table the table that the default value constraint will be added to.
1168
     * The name will be properly quoted by the method.
1169
     * @param string $column the name of the column to that the constraint will be added on.
1170
     * The name will be properly quoted by the method.
1171
     * @param mixed $value default value.
1172
     *
1173
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
1174
     *
1175
     * @return string the SQL statement for adding a default value constraint to an existing table.
1176
     */
1177
    public function addDefaultValue(string $name, string $table, string $column, $value): string
0 ignored issues
show
Unused Code introduced by
The parameter $table 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

1177
    public function addDefaultValue(string $name, /** @scrutinizer ignore-unused */ string $table, string $column, $value): 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...
Unused Code introduced by
The parameter $name 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

1177
    public function addDefaultValue(/** @scrutinizer ignore-unused */ string $name, string $table, string $column, $value): 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...
Unused Code introduced by
The parameter $column 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

1177
    public function addDefaultValue(string $name, string $table, /** @scrutinizer ignore-unused */ string $column, $value): 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...
Unused Code introduced by
The parameter $value 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

1177
    public function addDefaultValue(string $name, string $table, string $column, /** @scrutinizer ignore-unused */ $value): 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...
1178
    {
1179
        throw new NotSupportedException(
1180
            $this->db->getDriverName() . ' does not support adding default value constraints.'
1181
        );
1182
    }
1183
1184
    /**
1185
     * Creates a SQL command for dropping a default value constraint.
1186
     *
1187
     * @param string $name the name of the default value constraint to be dropped.
1188
     * The name will be properly quoted by the method.
1189
     * @param string $table the table whose default value constraint is to be dropped.
1190
     * The name will be properly quoted by the method.
1191
     *
1192
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
1193
     *
1194
     * @return string the SQL statement for dropping a default value constraint.
1195
     */
1196
    public function dropDefaultValue(string $name, string $table): string
0 ignored issues
show
Unused Code introduced by
The parameter $name 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

1196
    public function dropDefaultValue(/** @scrutinizer ignore-unused */ string $name, string $table): 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...
Unused Code introduced by
The parameter $table 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

1196
    public function dropDefaultValue(string $name, /** @scrutinizer ignore-unused */ string $table): 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...
1197
    {
1198
        throw new NotSupportedException(
1199
            $this->db->getDriverName() . ' does not support dropping default value constraints.'
1200
        );
1201
    }
1202
1203
    /**
1204
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
1205
     *
1206
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
1207
     * or 1.
1208
     *
1209
     * @param string $tableName the name of the table whose primary key sequence will be reset.
1210
     * @param array|string|null $value the value for the primary key of the next new row inserted. If this is not set,
1211
     * the next new row's primary key will have a value 1.
1212
     *
1213
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
1214
     *
1215
     * @return string the SQL statement for resetting sequence.
1216
     */
1217
    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

1217
    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...
Unused Code introduced by
The parameter $value 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

1217
    public function resetSequence(string $tableName, /** @scrutinizer ignore-unused */ $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...
1218
    {
1219
        throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.');
1220
    }
1221
1222
    /**
1223
     * Builds a SQL statement for enabling or disabling integrity check.
1224
     *
1225
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
1226
     * @param string $table the table name. Defaults to empty string, meaning that no table will be changed.
1227
     * @param bool $check whether to turn on or off the integrity check.
1228
     *
1229
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
1230
     *
1231
     * @return string the SQL statement for checking integrity.
1232
     */
1233
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
0 ignored issues
show
Unused Code introduced by
The parameter $table 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

1233
    public function checkIntegrity(string $schema = '', /** @scrutinizer ignore-unused */ string $table = '', 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...
Unused Code introduced by
The parameter $schema 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

1233
    public function checkIntegrity(/** @scrutinizer ignore-unused */ string $schema = '', string $table = '', 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...
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

1233
    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...
1234
    {
1235
        throw new NotSupportedException(
1236
            $this->db->getDriverName() . ' does not support enabling/disabling integrity check.'
1237
        );
1238
    }
1239
1240
    /**
1241
     * Builds a SQL command for adding comment to column.
1242
     *
1243
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1244
     * method.
1245
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1246
     * method.
1247
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1248
     *
1249
     * @throws Exception
1250
     *
1251
     * @return string the SQL statement for adding comment on column.
1252
     */
1253 3
    public function addCommentOnColumn(string $table, string $column, string $comment): string
1254
    {
1255 3
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column)
1256 3
            . ' IS ' . $this->db->quoteValue($comment);
0 ignored issues
show
Bug introduced by
The method quoteValue() does not exist on Yiisoft\Db\Connection\ConnectionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yiisoft\Db\Connection\ConnectionInterface. ( Ignorable by Annotation )

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

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

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