Passed
Pull Request — master (#259)
by Def
23:22 queued 10:20
created

QueryBuilder::buildSelect()   C

Complexity

Conditions 13
Paths 40

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 15.6406

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 18
cts 24
cp 0.75
crap 15.6406
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
    public function __construct(ConnectionInterface $db)
133 1960
    {
134
        $this->db = $db;
135 1960
        $this->expressionBuilders = $this->defaultExpressionBuilders();
136 1960
        $this->conditionClasses = $this->defaultConditionClasses();
137 1960
    }
138 1960
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
    protected function defaultConditionClasses(): array
148 1960
    {
149
        return [
150
            'NOT' => Conditions\NotCondition::class,
151 1960
            '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
    protected function defaultExpressionBuilders(): array
175 1960
    {
176
        return [
177
            Query::class => QueryExpressionBuilder::class,
178 1960
            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
    public function build(Query $query, array $params = []): array
237 1234
    {
238
        $query = $query->prepare($this);
239 1234
240
        $params = empty($params) ? $query->getParams() : array_merge($params, $query->getParams());
241 1234
242
        $clauses = [
243
            $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 1234
        ];
250
251
        $sql = implode($this->separator, array_filter($clauses));
252 1234
253
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
254 1234
255
        if (!empty($query->getOrderBy())) {
256 1234
            foreach ($query->getOrderBy() as $expression) {
257 180
                if ($expression instanceof ExpressionInterface) {
258 180
                    $this->buildExpression($expression, $params);
259 4
                }
260
            }
261
        }
262
263
        if (!empty($query->getGroupBy())) {
264 1234
            foreach ($query->getGroupBy() as $expression) {
265 12
                if ($expression instanceof ExpressionInterface) {
266 12
                    $this->buildExpression($expression, $params);
267 4
                }
268
            }
269
        }
270
271
        $union = $this->buildUnion($query->getUnion(), $params);
272 1234
273
        if ($union !== '') {
274 1234
            $sql = "($sql){$this->separator}$union";
275 9
        }
276
277
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
278 1234
279
        if ($with !== '') {
280 1234
            $sql = "$with{$this->separator}$sql";
281 8
        }
282
283
        return [$sql, $params];
284 1234
    }
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
    public function buildExpression(ExpressionInterface $expression, array &$params = []): string
303 1376
    {
304
        $builder = $this->getExpressionBuilder($expression);
305 1376
306
        return (string) $builder->build($expression, $params);
307 1376
    }
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
    public function getExpressionBuilder(ExpressionInterface $expression)
323 1376
    {
324
        $className = get_class($expression);
325 1376
326
        if (!isset($this->expressionBuilders[$className])) {
327 1376
            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
        if ($this->expressionBuilders[$className] === __CLASS__) {
342 1376
            return $this;
343
        }
344
345
        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 1376
        }
348
349
        return $this->expressionBuilders[$className];
350 1376
    }
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
    public function insert(string $table, $columns, array &$params = []): string
377 209
    {
378
        [$names, $placeholders, $values, $params] = $this->prepareInsertValues($table, $columns, $params);
379 209
380
        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 197
    }
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
    protected function prepareInsertValues(string $table, $columns, array $params = []): array
399 298
    {
400
        $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 298
407
        if ($columns instanceof Query) {
408 298
            [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
409 69
        } else {
410
            foreach ($columns as $name => $value) {
411 238
                $names[] = $schema->quoteColumnName($name);
412 233
                $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
413 233
414
                if ($value instanceof ExpressionInterface) {
415 233
                    $placeholders[] = $this->buildExpression($value, $params);
416 51
                } elseif ($value instanceof Query) {
417 227
                    [$sql, $params] = $this->build($value, $params);
418
                    $placeholders[] = "($sql)";
419
                } else {
420
                    $placeholders[] = $this->bindParam($value, $params);
421 227
                }
422
            }
423
        }
424
425
        return [$names, $placeholders, $values, $params];
426 283
    }
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
    protected function prepareInsertSelectSubQuery(Query $columns, Schema $schema, array $params = []): array
441 69
    {
442
        if (
443
            !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 69
        ) {
447
            throw new InvalidArgumentException('Expected select query object with enumerated (named) parameters');
448 15
        }
449
450
        [$values, $params] = $this->build($columns, $params);
451 54
452
        $names = [];
453 54
        $values = ' ' . $values;
454 54
455
        foreach ($columns->getSelect() as $title => $field) {
456 54
            if (is_string($title)) {
457 54
                $names[] = $schema->quoteColumnName($title);
458 54
            } 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
        return [$names, $values, $params];
466 54
    }
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
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
495 40
    {
496
        if (empty($rows)) {
497 40
            return '';
498 4
        }
499
500
        $schema = $this->db->getSchema();
501 38
502
503
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
504 38
            $columnSchemas = $tableSchema->getColumns();
505 38
        } else {
506
            $columnSchemas = [];
507
        }
508
509
        $values = [];
510 38
511
        foreach ($rows as $row) {
512 38
            $vs = [];
513 35
            foreach ($row as $i => $value) {
514 35
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
515 35
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
516 26
                }
517
                if (is_string($value)) {
518 35
                    $value = $schema->quoteValue($value);
519 23
                } elseif (is_float($value)) {
520 21
                    /* ensure type cast always has . as decimal separator in all locales */
521
                    $value = NumericHelper::normalize((string) $value);
522 2
                } elseif ($value === false) {
523 21
                    $value = 0;
524 7
                } elseif ($value === null) {
525 21
                    $value = 'NULL';
526 12
                } elseif ($value instanceof ExpressionInterface) {
527 13
                    $value = $this->buildExpression($value, $params);
528 9
                }
529
                $vs[] = $value;
530 35
            }
531
            $values[] = '(' . implode(', ', $vs) . ')';
532 35
        }
533
534
        if (empty($values)) {
535 38
            return '';
536 3
        }
537
538
        foreach ($columns as $i => $name) {
539 35
            $columns[$i] = $schema->quoteColumnName($name);
540 32
        }
541
542
        return 'INSERT INTO ' . $schema->quoteTableName($table)
543 35
            . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
544 35
    }
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
    protected function prepareUpsertColumns(string $table, $insertColumns, $updateColumns, array &$constraints = []): array
594 90
    {
595
        if ($insertColumns instanceof Query) {
596 90
            [$insertNames] = $this->prepareInsertSelectSubQuery($insertColumns, $this->db->getSchema());
597 40
        } else {
598
            $insertNames = array_map([$this->db, 'quoteColumnName'], array_keys($insertColumns));
599 50
        }
600
601
        $uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
602 90
603
        $uniqueNames = array_map([$this->db, 'quoteColumnName'], $uniqueNames);
604 90
605
        if ($updateColumns !== true) {
606 90
            return [$uniqueNames, $insertNames, null];
607 65
        }
608
609
        return [$uniqueNames, $insertNames, array_diff($insertNames, $uniqueNames)];
610 25
    }
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
    private function getTableUniqueColumnNames(string $name, array $columns, array &$constraints = []): array
628 90
    {
629
        $schema = $this->db->getSchema();
630 90
631
        $constraints = [];
632 90
        $primaryKey = $schema->getTablePrimaryKey($name);
633
634
        if ($primaryKey !== null) {
635
            $constraints[] = $primaryKey;
636 90
        }
637 90
638
        foreach ($schema->getTableIndexes($name) as $constraint) {
639 90
            if ($constraint->isUnique()) {
640 88
                $constraints[] = $constraint;
641
            }
642
        }
643 90
644 89
        $constraints = array_merge($constraints, $schema->getTableUniques($name));
645 89
646
        /** Remove duplicates */
647
        $constraints = array_combine(
648
            array_map(
649 90
                static function ($constraint) {
650
                    $columns = $constraint->getColumnNames();
651
                    sort($columns, SORT_STRING);
652 90
653 90
                    return json_encode($columns, JSON_THROW_ON_ERROR);
654 90
                },
655 90
                $constraints
656 90
            ),
657
            $constraints
658 90
        );
659
660 90
        $columnNames = [];
661
662 90
        /** Remove all constraints which do not cover the specified column list */
663
        $constraints = array_values(
664
            array_filter(
665 90
                $constraints,
666
                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 90
671 90
                    if ($result) {
672
                        $columnNames = array_merge($columnNames, $constraintColumnNames);
673 90
                    }
674 90
675
                    return $result;
676 90
                }
677 75
            )
678
        );
679
680 90
        return array_unique($columnNames);
681 90
    }
682
683
    /**
684
     * Creates an UPDATE SQL statement.
685 90
     *
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
    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 94
         */
714
        [$lines, $params] = $this->prepareUpdateSets($table, $columns, $params);
715
        $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
716
        $where = $this->buildWhere($condition, $params);
717
718
        return ($where === '') ? $sql : ($sql . ' ' . $where);
719 94
    }
720 94
721 94
    /**
722
     * Prepares a `SET` parts for an `UPDATE` SQL statement.
723 94
     *
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
    protected function prepareUpdateSets(string $table, array $columns, array $params = []): array
737
    {
738
        $tableSchema = $this->db->getTableSchema($table);
739
740
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
741 140
742
        $sets = [];
743 140
744
        foreach ($columns as $name => $value) {
745 140
            /** @psalm-var mixed $value */
746
            $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
747 140
            if ($value instanceof ExpressionInterface) {
748
                $placeholder = $this->buildExpression($value, $params);
749 140
            } else {
750
                $placeholder = $this->bindParam($value, $params);
751 140
            }
752 140
753 70
            $sets[] = $this->db->quoteColumnName($name) . '=' . $placeholder;
754
        }
755 96
756
        return [$sets, $params];
757
    }
758 140
759
    /**
760
     * Creates a DELETE SQL statement.
761 140
     *
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
    public function delete(string $table, $condition, array &$params): string
781
    {
782
        $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
783
        $where = $this->buildWhere($condition, $params);
784
785 47
        return ($where === '') ? $sql : ($sql . ' ' . $where);
786
    }
787 47
788 47
    /**
789
     * Builds a SQL statement for creating a new DB table.
790 47
     *
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
    public function createTable(string $table, array $columns, ?string $options = null): string
819
    {
820
        $cols = [];
821
822
        foreach ($columns as $name => $type) {
823 63
            if (is_string($name)) {
824
                $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
825 63
            } else {
826
                $cols[] = "\t" . $type;
827 63
            }
828 63
        }
829 63
830
        $sql = 'CREATE TABLE ' . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)";
831 5
832
        return ($options === null) ? $sql : ($sql . ' ' . $options);
833
    }
834
835 63
    /**
836
     * Builds a SQL statement for renaming a DB table.
837 63
     *
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
    public function renameTable(string $oldName, string $newName): string
844
    {
845
        return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName);
846
    }
847
848 2
    /**
849
     * Builds a SQL statement for dropping a DB table.
850 2
     *
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
    public function dropTable(string $table): string
856
    {
857
        return 'DROP TABLE ' . $this->db->quoteTableName($table);
858
    }
859
860 12
    /**
861
     * Builds a SQL statement for adding a primary key constraint to an existing table.
862 12
     *
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
    public function addPrimaryKey(string $name, string $table, $columns): string
872
    {
873
        if (is_string($columns)) {
874
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
875
        }
876 11
877
        foreach ($columns as $i => $col) {
878 11
            $columns[$i] = $this->db->quoteColumnName($col);
879 8
        }
880
881
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
882 11
            . $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
883 11
            . implode(', ', $columns) . ')';
884
    }
885
886 11
    /**
887 11
     * Builds a SQL statement for removing a primary key constraint to an existing table.
888 11
     *
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
    public function dropPrimaryKey(string $name, string $table): string
895
    {
896
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
897
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
898
    }
899 5
900
    /**
901 5
     * Builds a SQL statement for truncating a DB table.
902 5
     *
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
    public function truncateTable(string $table): string
908
    {
909
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table);
910
    }
911
912 3
    /**
913
     * Builds a SQL statement for adding a new DB column.
914 3
     *
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
    public function addColumn(string $table, string $column, string $type): string
927
    {
928
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
929
            . ' ADD ' . $this->db->quoteColumnName($column) . ' '
930
            . $this->getColumnType($type);
931 2
    }
932
933 2
    /**
934 2
     * Builds a SQL statement for dropping a DB column.
935 2
     *
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
    public function alterColumn(string $table, string $column, string $type): string
977
    {
978
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
979
            . $this->db->quoteColumnName($column) . ' '
980
            . $this->db->quoteColumnName($column) . ' '
981 1
            . $this->getColumnType($type);
982
    }
983 1
984 1
    /**
985 1
     * Builds a SQL statement for adding a foreign key constraint to an existing table. The method will properly quote
986 1
     * 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
    public function addForeignKey(
1008
        string $name,
1009
        string $table,
1010
        $columns,
1011
        string $refTable,
1012 9
        $refColumns,
1013
        ?string $delete = null,
1014
        ?string $update = null
1015
    ): string {
1016
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
1017
            . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
1018
            . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
1019
            . ' REFERENCES ' . $this->db->quoteTableName($refTable)
1020
            . ' (' . $this->buildColumns($refColumns) . ')';
1021 9
1022 9
        if ($delete !== null) {
1023 9
            $sql .= ' ON DELETE ' . $delete;
1024 9
        }
1025 9
1026
        if ($update !== null) {
1027 9
            $sql .= ' ON UPDATE ' . $update;
1028 6
        }
1029
1030
        return $sql;
1031 9
    }
1032 6
1033
    /**
1034
     * Builds a SQL statement for dropping a foreign key constraint.
1035 9
     *
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
    public function dropForeignKey(string $name, string $table): string
1043
    {
1044
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1045
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1046
    }
1047 7
1048
    /**
1049 7
     * Builds a SQL statement for creating a new index.
1050 7
     *
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
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
1066
    {
1067
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
1068
            . $this->db->quoteTableName($name) . ' ON '
1069
            . $this->db->quoteTableName($table)
1070 11
            . ' (' . $this->buildColumns($columns) . ')';
1071
    }
1072 11
1073 11
    /**
1074 11
     * Builds a SQL statement for dropping an index.
1075 11
     *
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
    public function dropIndex(string $name, string $table): string
1082
    {
1083
        return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
1084
    }
1085
1086 6
    /**
1087
     * Creates a SQL command for adding an unique constraint to an existing table.
1088 6
     *
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
    public function addUnique(string $name, string $table, $columns): string
1100
    {
1101
        if (is_string($columns)) {
1102
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1103
        }
1104 12
1105
        foreach ($columns as $i => $col) {
1106 12
            $columns[$i] = $this->db->quoteColumnName($col);
1107 8
        }
1108
1109
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1110 12
            . $this->db->quoteColumnName($name) . ' UNIQUE ('
1111 12
            . implode(', ', $columns) . ')';
1112
    }
1113
1114 12
    /**
1115 12
     * Creates a SQL command for dropping an unique constraint.
1116 12
     *
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
    public function dropUnique(string $name, string $table): string
1125
    {
1126
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1127
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1128
    }
1129 6
1130
    /**
1131 6
     * Creates a SQL command for adding a check constraint to an existing table.
1132 6
     *
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
    public function addCheck(string $name, string $table, string $expression): string
1141
    {
1142
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1143
            . $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 6
1146
    /**
1147 6
     * Creates a SQL command for dropping a check constraint.
1148 6
     *
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
    public function dropCheck(string $name, string $table): string
1157
    {
1158
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1159
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1160
    }
1161 6
1162
    /**
1163 6
     * Creates a SQL command for adding a default value constraint to an existing table.
1164 6
     *
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
    public function addCommentOnColumn(string $table, string $column, string $comment): string
1254
    {
1255
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column)
1256
            . ' 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 3
1259
    /**
1260 3
     * Builds a SQL command for adding comment to table.
1261 3
     *
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
    public function addCommentOnTable(string $table, string $comment): string
1271
    {
1272
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS ' . $this->db->quoteValue($comment);
1273
    }
1274
1275 2
    /**
1276
     * Builds a SQL command for adding comment to column.
1277 2
     *
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
    public function dropCommentFromColumn(string $table, string $column): string
1286
    {
1287
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column)
1288
            . ' IS NULL';
1289
    }
1290 1
1291
    /**
1292 1
     * Builds a SQL command for adding comment to table.
1293 1
     *
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
    public function dropCommentFromTable(string $table): string
1300
    {
1301
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS NULL';
1302
    }
1303
1304 1
    /**
1305
     * Creates a SQL View.
1306 1
     *
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
    public function createView(string $viewName, $subQuery): string
1317
    {
1318
        if ($subQuery instanceof Query) {
1319
            /** @psalm-var array<array-key, int|string> $params */
1320
            [$rawQuery, $params] = $this->build($subQuery);
1321 5
1322
            foreach ($params as $key => $value) {
1323 5
                $params[$key] = $this->db->quoteValue($value);
1324
            }
1325 5
1326
            $subQuery = strtr($rawQuery, $params);
1327 5
        }
1328 5
1329
        return 'CREATE VIEW ' . $this->db->quoteTableName($viewName) . ' AS ' . $subQuery;
1330
    }
1331 5
1332
    /**
1333
     * Drops a SQL View.
1334 5
     *
1335
     * @param string $viewName the name of the view to be dropped.
1336
     *
1337
     * @return string the `DROP VIEW` SQL statement.
1338
     */
1339
    public function dropView(string $viewName): string
1340
    {
1341
        return 'DROP VIEW ' . $this->db->quoteTableName($viewName);
1342
    }
1343
1344 5
    /**
1345
     * Converts an abstract column type into a physical column type.
1346 5
     *
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
    public function getColumnType($type): string
1391
    {
1392
        if ($type instanceof ColumnSchemaBuilder) {
1393
            $type = $type->__toString();
1394
        }
1395 69
1396
        if (isset($this->typeMap[$type])) {
1397 69
            return $this->typeMap[$type];
1398 11
        }
1399
1400
        if (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
1401 69
            if (isset($this->typeMap[$matches[1]])) {
1402 43
                return preg_replace(
1403
                    '/\(.+\)/',
1404
                    '(' . $matches[2] . ')',
1405 41
                    $this->typeMap[$matches[1]]
1406 20
                ) . $matches[3];
1407 17
            }
1408
        } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
1409 17
            if (isset($this->typeMap[$matches[1]])) {
1410 17
                return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
1411 20
            }
1412
        }
1413 34
1414 33
        return $type;
1415 33
    }
1416
1417
    /**
1418
     * @param array $columns
1419 5
     * @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
    public function buildSelect(
1430
        array $columns,
1431
        array &$params,
1432
        ?bool $distinct = false,
1433
        string $selectOption = null
1434 1522
    ): string {
1435
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
1436
1437
        if ($selectOption !== null) {
1438
            $select .= ' ' . $selectOption;
1439
        }
1440 1522
1441
        if (empty($columns)) {
1442 1522
            return $select . ' *';
1443
        }
1444
1445
        foreach ($columns as $i => $column) {
1446 1522
            if ($column instanceof ExpressionInterface) {
1447 1294
                if (is_int($i)) {
1448
                    $columns[$i] = $this->buildExpression($column, $params);
1449
                } else {
1450 441
                    $columns[$i] = $this->buildExpression($column, $params) . ' AS ' . $this->db->quoteColumnName($i);
1451 441
                }
1452 73
            } elseif ($column instanceof Query) {
1453 10
                [$sql, $params] = $this->build($column, $params);
1454
                $columns[$i] = "($sql) AS " . $this->db->quoteColumnName((string) $i);
1455 73
            } elseif (is_string($i) && $i !== $column) {
1456
                if (strpos($column, '(') === false) {
1457 423
                    $column = $this->db->quoteColumnName($column);
1458
                }
1459
                $columns[$i] = "$column AS " . $this->db->quoteColumnName($i);
1460 423
            } elseif (strpos($column, '(') === false) {
1461 19
                if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_.]+)$/', $column, $matches)) {
1462 14
                    $columns[$i] = $this->db->quoteColumnName(
1463
                        $matches[1]
1464 19
                    ) . ' AS ' . $this->db->quoteColumnName($matches[2]);
1465 419
                } else {
1466 324
                    $columns[$i] = $this->db->quoteColumnName($column);
1467
                }
1468
            }
1469
        }
1470
1471 324
        return $select . ' ' . implode(', ', $columns);
1472
    }
1473
1474
    /**
1475
     * @param array|null $tables
1476 441
     * @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
    public function buildFrom(?array $tables, array &$params): string
1485
    {
1486
        if (empty($tables)) {
1487
            return '';
1488
        }
1489 1542
1490
        $tables = $this->quoteTableNames($tables, $params);
1491 1542
1492 668
        return 'FROM ' . implode(', ', $tables);
1493
    }
1494
1495 936
    /**
1496
     * @param array $joins
1497 936
     * @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
    public function buildJoin(array $joins, array &$params): string
1513
    {
1514
        if (empty($joins)) {
1515
            return '';
1516
        }
1517 1522
1518
        foreach ($joins as $i => $join) {
1519 1522
            if (!is_array($join) || !isset($join[0], $join[1])) {
1520 1517
                throw new Exception(
1521
                    'A join clause must be specified as an array of join type, join table, and optionally join '
1522
                    . 'condition.'
1523 110
                );
1524 110
            }
1525
1526
            /* 0:join type, 1:join table, 2:on-condition (optional) */
1527
            [$joinType, $table] = $join;
1528
1529
            $tables = $this->quoteTableNames((array) $table, $params);
1530
1531
            /** @var string $table */
1532 110
            $table = reset($tables);
1533
            $joins[$i] = "$joinType $table";
1534 110
1535
            if (isset($join[2])) {
1536
                $condition = $this->buildCondition($join[2], $params);
1537 110
                if ($condition !== '') {
1538 110
                    $joins[$i] .= ' ON ' . $condition;
1539
                }
1540 110
            }
1541 110
        }
1542 110
1543 110
        /** @psalm-var array<string> $joins */
1544
        return implode($this->separator, $joins);
1545
    }
1546
1547
    /**
1548
     * Quotes table names passed.
1549 110
     *
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
    private function quoteTableNames(array $tables, array &$params): array
1560
    {
1561
        foreach ($tables as $i => $table) {
1562
            if ($table instanceof Query) {
1563
                [$sql, $params] = $this->build($table, $params);
1564 936
                $tables[$i] = "($sql) " . $this->db->quoteTableName((string) $i);
1565
            } elseif (is_string($table) && is_string($i)) {
1566 936
                if (strpos($table, '(') === false) {
1567 936
                    $table = $this->db->quoteTableName($table);
1568 16
                }
1569 16
                $tables[$i] = "$table " . $this->db->quoteTableName($i);
1570 936
            } elseif (is_string($table) && strpos($table, '(') === false) {
1571 58
                $tableWithAlias = $this->extractAlias($table);
1572 40
                if (is_array($tableWithAlias)) { // with alias
1573
                    $tables[$i] = $this->db->quoteTableName($tableWithAlias[1]) . ' '
1574 58
                        . $this->db->quoteTableName($tableWithAlias[2]);
1575 913
                } else {
1576 902
                    $tables[$i] = $this->db->quoteTableName($table);
1577 902
                }
1578 50
            }
1579 50
        }
1580
1581 862
        return $tables;
1582
    }
1583
1584
    /**
1585
     * @param array|string $condition
1586 936
     * @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
    public function buildWhere($condition, array &$params = []): string
1593
    {
1594
        $where = $this->buildCondition($condition, $params);
1595
1596
        return ($where === '') ? '' : ('WHERE ' . $where);
1597 1548
    }
1598
1599 1548
    /**
1600
     * @param array $columns
1601 1548
     * @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
    public function buildGroupBy(array $columns, array &$params = []): string
1610
    {
1611
        if (empty($columns)) {
1612
            return '';
1613
        }
1614 1522
1615
        foreach ($columns as $i => $column) {
1616 1522
            if ($column instanceof Expression) {
1617 1512
                $columns[$i] = $this->buildExpression($column);
1618
                $params = array_merge($params, $column->getParams());
1619
            } elseif (strpos($column, '(') === false) {
1620 15
                $columns[$i] = $this->db->quoteColumnName($column);
1621 15
            }
1622 5
        }
1623 5
1624 15
        return 'GROUP BY ' . implode(', ', $columns);
1625 15
    }
1626
1627
    /**
1628
     * @param array|string $condition
1629 15
     * @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
    public function buildHaving($condition, array &$params = []): string
1636
    {
1637
        $having = $this->buildCondition($condition, $params);
1638
1639
        return ($having === '') ? '' : ('HAVING ' . $having);
1640 1522
    }
1641
1642 1522
    /**
1643
     * Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
1644 1522
     *
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
    public function buildOrderByAndLimit(
1659
        string $sql,
1660
        array $orderBy,
1661
        $limit,
1662
        $offset,
1663 948
        array &$params = []
1664
    ): string {
1665
        $orderBy = $this->buildOrderBy($orderBy, $params);
1666
        if ($orderBy !== '') {
1667
            $sql .= $this->separator . $orderBy;
1668
        }
1669
        $limit = $this->buildLimit($limit, $offset);
1670 948
        if ($limit !== '') {
1671 948
            $sql .= $this->separator . $limit;
1672 138
        }
1673
1674 948
        return $sql;
1675 948
    }
1676 36
1677
    /**
1678
     * @param array $columns
1679 948
     * @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
    public function buildOrderBy(array $columns, array &$params = []): string
1688
    {
1689
        if (empty($columns)) {
1690
            return '';
1691
        }
1692 1534
1693
        $orders = [];
1694 1534
1695 1479
        foreach ($columns as $name => $direction) {
1696
            if ($direction instanceof Expression) {
1697
                $orders[] = $this->buildExpression($direction);
1698 229
                $params = array_merge($params, $direction->getParams());
1699
            } else {
1700 229
                $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
1701 229
            }
1702 5
        }
1703 5
1704
        return 'ORDER BY ' . implode(', ', $orders);
1705 229
    }
1706
1707
    /**
1708
     * @param Expression|int|null $limit
1709 229
     * @param Expression|int|null $offset
1710
     *
1711
     * @return string the LIMIT and OFFSET clauses.
1712
     */
1713
    public function buildLimit($limit, $offset): string
1714
    {
1715
        $sql = '';
1716
1717
        if ($this->hasLimit($limit)) {
1718 347
            $sql = 'LIMIT ' . (string) $limit;
1719
        }
1720 347
1721
        if ($this->hasOffset($offset)) {
1722 347
            $sql .= ' OFFSET ' . (string) $offset;
1723 11
        }
1724
1725
        return ltrim($sql);
1726 347
    }
1727 3
1728
    /**
1729
     * Checks to see if the given limit is effective.
1730 347
     *
1731
     * @param mixed $limit the given limit.
1732
     *
1733
     * @return bool whether the limit is effective.
1734
     */
1735
    protected function hasLimit($limit): bool
1736
    {
1737
        return ($limit instanceof ExpressionInterface) || ctype_digit((string) $limit);
1738
    }
1739
1740 1224
    /**
1741
     * Checks to see if the given offset is effective.
1742 1224
     *
1743
     * @param mixed $offset the given offset.
1744
     *
1745
     * @return bool whether the offset is effective.
1746
     */
1747
    protected function hasOffset($offset): bool
1748
    {
1749
        return ($offset instanceof ExpressionInterface) || (ctype_digit((string)$offset) && (string)$offset !== '0');
1750
    }
1751
1752 1224
    /**
1753
     * @param array $unions
1754 1224
     * @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
    public function buildUnion(array $unions, array &$params): string
1763
    {
1764
        if (empty($unions)) {
1765
            return '';
1766
        }
1767 1234
1768
        $result = '';
1769 1234
1770 1234
        foreach ($unions as $i => $union) {
1771
            $query = $union['query'];
1772
            if ($query instanceof Query) {
1773 9
                [$unions[$i]['query'], $params] = $this->build($query, $params);
1774
            }
1775 9
1776 9
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
1777 9
        }
1778 9
1779
        return trim($result);
1780
    }
1781 9
1782
    /**
1783
     * Processes columns and properly quotes them if necessary.
1784 9
     *
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
    public function buildColumns($columns): string
1796
    {
1797
        if (!is_array($columns)) {
1798
            if (strpos($columns, '(') !== false) {
1799
                return $columns;
1800 44
            }
1801
1802 44
            $rawColumns = $columns;
1803 33
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1804
1805
            if ($columns === false) {
1806
                throw new InvalidArgumentException("$rawColumns is not valid columns.");
1807 33
            }
1808 33
        }
1809
        foreach ($columns as $i => $column) {
1810 33
            if ($column instanceof ExpressionInterface) {
1811
                $columns[$i] = $this->buildExpression($column);
1812
            } elseif (strpos($column, '(') === false) {
1813
                $columns[$i] = $this->db->quoteColumnName($column);
1814 44
            }
1815 44
        }
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 44
    }
1819
1820
    /**
1821
     * Parses the condition specification and generates the corresponding SQL expression.
1822 44
     *
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
    public function buildCondition($condition, array &$params = []): string
1832
    {
1833
        if (is_array($condition)) {
1834
            if (empty($condition)) {
1835
                return '';
1836 1552
            }
1837
1838 1552
            $condition = $this->createConditionFromArray($condition);
1839 1179
        }
1840 5
1841
        if ($condition instanceof ExpressionInterface) {
1842
            return $this->buildExpression($condition, $params);
1843 1179
        }
1844
1845
        return $condition ?? '';
1846 1552
    }
1847 1284
1848
    /**
1849
     * Transforms $condition defined in array format (as described in {@see Query::where()} to instance of
1850 1528
     *
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
    public function createConditionFromArray(array $condition): ConditionInterface
1860
    {
1861
        /** operator format: operator, operand 1, operand 2, ... */
1862
        if (isset($condition[0])) {
1863
            $operator = strtoupper((string) array_shift($condition));
1864 1179
1865
            $className = $this->conditionClasses[$operator] ?? SimpleCondition::class;
1866
1867 1179
            /** @var ConditionInterface $className */
1868 821
            return $className::fromArrayDefinition($operator, $condition);
1869
        }
1870 821
1871
        /** hash format: 'column1' => 'value1', 'column2' => 'value2', ... */
1872
        return new HashCondition($condition);
1873 821
    }
1874
1875
    /**
1876
     * Creates a SELECT EXISTS() SQL statement.
1877 626
     *
1878
     * @param string $rawSql the subquery in a raw form to select from.
1879
     *
1880
     * @return string the SELECT EXISTS() SQL statement.
1881
     */
1882
    public function selectExists(string $rawSql): string
1883
    {
1884
        return 'SELECT EXISTS(' . $rawSql . ')';
1885
    }
1886
1887 9
    /**
1888
     * Helper method to add $value to $params array using {@see PARAM_PREFIX}.
1889 9
     *
1890
     * @param mixed $value
1891
     * @param array $params passed by reference.
1892
     *
1893
     * @return string the placeholder name in $params array.
1894
     */
1895
    public function bindParam($value, array &$params = []): string
1896
    {
1897
        $phName = self::PARAM_PREFIX . count($params);
1898
1899
        /** @psalm-var mixed */
1900 1202
        $params[$phName] = $value;
1901
1902 1202
        return $phName;
1903
    }
1904
1905 1202
    /**
1906
     * Extracts table alias if there is one or returns false.
1907 1202
     *
1908
     * @param $table
1909
     *
1910
     * @return array|bool
1911
     *
1912
     * @psalm-return array<array-key, string>|bool
1913
     */
1914
    protected function extractAlias(string $table)
1915
    {
1916
        if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) {
1917
            return $matches;
1918
        }
1919 902
1920
        return false;
1921 902
    }
1922 50
1923
    /**
1924
     * @param array $withs
1925 862
     * @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
    public function buildWithQueries(array $withs, array &$params): string
1934
    {
1935
        if (empty($withs)) {
1936
            return '';
1937
        }
1938 1522
1939
        $recursive = false;
1940 1522
        $result = [];
1941 1522
1942
        foreach ($withs as $i => $with) {
1943
            if ($with['recursive']) {
1944 10
                $recursive = true;
1945 10
            }
1946
1947 10
            $query = $with['query'];
1948 10
            if ($query instanceof Query) {
1949 5
                [$with['query'], $params] = $this->build($query, $params);
1950
            }
1951
1952 10
            $result[] = $with['alias'] . ' AS (' . $with['query'] . ')';
1953 10
        }
1954 10
1955
        return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . implode(', ', $result);
1956
    }
1957 10
1958
    public function getDb(): ConnectionInterface
1959
    {
1960 10
        return $this->db;
1961
    }
1962
1963 1351
    /**
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 1351
     *
1966
     * Defaults to an empty space. This is mainly used by {@see build()} when generating a SQL statement.
1967
     */
1968
    public function setSeparator(string $separator): void
1969
    {
1970
        $this->separator = $separator;
1971
    }
1972
}
1973