Passed
Pull Request — master (#163)
by Wilmer
12:24
created

QueryBuilder::buildSelect()   C

Complexity

Conditions 13
Paths 40

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 15.6406

Importance

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

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

350
        return 'INSERT INTO ' . $this->db->/** @scrutinizer ignore-call */ quoteTableName($table)

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
351
            . (!empty($names) ? ' (' . \implode(', ', $names) . ')' : '')
352
            . (!empty($placeholders) ? ' VALUES (' . \implode(', ', $placeholders) . ')' : $values);
353
    }
354 125
355
    /**
356 125
     * Prepares a `VALUES` part for an `INSERT` SQL statement.
357 125
     *
358 125
     * @param string $table the table that new rows will be inserted into.
359 125
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
360 125
     * {\Yiisoft\Db\Query\Query|Query} to perform INSERT INTO ... SELECT SQL statement.
361 125
     * @param array $params the binding parameters that will be generated by this method.
362
     * They should be bound to the DB command later.
363 125
     *
364 42
     * @throws Exception
365
     * @throws InvalidArgumentException
366 89
     * @throws InvalidConfigException
367 89
     * @throws NotSupportedException
368 89
     *
369
     * @return array array of column names, placeholders, values and params.
370 89
     */
371 23
    protected function prepareInsertValues(string $table, $columns, array $params = []): array
372 88
    {
373
        $schema = $this->db->getSchema();
374
        $tableSchema = $schema->getTableSchema($table);
375
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
376 88
        $names = [];
377
        $placeholders = [];
378
        $values = ' DEFAULT VALUES';
379
380
        if ($columns instanceof Query) {
381 116
            [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
382
        } else {
383
            foreach ($columns as $name => $value) {
384
                $names[] = $schema->quoteColumnName($name);
385
                $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
386
387
                if ($value instanceof ExpressionInterface) {
388
                    $placeholders[] = $this->buildExpression($value, $params);
389
                } elseif ($value instanceof Query) {
390
                    [$sql, $params] = $this->build($value, $params);
391
                    $placeholders[] = "($sql)";
392
                } else {
393
                    $placeholders[] = $this->bindParam($value, $params);
394 42
                }
395
            }
396 42
        }
397 9
398
        return [$names, $placeholders, $values, $params];
399
    }
400 33
401
    /**
402 33
     * Prepare select-subquery and field names for INSERT INTO ... SELECT SQL statement.
403 33
     *
404
     * @param Query $columns Object, which represents select query.
405 33
     * @param Schema $schema  Schema object to quote column name.
406 33
     * @param array $params  the parameters to be bound to the generated SQL statement. These parameters will
407 33
     * be included in the result with the additional parameters generated during the query building process.
408
     *
409
     * @return array array of column names, values and params.
410
     */
411
    protected function prepareInsertSelectSubQuery(Query $columns, Schema $schema, array $params = []): array
412
    {
413
        if (!\is_array($columns->getSelect()) || empty($columns->getSelect()) || \in_array('*', $columns->getSelect(), true)) {
414
            throw new InvalidArgumentException('Expected select query object with enumerated (named) parameters');
415 33
        }
416
417
        [$values, $params] = $this->build($columns, $params);
418
419
        $names = [];
420
        $values = ' ' . $values;
421
422
        foreach ($columns->getSelect() as $title => $field) {
423
            if (\is_string($title)) {
424
                $names[] = $schema->quoteColumnName($title);
425
            } elseif (\preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $field, $matches)) {
426
                $names[] = $schema->quoteColumnName($matches[2]);
427
            } else {
428
                $names[] = $schema->quoteColumnName($field);
429
            }
430
        }
431
432
        return [$names, $values, $params];
433
    }
434
435
    /**
436
     * Generates a batch INSERT SQL statement.
437
     *
438
     * For example,
439
     *
440
     * ```php
441
     * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
442
     *     ['Tom', 30],
443
     *     ['Jane', 20],
444
     *     ['Linda', 25],
445
     * ]);
446
     * ```
447 26
     *
448
     * Note that the values in each row must match the corresponding column names.
449 26
     *
450 2
     * The method will properly escape the column names, and quote the values to be inserted.
451
     *
452
     * @param string $table the table that new rows will be inserted into.
453 25
     * @param array $columns the column names.
454
     * @param array|Generator $rows the rows to be batch inserted into the table.
455 25
     * @param array $params the binding parameters. This parameter exists.
456 25
     *
457
     * @throws Exception
458
     * @throws InvalidArgumentException
459
     * @throws InvalidConfigException
460
     * @throws NotSupportedException
461 25
     *
462
     * @return string the batch INSERT SQL statement.
463 25
     */
464 23
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
465 23
    {
466 23
        if (empty($rows)) {
467 17
            return '';
468
        }
469 23
470 15
        $schema = $this->db->getSchema();
471 14
472
473 2
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
474 14
            $columnSchemas = $tableSchema->getColumns();
475 6
        } else {
476 14
            $columnSchemas = [];
477 8
        }
478 8
479 6
        $values = [];
480
481 23
        foreach ($rows as $row) {
482
            $vs = [];
483 23
            foreach ($row as $i => $value) {
484
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
485
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
486 25
                }
487 2
                if (\is_string($value)) {
488
                    $value = $schema->quoteValue($value);
489
                } elseif (\is_float($value)) {
490 23
                    // ensure type cast always has . as decimal separator in all locales
491 21
                    $value = NumericHelper::normalize($value);
492
                } elseif ($value === false) {
493
                    $value = 0;
494 23
                } elseif ($value === null) {
495 23
                    $value = 'NULL';
496
                } elseif ($value instanceof ExpressionInterface) {
497
                    $value = $this->buildExpression($value, $params);
498
                }
499
                $vs[] = $value;
500
            }
501
            $values[] = '(' . \implode(', ', $vs) . ')';
502
        }
503
504
        if (empty($values)) {
505
            return '';
506
        }
507
508
        foreach ($columns as $i => $name) {
509
            $columns[$i] = $schema->quoteColumnName($name);
510
        }
511
512
        return 'INSERT INTO ' . $schema->quoteTableName($table)
513
            . ' (' . \implode(', ', $columns) . ') VALUES ' . \implode(', ', $values);
514
    }
515
516
    /**
517
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
518
     * constraints), or update them if they do.
519
     *
520
     * For example,
521
     *
522
     * ```php
523
     * $sql = $queryBuilder->upsert('pages', [
524
     *     'name' => 'Front page',
525
     *     'url' => 'http://example.com/', // url is unique
526
     *     'visits' => 0,
527
     * ], [
528
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
529
     * ], $params);
530
     * ```
531
     *
532
     * The method will properly escape the table and column names.
533
     *
534
     * @param string $table the table that new rows will be inserted into/updated in.
535
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
536
     * of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
537
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
538
     * If `true` is passed, the column data will be updated to match the insert column data.
539
     * If `false` is passed, no update will be performed if the column data already exists.
540
     * @param array $params the binding parameters that will be generated by this method.
541
     * They should be bound to the DB command later.
542
     *
543
     * @throws Exception
544
     * @throws InvalidConfigException
545
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
546
     *
547
     * @return string the resulting SQL.
548
     */
549 54
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
550
    {
551 54
        throw new NotSupportedException($this->db->getDriverName() . ' does not support upsert statements.');
552 24
    }
553
554 30
    /**
555
     * @param string $table
556
     * @param array|Query $insertColumns
557 54
     * @param array|bool $updateColumns
558 54
     * @param Constraint[] $constraints this parameter recieves a matched constraint list.
559
     * The constraints will be unique by their column names.
560 54
     *
561 39
     * @throws Exception
562
     * @throws InvalidConfigException
563
     * @throws NotSupportedException
564 15
     *
565
     * @return array
566
     */
567
    protected function prepareUpsertColumns(string $table, $insertColumns, $updateColumns, array &$constraints = []): array
568
    {
569
        if ($insertColumns instanceof Query) {
570
            [$insertNames] = $this->prepareInsertSelectSubQuery($insertColumns, $this->db->getSchema());
571
        } else {
572
            $insertNames = \array_map([$this->db, 'quoteColumnName'], \array_keys($insertColumns));
573
        }
574
575
        $uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
576
        $uniqueNames = \array_map([$this->db, 'quoteColumnName'], $uniqueNames);
577
578
        if ($updateColumns !== true) {
579
            return [$uniqueNames, $insertNames, null];
580
        }
581
582
        return [$uniqueNames, $insertNames, \array_diff($insertNames, $uniqueNames)];
583
    }
584 54
585
    /**
586 54
     * Returns all column names belonging to constraints enforcing uniqueness (`PRIMARY KEY`, `UNIQUE INDEX`, etc.)
587
     * for the named table removing constraints which did not cover the specified column list.
588 54
     *
589
     * The column list will be unique by column names.
590
     *
591
     * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
592 54
     * @param string[] $columns source column list.
593 54
     * @param Constraint[] $constraints this parameter optionally recieves a matched constraint list.
594
     * The constraints will be unique by their column names.
595 54
     *
596 54
     * @throws Exception
597
     * @throws InvalidConfigException
598
     * @throws NotSupportedException
599 54
     *
600 53
     * @return array column list.
601 53
     */
602
    private function getTableUniqueColumnNames(string $name, array $columns, array &$constraints = []): array
603
    {
604
        $schema = $this->db->getSchema();
605 54
606
        if (!$schema instanceof ConstraintFinderInterface) {
607
            return [];
608 54
        }
609 54
610 54
        $constraints = [];
611 54
        $primaryKey = $schema->getTablePrimaryKey($name);
612 54
613
        if ($primaryKey !== null) {
614 54
            $constraints[] = $primaryKey;
615
        }
616 54
617
        foreach ($schema->getTableIndexes($name) as $constraint) {
618 54
            if ($constraint->isUnique()) {
619
                $constraints[] = $constraint;
620
            }
621 54
        }
622
623
        $constraints = \array_merge($constraints, $schema->getTableUniques($name));
624 54
625 54
        // Remove duplicates
626 54
        $constraints = \array_combine(
627 54
            \array_map(
628 54
                static function ($constraint) {
629 54
                    $columns = $constraint->getColumnNames();
630
                    sort($columns, SORT_STRING);
631 54
632 45
                    return \json_encode($columns);
633
                },
634
                $constraints
635 54
            ),
636 54
            $constraints
637
        );
638
639
        $columnNames = [];
640 54
641
        // Remove all constraints which do not cover the specified column list
642
        $constraints = \array_values(
643
            \array_filter(
644
                $constraints,
0 ignored issues
show
Bug introduced by
It seems like $constraints can also be of type false; however, parameter $input of array_filter() 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

644
                /** @scrutinizer ignore-type */ $constraints,
Loading history...
645
                static function ($constraint) use ($schema, $columns, &$columnNames) {
646
                    $constraintColumnNames = \array_map([$schema, 'quoteColumnName'], $constraint->getColumnNames());
647
                    $result = !\array_diff($constraintColumnNames, $columns);
648
649
                    if ($result) {
650
                        $columnNames = \array_merge($columnNames, $constraintColumnNames);
651
                    }
652
653
                    return $result;
654
                }
655
            )
656
        );
657
658
        return \array_unique($columnNames);
659
    }
660
661
    /**
662
     * Creates an UPDATE SQL statement.
663
     *
664
     * For example,
665
     *
666
     * ```php
667
     * $params = [];
668
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
669 19
     * ```
670
     *
671 19
     * The method will properly escape the table and column names.
672 19
     *
673 19
     * @param string $table the table to be updated.
674
     * @param array $columns the column data (name => value) to be updated.
675 19
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
676
     * {@see Query::where()} on how to specify condition.
677
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
678
     * DB command later.
679
     *
680
     * @throws Exception
681
     * @throws InvalidArgumentException
682
     * @throws InvalidConfigException
683
     * @throws NotSupportedException
684
     *
685
     * @return string the UPDATE SQL.
686
     */
687
    public function update(string $table, array $columns, $condition, array &$params = []): string
688
    {
689
        [$lines, $params] = $this->prepareUpdateSets($table, $columns, $params);
690
        $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
691
        $where = $this->buildWhere($condition, $params);
692
693
        return ($where === '') ? $sql : ($sql . ' ' . $where);
694 44
    }
695
696 44
    /**
697
     * Prepares a `SET` parts for an `UPDATE` SQL statement.
698 44
     *
699
     * @param string $table the table to be updated.
700 44
     * @param array $columns the column data (name => value) to be updated.
701
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
702 44
     * DB command later.
703 44
     *
704 44
     * @throws Exception
705 35
     * @throws InvalidArgumentException
706
     * @throws InvalidConfigException
707 24
     * @throws NotSupportedException
708
     *
709
     * @return array an array `SET` parts for an `UPDATE` SQL statement (the first array element) and params (the second
710 44
     * array element).
711
     */
712
    protected function prepareUpdateSets(string $table, array $columns, array $params = []): array
713 44
    {
714
        $tableSchema = $this->db->getTableSchema($table);
715
716
        $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
717
718
        $sets = [];
719
720
        foreach ($columns as $name => $value) {
721
            $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
722
            if ($value instanceof ExpressionInterface) {
723
                $placeholder = $this->buildExpression($value, $params);
724
            } else {
725
                $placeholder = $this->bindParam($value, $params);
726
            }
727
728
            $sets[] = $this->db->quoteColumnName($name) . '=' . $placeholder;
729
        }
730
731
        return [$sets, $params];
732
    }
733
734
    /**
735
     * Creates a DELETE SQL statement.
736
     *
737
     * For example,
738
     *
739
     * ```php
740 7
     * $sql = $queryBuilder->delete('user', 'status = 0');
741
     * ```
742 7
     *
743 7
     * The method will properly escape the table and column names.
744
     *
745 7
     * @param string $table the table where the data will be deleted from.
746
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
747
     * {@see Query::where()} on how to specify condition.
748
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
749
     * DB command later.
750
     *
751
     * @throws Exception
752
     * @throws InvalidArgumentException
753
     * @throws InvalidConfigException
754
     * @throws NotSupportedException
755
     *
756
     * @return string the DELETE SQL.
757
     */
758
    public function delete(string $table, $condition, array &$params): string
759
    {
760
        $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
761
        $where = $this->buildWhere($condition, $params);
762
763
        return ($where === '') ? $sql : ($sql . ' ' . $where);
764
    }
765
766
    /**
767
     * Builds a SQL statement for creating a new DB table.
768
     *
769
     * The columns in the new  table should be specified as name-definition pairs (e.g. 'name' => 'string'), where name
770
     * stands for a column name which will be properly quoted by the method, and definition stands for the column type
771
     * which can contain an abstract DB type.
772
     *
773
     * The {@see getColumnType()} method will be invoked to convert any abstract type into a physical one.
774
     *
775
     * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly inserted
776
     * into the generated SQL.
777
     *
778
     * For example,
779
     *
780 36
     * ```php
781
     * $sql = $queryBuilder->createTable('user', [
782 36
     *  'id' => 'pk',
783 36
     *  'name' => 'string',
784 36
     *  'age' => 'integer',
785 36
     * ]);
786
     * ```
787 3
     *
788
     * @param string $table the name of the table to be created. The name will be properly quoted by the method.
789
     * @param array $columns the columns (name => definition) in the new table.
790
     * @param string|null $options additional SQL fragment that will be appended to the generated SQL.
791 36
     *
792
     * @throws Exception
793 36
     * @throws InvalidConfigException
794
     * @throws NotSupportedException
795
     *
796
     * @return string the SQL statement for creating a new DB table.
797
     */
798
    public function createTable(string $table, array $columns, ?string $options = null): string
799
    {
800
        $cols = [];
801
        foreach ($columns as $name => $type) {
802
            if (\is_string($name)) {
803
                $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
804
            } else {
805
                $cols[] = "\t" . $type;
806
            }
807
        }
808 2
809
        $sql = 'CREATE TABLE ' . $this->db->quoteTableName($table) . " (\n" . \implode(",\n", $cols) . "\n)";
810 2
811
        return ($options === null) ? $sql : ($sql . ' ' . $options);
812
    }
813
814
    /**
815
     * Builds a SQL statement for renaming a DB table.
816
     *
817
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
818
     * @param string $newName the new table name. The name will be properly quoted by the method.
819
     *
820
     * @throws Exception
821
     * @throws InvalidConfigException
822
     * @throws NotSupportedException
823
     *
824 9
     * @return string the SQL statement for renaming a DB table.
825
     */
826 9
    public function renameTable(string $oldName, string $newName): string
827
    {
828
        return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName);
829
    }
830
831
    /**
832
     * Builds a SQL statement for dropping a DB table.
833
     *
834
     * @param string $table the table to be dropped. The name will be properly quoted by the method.
835
     *
836
     * @throws Exception
837
     * @throws InvalidConfigException
838
     * @throws NotSupportedException
839
     *
840
     * @return string the SQL statement for dropping a DB table.
841
     */
842 6
    public function dropTable($table): string
843
    {
844 6
        return 'DROP TABLE ' . $this->db->quoteTableName($table);
845 4
    }
846
847
    /**
848 6
     * Builds a SQL statement for adding a primary key constraint to an existing table.
849 6
     *
850
     * @param string $name the name of the primary key constraint.
851
     * @param string $table the table that the primary key constraint will be added to.
852 6
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
853 6
     *
854 6
     * @throws Exception
855
     * @throws InvalidConfigException
856
     * @throws NotSupportedException
857
     *
858
     * @return string the SQL statement for adding a primary key constraint to an existing table.
859
     */
860
    public function addPrimaryKey(string $name, string $table, $columns): string
861
    {
862
        if (\is_string($columns)) {
863
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
864
        }
865
866
        foreach ($columns as $i => $col) {
867
            $columns[$i] = $this->db->quoteColumnName($col);
868
        }
869 2
870
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
871 2
            . $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
872 2
            . \implode(', ', $columns) . ')';
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type false; 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

872
            . \implode(', ', /** @scrutinizer ignore-type */ $columns) . ')';
Loading history...
873
    }
874
875
    /**
876
     * Builds a SQL statement for removing a primary key constraint to an existing table.
877
     *
878
     * @param string $name the name of the primary key constraint to be removed.
879
     * @param string $table the table that the primary key constraint will be removed from.
880
     *
881
     * @throws Exception
882
     * @throws InvalidConfigException
883
     * @throws NotSupportedException
884
     *
885
     * @return string the SQL statement for removing a primary key constraint from an existing table.
886 1
     */
887
    public function dropPrimaryKey(string $name, string $table): string
888 1
    {
889
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
890
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
891
    }
892
893
    /**
894
     * Builds a SQL statement for truncating a DB table.
895
     *
896
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
897
     *
898
     * @throws Exception
899
     * @throws InvalidConfigException
900
     * @throws NotSupportedException
901
     *
902
     * @return string the SQL statement for truncating a DB table.
903
     */
904
    public function truncateTable(string $table): string
905
    {
906
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table);
907
    }
908
909 3
    /**
910
     * Builds a SQL statement for adding a new DB column.
911 3
     *
912 3
     * @param string $table the table that the new column will be added to. The table name will be properly quoted by
913 3
     * the method.
914
     * @param string $column the name of the new column. The name will be properly quoted by the method.
915
     * @param string $type the column type. The {@see getColumnType()} method will be invoked to convert abstract column
916
     * type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
917
     * generated SQL.
918
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
919
     * 'varchar(255) not null'.
920
     *
921
     * @throws Exception
922
     * @throws InvalidConfigException
923
     * @throws NotSupportedException
924
     *
925
     * @return string the SQL statement for adding a new column.
926
     */
927
    public function addColumn(string $table, string $column, string $type): string
928
    {
929
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
930
            . ' ADD ' . $this->db->quoteColumnName($column) . ' '
931
            . $this->getColumnType($type);
932
    }
933
934
    /**
935
     * Builds a SQL statement for dropping a DB column.
936
     *
937
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
938
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
939
     *
940
     * @throws Exception
941
     * @throws InvalidConfigException
942
     * @throws NotSupportedException
943
     *
944
     * @return string the SQL statement for dropping a DB column.
945
     */
946
    public function dropColumn(string $table, string $column): string
947
    {
948
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
949
            . ' DROP COLUMN ' . $this->db->quoteColumnName($column);
950
    }
951
952
    /**
953
     * Builds a SQL statement for renaming a column.
954
     *
955
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
956
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
957
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
958
     *
959
     * @throws Exception
960
     * @throws InvalidConfigException
961
     * @throws NotSupportedException
962
     *
963
     * @return string the SQL statement for renaming a DB column.
964
     */
965
    public function renameColumn(string $table, string $oldName, string $newName): string
966
    {
967
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
968
            . ' RENAME COLUMN ' . $this->db->quoteColumnName($oldName)
969
            . ' TO ' . $this->db->quoteColumnName($newName);
970
    }
971 1
972
    /**
973 1
     * Builds a SQL statement for changing the definition of a column.
974 1
     *
975 1
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
976 1
     * method.
977
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
978
     * @param string $type the new column type. The {@see getColumnType()} method will be invoked to convert abstract
979
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
980
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
981
     * will become 'varchar(255) not null'.
982
     *
983
     * @throws Exception
984
     * @throws InvalidConfigException
985
     * @throws NotSupportedException
986
     *
987
     * @return string the SQL statement for changing the definition of a column.
988
     */
989
    public function alterColumn(string $table, string $column, string $type): string
990
    {
991
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
992
            . $this->db->quoteColumnName($column) . ' '
993
            . $this->db->quoteColumnName($column) . ' '
994
            . $this->getColumnType($type);
995
    }
996
997
    /**
998
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
999
     * The method will properly quote the table and column names.
1000
     *
1001
     * @param string $name the name of the foreign key constraint.
1002 8
     * @param string $table the table that the foreign key constraint will be added to.
1003
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
1004
     * multiple columns, separate them with commas or use an array to represent them.
1005
     * @param string $refTable the table that the foreign key references to.
1006
     * @param string|array $refColumns the name of the column that the foreign key references to. If there are multiple
1007
     * columns, separate them with commas or use an array to represent them.
1008
     * @param string|null $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
1009
     * SET DEFAULT, SET NULL
1010
     * @param string|null $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
1011 8
     * SET DEFAULT, SET NULL
1012 8
     *
1013 8
     * @throws Exception
1014 8
     * @throws InvalidArgumentException
1015 8
     * @throws InvalidConfigException
1016
     * @throws NotSupportedException
1017 8
     *
1018 4
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
1019
     */
1020
    public function addForeignKey(
1021 8
        string $name,
1022 4
        string $table,
1023
        $columns,
1024
        string $refTable,
1025 8
        $refColumns,
1026
        ?string $delete = null,
1027
        ?string $update = null
1028
    ): string {
1029
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
1030
            . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
1031
            . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
1032
            . ' REFERENCES ' . $this->db->quoteTableName($refTable)
1033
            . ' (' . $this->buildColumns($refColumns) . ')';
1034
1035
        if ($delete !== null) {
1036
            $sql .= ' ON DELETE ' . $delete;
1037
        }
1038
1039
        if ($update !== null) {
1040
            $sql .= ' ON UPDATE ' . $update;
1041 3
        }
1042
1043 3
        return $sql;
1044 3
    }
1045
1046
    /**
1047
     * Builds a SQL statement for dropping a foreign key constraint.
1048
     *
1049
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by
1050
     * the method.
1051
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
1052
     *
1053
     * @throws Exception
1054
     * @throws InvalidConfigException
1055
     * @throws NotSupportedException
1056
     *
1057
     * @return string the SQL statement for dropping a foreign key constraint.
1058
     */
1059
    public function dropForeignKey(string $name, string $table): string
1060
    {
1061
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1062
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1063
    }
1064
1065
    /**
1066
     * Builds a SQL statement for creating a new index.
1067
     *
1068
     * @param string $name the name of the index. The name will be properly quoted by the method.
1069
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
1070
     * the method.
1071
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
1072
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
1073
     * method, unless a parenthesis is found in the name.
1074
     * @param bool $unique whether to add UNIQUE constraint on the created index.
1075
     *
1076
     * @throws Exception
1077
     * @throws InvalidArgumentException
1078
     * @throws InvalidConfigException
1079
     * @throws NotSupportedException
1080
     *
1081
     * @return string the SQL statement for creating a new index.
1082
     */
1083
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
1084
    {
1085 4
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
1086
            . $this->db->quoteTableName($name) . ' ON '
1087 4
            . $this->db->quoteTableName($table)
1088
            . ' (' . $this->buildColumns($columns) . ')';
1089
    }
1090
1091
    /**
1092
     * Builds a SQL statement for dropping an index.
1093
     *
1094
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
1095
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
1096
     *
1097
     * @throws Exception
1098
     * @throws InvalidConfigException
1099
     * @throws NotSupportedException
1100
     *
1101
     * @return string the SQL statement for dropping an index.
1102
     */
1103
    public function dropIndex(string $name, string $table): string
1104
    {
1105 6
        return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
1106
    }
1107 6
1108 4
    /**
1109
     * Creates a SQL command for adding an unique constraint to an existing table.
1110 6
     *
1111 6
     * @param string $name the name of the unique constraint. The name will be properly quoted by the method.
1112
     * @param string $table the table that the unique constraint will be added to. The name will be properly quoted by
1113
     * the method.
1114 6
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
1115 6
     * multiple columns, separate them with commas. The name will be properly quoted by the method.
1116 6
     *
1117
     * @throws Exception
1118
     * @throws InvalidConfigException
1119
     * @throws NotSupportedException
1120
     *
1121
     * @return string the SQL statement for adding an unique constraint to an existing table.
1122
     */
1123
    public function addUnique(string $name, string $table, $columns): string
1124
    {
1125
        if (\is_string($columns)) {
1126
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1127
        }
1128
        foreach ($columns as $i => $col) {
1129
            $columns[$i] = $this->db->quoteColumnName($col);
1130
        }
1131
1132
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1133 2
            . $this->db->quoteColumnName($name) . ' UNIQUE ('
1134
            . implode(', ', $columns) . ')';
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type false; 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

1134
            . implode(', ', /** @scrutinizer ignore-type */ $columns) . ')';
Loading history...
1135 2
    }
1136 2
1137
    /**
1138
     * Creates a SQL command for dropping an unique constraint.
1139
     *
1140
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
1141
     * method.
1142
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by the
1143
     * method.
1144
     *
1145
     * @throws Exception
1146
     * @throws InvalidConfigException
1147
     * @throws NotSupportedException
1148
     *
1149
     * @return string the SQL statement for dropping an unique constraint.
1150
     */
1151
    public function dropUnique(string $name, string $table): string
1152
    {
1153
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1154 2
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1155
    }
1156 2
1157 2
    /**
1158
     * Creates a SQL command for adding a check constraint to an existing table.
1159
     *
1160
     * @param string $name the name of the check constraint.
1161
     * The name will be properly quoted by the method.
1162
     * @param string $table the table that the check constraint will be added to.
1163
     * The name will be properly quoted by the method.
1164
     * @param string $expression the SQL of the `CHECK` constraint.
1165
     *
1166
     * @throws Exception
1167
     * @throws InvalidConfigException
1168
     * @throws NotSupportedException
1169
     *
1170
     * @return string the SQL statement for adding a check constraint to an existing table.
1171
     */
1172
    public function addCheck(string $name, string $table, string $expression): string
1173
    {
1174 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
1175
            . $this->db->quoteColumnName($name) . ' CHECK (' . $this->db->quoteSql($expression) . ')';
1176 2
    }
1177 2
1178
    /**
1179
     * Creates a SQL command for dropping a check constraint.
1180
     *
1181
     * @param string $name the name of the check constraint to be dropped.
1182
     * The name will be properly quoted by the method.
1183
     * @param string $table the table whose check constraint is to be dropped.
1184
     * The name will be properly quoted by the method.
1185
     *
1186
     * @throws Exception
1187
     * @throws InvalidConfigException
1188
     * @throws NotSupportedException
1189
     *
1190
     * @return string the SQL statement for dropping a check constraint.
1191
     */
1192
    public function dropCheck(string $name, string $table): string
1193
    {
1194
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
1195
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
1196
    }
1197
1198
    /**
1199
     * Creates a SQL command for adding a default value constraint to an existing table.
1200
     *
1201
     * @param string $name the name of the default value constraint.
1202
     * The name will be properly quoted by the method.
1203
     * @param string $table the table that the default value constraint will be added to.
1204
     * The name will be properly quoted by the method.
1205
     * @param string $column the name of the column to that the constraint will be added on.
1206
     * The name will be properly quoted by the method.
1207
     * @param mixed $value default value.
1208
     *
1209
     * @throws Exception
1210
     * @throws InvalidConfigException
1211
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
1212
     *
1213
     * @return string the SQL statement for adding a default value constraint to an existing table.
1214
     */
1215
    public function addDefaultValue(string $name, string $table, string $column, $value): string
1216
    {
1217
        throw new NotSupportedException(
1218
            $this->db->getDriverName() . ' does not support adding default value constraints.'
1219
        );
1220
    }
1221
1222
    /**
1223
     * Creates a SQL command for dropping a default value constraint.
1224
     *
1225
     * @param string $name the name of the default value constraint to be dropped.
1226
     * The name will be properly quoted by the method.
1227
     * @param string $table the table whose default value constraint is to be dropped.
1228
     * The name will be properly quoted by the method.
1229
     *
1230
     * @throws Exception
1231
     * @throws InvalidConfigException
1232
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
1233
     *
1234
     * @return string the SQL statement for dropping a default value constraint.
1235
     */
1236
    public function dropDefaultValue(string $name, string $table): string
1237
    {
1238
        throw new NotSupportedException(
1239
            $this->db->getDriverName() . ' does not support dropping default value constraints.'
1240
        );
1241
    }
1242
1243
    /**
1244
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
1245
     *
1246
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
1247
     * or 1.
1248
     *
1249
     * @param string $tableName the name of the table whose primary key sequence will be reset.
1250
     * @param array|string $value the value for the primary key of the next new row inserted. If this is not set, the
1251
     * next new row's primary key will have a value 1.
1252
     *
1253
     * @throws Exception
1254
     * @throws InvalidConfigException
1255
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
1256
     *
1257
     * @return string the SQL statement for resetting sequence.
1258
     */
1259
    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

1259
    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...
1260
    {
1261
        throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.');
1262
    }
1263
1264
    /**
1265
     * Builds a SQL statement for enabling or disabling integrity check.
1266
     *
1267
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
1268
     * @param string $table the table name. Defaults to empty string, meaning that no table will be changed.
1269
     *
1270
     * @param bool $check whether to turn on or off the integrity check.
1271
     *
1272
     * @throws Exception
1273
     * @throws InvalidConfigException
1274
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
1275
     *
1276
     * @return string the SQL statement for checking integrity.
1277
     */
1278
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
0 ignored issues
show
Unused Code introduced by
The parameter $check is not used and could be removed. ( Ignorable by Annotation )

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

1278
    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...
1279
    {
1280
        throw new NotSupportedException(
1281
            $this->db->getDriverName() . ' does not support enabling/disabling integrity check.'
1282 2
        );
1283
    }
1284 2
1285 2
    /**
1286
     * Builds a SQL command for adding comment to column.
1287
     *
1288
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1289
     * method.
1290
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1291
     * method.
1292
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1293
     *
1294
     * @throws Exception
1295
     * @throws InvalidConfigException
1296
     * @throws NotSupportedException
1297
     *
1298
     * @return string the SQL statement for adding comment on column.
1299
     */
1300
    public function addCommentOnColumn(string $table, string $column, string $comment): string
1301 1
    {
1302
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column)
1303 1
            . ' IS ' . $this->db->quoteValue($comment);
1304
    }
1305
1306
    /**
1307
     * Builds a SQL command for adding comment to table.
1308
     *
1309
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1310
     * method.
1311
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
1312
     *
1313
     * @throws Exception
1314
     * @throws InvalidConfigException
1315
     * @throws NotSupportedException
1316
     *
1317
     * @return string the SQL statement for adding comment on table.
1318
     */
1319
    public function addCommentOnTable(string $table, string $comment): string
1320 2
    {
1321
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS ' . $this->db->quoteValue($comment);
1322 2
    }
1323 2
1324
    /**
1325
     * Builds a SQL command for adding comment to column.
1326
     *
1327
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1328
     * method.
1329
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
1330
     * method.
1331
     *
1332
     * @throws Exception
1333
     * @throws InvalidConfigException
1334
     * @throws NotSupportedException
1335
     *
1336
     * @return string the SQL statement for adding comment on column
1337
     */
1338 1
    public function dropCommentFromColumn(string $table, string $column): string
1339
    {
1340 1
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column)
1341
            . ' IS NULL';
1342
    }
1343
1344
    /**
1345
     * Builds a SQL command for adding comment to table.
1346
     *
1347
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
1348
     * method.
1349
     *
1350
     * @throws Exception
1351
     * @throws InvalidConfigException
1352
     * @throws NotSupportedException
1353
     *
1354
     * @return string the SQL statement for adding comment on column*
1355
     */
1356
    public function dropCommentFromTable(string $table): string
1357 3
    {
1358
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS NULL';
1359 3
    }
1360 3
1361 3
    /**
1362
     * Creates a SQL View.
1363 3
     *
1364 3
     * @param string $viewName the name of the view to be created.
1365 3
     * @param string|Query $subQuery the select statement which defines the view.
1366
     *
1367 3
     * This can be either a string or a {@see Query} object.
1368
     *
1369
     * @throws Exception
1370 3
     * @throws InvalidConfigException
1371
     * @throws NotSupportedException
1372
     *
1373
     * @return string the `CREATE VIEW` SQL statement.
1374
     */
1375
    public function createView(string $viewName, $subQuery): string
1376
    {
1377
        if ($subQuery instanceof Query) {
1378
            [$rawQuery, $params] = $this->build($subQuery);
1379
            \array_walk(
1380
                $params,
1381
                function (&$param) {
1382
                    $param = $this->db->quoteValue($param);
1383
                }
1384 3
            );
1385
            $subQuery = strtr($rawQuery, $params);
1386 3
        }
1387
1388
        return 'CREATE VIEW ' . $this->db->quoteTableName($viewName) . ' AS ' . $subQuery;
1389
    }
1390
1391
    /**
1392
     * Drops a SQL View.
1393
     *
1394
     * @param string $viewName the name of the view to be dropped.
1395
     *
1396
     * @throws Exception
1397
     * @throws InvalidConfigException
1398
     * @throws NotSupportedException
1399
     *
1400
     * @return string the `DROP VIEW` SQL statement.
1401
     */
1402
    public function dropView(string $viewName): string
1403
    {
1404
        return 'DROP VIEW ' . $this->db->quoteTableName($viewName);
1405
    }
1406
1407
    /**
1408
     * Converts an abstract column type into a physical column type.
1409
     *
1410
     * The conversion is done using the type map specified in {@see typeMap}.
1411
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
1412
     * physical types):
1413
     *
1414
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY
1415
     *    KEY"
1416
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT
1417
     *    PRIMARY KEY"
1418
     * - `upk`: an unsigned auto-incremental primary key type, will be converted into "int(10) UNSIGNED NOT NULL
1419
     *    AUTO_INCREMENT PRIMARY KEY"
1420
     * - `char`: char type, will be converted into "char(1)"
1421
     * - `string`: string type, will be converted into "varchar(255)"
1422
     * - `text`: a long string type, will be converted into "text"
1423
     * - `smallint`: a small integer type, will be converted into "smallint(6)"
1424
     * - `integer`: integer type, will be converted into "int(11)"
1425
     * - `bigint`: a big integer type, will be converted into "bigint(20)"
1426
     * - `boolean`: boolean type, will be converted into "tinyint(1)"
1427
     * - `float``: float number type, will be converted into "float"
1428
     * - `decimal`: decimal number type, will be converted into "decimal"
1429
     * - `datetime`: datetime type, will be converted into "datetime"
1430
     * - `timestamp`: timestamp type, will be converted into "timestamp"
1431
     * - `time`: time type, will be converted into "time"
1432
     * - `date`: date type, will be converted into "date"
1433
     * - `money`: money type, will be converted into "decimal(19,4)"
1434
     * - `binary`: binary data type, will be converted into "blob"
1435 39
     *
1436
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only the first
1437 39
     * part will be converted, and the rest of the parts will be appended to the converted result.
1438 6
     *
1439
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
1440
     *
1441 39
     * For some of the abstract types you can also specify a length or precision constraint by appending it in round
1442 23
     * brackets directly to the type.
1443
     *
1444
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. If the underlying DBMS does
1445 25
     * not support these kind of constraints for a type it will be ignored.
1446 10
     *
1447 8
     * If a type cannot be found in {@see typeMap}, it will be returned without any change.
1448
     *
1449 8
     * @param string|ColumnSchemaBuilder $type abstract column type
1450 8
     *
1451 10
     * @return string physical column type.
1452
     */
1453 21
    public function getColumnType($type): string
1454 18
    {
1455 18
        if ($type instanceof ColumnSchemaBuilder) {
1456
            $type = $type->__toString();
1457
        }
1458
1459 6
        if (isset($this->typeMap[$type])) {
1460
            return $this->typeMap[$type];
1461
        }
1462
1463
        if (\preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
1464
            if (isset($this->typeMap[$matches[1]])) {
1465
                return \preg_replace(
1466
                    '/\(.+\)/',
1467
                    '(' . $matches[2] . ')',
1468
                    $this->typeMap[$matches[1]]
1469
                ) . $matches[3];
1470
            }
1471
        } elseif (\preg_match('/^(\w+)\s+/', $type, $matches)) {
1472
            if (isset($this->typeMap[$matches[1]])) {
1473
                return \preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
1474
            }
1475 476
        }
1476
1477
        return $type;
1478
    }
1479
1480
    /**
1481 476
     * @param array $columns
1482
     * @param array $params the binding parameters to be populated.
1483 476
     * @param bool|null $distinct
1484
     * @param string $selectOption
1485
     *
1486
     * @throws Exception
1487 476
     * @throws InvalidArgumentException
1488 373
     * @throws InvalidConfigException
1489
     * @throws NotSupportedException
1490
     *
1491 147
     * @return string the SELECT clause built from {@see Query::$select}.
1492 147
     */
1493 42
    public function buildSelect(
1494 6
        array $columns,
1495
        array &$params,
1496 42
        ?bool $distinct = false,
1497
        string $selectOption = null
1498 138
    ): string {
1499
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
1500
1501 138
        if ($selectOption !== null) {
1502 12
            $select .= ' ' . $selectOption;
1503 9
        }
1504
1505 12
        if (empty($columns)) {
1506 135
            return $select . ' *';
1507 121
        }
1508
1509
        foreach ($columns as $i => $column) {
1510
            if ($column instanceof ExpressionInterface) {
1511
                if (\is_int($i)) {
1512 121
                    $columns[$i] = $this->buildExpression($column, $params);
1513
                } else {
1514
                    $columns[$i] = $this->buildExpression($column, $params) . ' AS ' . $this->db->quoteColumnName($i);
1515
                }
1516
            } elseif ($column instanceof Query) {
1517 147
                [$sql, $params] = $this->build($column, $params);
1518
                $columns[$i] = "($sql) AS " . $this->db->quoteColumnName($i);
1519
            } elseif (\is_string($i) && $i !== $column) {
1520
                if (\strpos($column, '(') === false) {
1521
                    $column = $this->db->quoteColumnName($column);
1522
                }
1523
                $columns[$i] = "$column AS " . $this->db->quoteColumnName($i);
1524
            } elseif (\strpos($column, '(') === false) {
1525
                if (\preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
1526
                    $columns[$i] = $this->db->quoteColumnName(
1527
                        $matches[1]
1528
                    ) . ' AS ' . $this->db->quoteColumnName($matches[2]);
1529
                } else {
1530 476
                    $columns[$i] = $this->db->quoteColumnName($column);
1531
                }
1532 476
            }
1533 347
        }
1534
1535
        return $select . ' ' . implode(', ', $columns);
1536 166
    }
1537
1538 166
    /**
1539
     * @param array|null $tables
1540
     * @param array $params the binding parameters to be populated
1541
     *
1542
     * @throws Exception
1543
     * @throws InvalidConfigException
1544
     * @throws NotSupportedException
1545
     *
1546
     * @return string the FROM clause built from {@see Query::$from}.
1547
     */
1548
    public function buildFrom(?array $tables, array &$params): string
1549 476
    {
1550
        if (empty($tables)) {
1551 476
            return '';
1552 473
        }
1553
1554
        $tables = $this->quoteTableNames($tables, $params);
1555 9
1556 9
        return 'FROM ' . \implode(', ', $tables);
1557
    }
1558
1559
    /**
1560
     * @param array $joins
1561
     * @param array $params the binding parameters to be populated
1562
     *
1563
     * @throws Exception if the $joins parameter is not in proper format
1564
     *
1565 9
     * @return string the JOIN clause built from {@see Query::$join}.
1566
     */
1567 9
    public function buildJoin(array $joins, array &$params): string
1568 9
    {
1569 9
        if (empty($joins)) {
1570
            return '';
1571 9
        }
1572 9
1573 9
        foreach ($joins as $i => $join) {
1574 9
            if (!\is_array($join) || !isset($join[0], $join[1])) {
1575
                throw new Exception(
1576
                    'A join clause must be specified as an array of join type, join table, and optionally join '
1577
                    . 'condition.'
1578
                );
1579 9
            }
1580
1581
            /* 0:join type, 1:join table, 2:on-condition (optional) */
1582
1583
            [$joinType, $table] = $join;
1584
1585
            $tables = $this->quoteTableNames((array) $table, $params);
1586
            $table = \reset($tables);
1587
            $joins[$i] = "$joinType $table";
1588
1589
            if (isset($join[2])) {
1590
                $condition = $this->buildCondition($join[2], $params);
1591
                if ($condition !== '') {
1592
                    $joins[$i] .= ' ON ' . $condition;
1593
                }
1594 166
            }
1595
        }
1596 166
1597 166
        return \implode($this->separator, $joins);
1598 7
    }
1599 7
1600 166
    /**
1601 6
     * Quotes table names passed.
1602 3
     *
1603
     * @param array $tables
1604 6
     * @param array $params
1605 166
     *
1606 159
     * @throws Exception
1607 159
     * @throws InvalidConfigException
1608 18
     * @throws NotSupportedException
1609 18
     *
1610
     * @return array
1611 144
     */
1612
    private function quoteTableNames(array $tables, array &$params): array
1613
    {
1614
        foreach ($tables as $i => $table) {
1615
            if ($table instanceof Query) {
1616
                [$sql, $params] = $this->build($table, $params);
1617 166
                $tables[$i] = "($sql) " . $this->db->quoteTableName((string) $i);
1618
            } elseif (\is_string($i)) {
1619
                if (\strpos($table, '(') === false) {
1620
                    $table = $this->db->quoteTableName($table);
1621
                }
1622
                $tables[$i] = "$table " . $this->db->quoteTableName($i);
1623
            } elseif (\is_string($table)) {
1624
                if (\strpos($table, '(') === false) {
1625
                    if ($tableWithAlias = $this->extractAlias($table)) { // with alias
1626
                        $tables[$i] = $this->db->quoteTableName($tableWithAlias[1]) . ' '
1627
                            . $this->db->quoteTableName($tableWithAlias[2]);
1628 494
                    } else {
1629
                        $tables[$i] = $this->db->quoteTableName($table);
1630 494
                    }
1631
                }
1632 494
            }
1633
        }
1634
1635
        return $tables;
1636
    }
1637
1638
    /**
1639
     * @param string|array $condition
1640
     * @param array $params the binding parameters to be populated.
1641
     *
1642
     * @throws InvalidArgumentException
1643
     *
1644
     * @return string the WHERE clause built from {@see Query::$where}.
1645
     */
1646 476
    public function buildWhere($condition, array &$params = []): string
1647
    {
1648 476
        $where = $this->buildCondition($condition, $params);
1649 473
1650
        return ($where === '') ? '' : ('WHERE ' . $where);
1651 6
    }
1652 6
1653 3
    /**
1654 3
     * @param array $columns
1655 6
     * @param array $params the binding parameters to be populated
1656 6
     *
1657
     * @throws Exception
1658
     * @throws InvalidArgumentException
1659
     * @throws InvalidConfigException
1660 6
     * @throws NotSupportedException
1661
     *
1662
     * @return string the GROUP BY clause
1663
     */
1664
    public function buildGroupBy(array $columns, array &$params = []): string
1665
    {
1666
        if (empty($columns)) {
1667
            return '';
1668
        }
1669
        foreach ($columns as $i => $column) {
1670
            if ($column instanceof ExpressionInterface) {
1671 476
                $columns[$i] = $this->buildExpression($column);
1672
                $params = \array_merge($params, $column->getParams());
0 ignored issues
show
Bug introduced by
The method getParams() does not exist on Yiisoft\Db\Expression\ExpressionInterface. It seems like you code against a sub-type of Yiisoft\Db\Expression\ExpressionInterface such as Yiisoft\Db\Expression\Expression. ( Ignorable by Annotation )

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

1672
                $params = \array_merge($params, $column->/** @scrutinizer ignore-call */ getParams());
Loading history...
1673 476
            } elseif (\strpos($column, '(') === false) {
1674
                $columns[$i] = $this->db->quoteColumnName($column);
1675 476
            }
1676
        }
1677
1678
        return 'GROUP BY ' . \implode(', ', $columns);
1679
    }
1680
1681
    /**
1682
     * @param string|array $condition
1683
     * @param array $params the binding parameters to be populated
1684
     *
1685
     * @throws InvalidArgumentException
1686
     *
1687
     * @return string the HAVING clause built from {@see Query::$having}.
1688
     */
1689
    public function buildHaving($condition, array &$params = []): string
1690
    {
1691
        $having = $this->buildCondition($condition, $params);
1692
1693
        return ($having === '') ? '' : ('HAVING ' . $having);
1694
    }
1695 476
1696
    /**
1697
     * Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
1698
     *
1699
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
1700
     * @param array $orderBy the order by columns. See {@see Query::orderBy} for more details on how to specify this
1701
     * parameter.
1702 476
     * @param int|object|null $limit the limit number. See {@see Query::limit} for more details.
1703 476
     * @param int|object|null $offset the offset number. See {@see Query::offset} for more details.
1704 15
     * @param array $params the binding parameters to be populated
1705
     *
1706 476
     * @throws Exception
1707 476
     * @throws InvalidArgumentException
1708 26
     * @throws InvalidConfigException
1709
     * @throws NotSupportedException
1710
     *
1711 476
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any).
1712
     */
1713
    public function buildOrderByAndLimit(
1714
        string $sql,
1715
        array $orderBy,
1716
        $limit,
1717
        $offset,
1718
        array &$params = []
1719
    ): string {
1720
        $orderBy = $this->buildOrderBy($orderBy, $params);
1721
        if ($orderBy !== '') {
1722
            $sql .= $this->separator . $orderBy;
1723
        }
1724
        $limit = $this->buildLimit($limit, $offset);
1725 476
        if ($limit !== '') {
1726
            $sql .= $this->separator . $limit;
1727 476
        }
1728 464
1729
        return $sql;
1730
    }
1731 15
1732
    /**
1733 15
     * @param array $columns
1734 15
     * @param array $params the binding parameters to be populated
1735 3
     *
1736 3
     * @throws Exception
1737
     * @throws InvalidArgumentException
1738 15
     * @throws InvalidConfigException
1739
     * @throws NotSupportedException
1740
     *
1741
     * @return string the ORDER BY clause built from {@see Query::$orderBy}.
1742 15
     */
1743
    public function buildOrderBy(array $columns, array &$params = []): string
1744
    {
1745
        if (empty($columns)) {
1746
            return '';
1747
        }
1748
1749
        $orders = [];
1750
1751 187
        foreach ($columns as $name => $direction) {
1752
            if ($direction instanceof ExpressionInterface) {
1753 187
                $orders[] = $this->buildExpression($direction);
1754
                $params = \array_merge($params, $direction->getParams());
1755 187
            } else {
1756 9
                $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
1757
            }
1758
        }
1759 187
1760 1
        return 'ORDER BY ' . \implode(', ', $orders);
1761
    }
1762
1763 187
    /**
1764
     * @param int|object|null $limit
1765
     * @param int|object|null $offset
1766
     *
1767
     * @return string the LIMIT and OFFSET clauses
1768
     */
1769
    public function buildLimit($limit, $offset): string
1770
    {
1771
        $sql = '';
1772
1773 323
        if ($this->hasLimit($limit)) {
1774
            $sql = 'LIMIT ' . $limit;
0 ignored issues
show
Bug introduced by
Are you sure $limit of type integer|null|object can be used in concatenation? ( Ignorable by Annotation )

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

1774
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
1775 323
        }
1776
1777
        if ($this->hasOffset($offset)) {
1778
            $sql .= ' OFFSET ' . $offset;
0 ignored issues
show
Bug introduced by
Are you sure $offset of type integer|null|object can be used in concatenation? ( Ignorable by Annotation )

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

1778
            $sql .= ' OFFSET ' . /** @scrutinizer ignore-type */ $offset;
Loading history...
1779
        }
1780
1781
        return ltrim($sql);
1782
    }
1783
1784
    /**
1785 323
     * Checks to see if the given limit is effective.
1786
     *
1787 323
     * @param mixed $limit the given limit
1788
     *
1789
     * @return bool whether the limit is effective
1790
     */
1791
    protected function hasLimit($limit): bool
1792
    {
1793
        return ($limit instanceof ExpressionInterface) || \ctype_digit((string) $limit);
1794
    }
1795
1796 340
    /**
1797
     * Checks to see if the given offset is effective.
1798 340
     *
1799 340
     * @param mixed $offset the given offset
1800
     *
1801
     * @return bool whether the offset is effective
1802 6
     */
1803
    protected function hasOffset($offset): bool
1804 6
    {
1805 6
        return ($offset instanceof ExpressionInterface) || (\ctype_digit((string)$offset) && (string)$offset !== '0');
1806 6
    }
1807 6
1808
    /**
1809
     * @param array $unions
1810 6
     * @param array $params the binding parameters to be populated
1811
     *
1812
     * @return string the UNION clause built from {@see Query::$union}.
1813 6
     */
1814
    public function buildUnion(array $unions, array &$params): string
1815
    {
1816
        if (empty($unions)) {
1817
            return '';
1818
        }
1819
1820
        $result = '';
1821
1822
        foreach ($unions as $i => $union) {
1823
            $query = $union['query'];
1824
            if ($query instanceof Query) {
1825
                [$unions[$i]['query'], $params] = $this->build($query, $params);
1826
            }
1827
1828
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
1829
        }
1830 28
1831
        return \trim($result);
1832 28
    }
1833 22
1834
    /**
1835
     * Processes columns and properly quotes them if necessary.
1836
     *
1837 22
     * It will join all columns into a string with comma as separators.
1838 22
     *
1839
     * @param string|array $columns the columns to be processed
1840 22
     *
1841
     * @throws InvalidArgumentException
1842
     * @throws NotSupportedException
1843
     * @throws Exception
1844 28
     * @throws InvalidConfigException
1845 28
     *
1846
     * @return string the processing result
1847 28
     */
1848 28
    public function buildColumns($columns): string
1849
    {
1850
        if (!\is_array($columns)) {
1851
            if (\strpos($columns, '(') !== false) {
1852 28
                return $columns;
1853
            }
1854
1855
            $rawColumns = $columns;
1856
            $columns = \preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1857
1858
            if ($columns === false) {
1859
                throw new InvalidArgumentException("$rawColumns is not valid columns.");
1860
            }
1861
        }
1862
        foreach ($columns as $i => $column) {
1863
            if ($column instanceof ExpressionInterface) {
1864
                $columns[$i] = $this->buildExpression($column);
1865
            } elseif (\strpos($column, '(') === false) {
1866 494
                $columns[$i] = $this->db->quoteColumnName($column);
1867
            }
1868 494
        }
1869 358
1870 3
        return \implode(', ', $columns);
0 ignored issues
show
Bug introduced by
It seems like $columns can also be of type false and 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

1870
        return \implode(', ', /** @scrutinizer ignore-type */ $columns);
Loading history...
1871
    }
1872
1873 358
    /**
1874
     * Parses the condition specification and generates the corresponding SQL expression.
1875
     *
1876 494
     * @param string|array|ExpressionInterface $condition the condition specification.
1877 379
     * Please refer to {@see Query::where()} on how to specify a condition.
1878
     * @param array $params the binding parameters to be populated
1879
     *
1880 480
     * @throws InvalidArgumentException
1881
     *
1882
     * @return string the generated SQL expression
1883
     */
1884
    public function buildCondition($condition, array &$params = []): string
1885
    {
1886
        if (\is_array($condition)) {
1887
            if (empty($condition)) {
1888
                return '';
1889
            }
1890
1891
            $condition = $this->createConditionFromArray($condition);
1892
        }
1893
1894
        if ($condition instanceof ExpressionInterface) {
1895
            return $this->buildExpression($condition, $params);
1896 358
        }
1897
1898 358
        return (string) $condition;
1899 297
    }
1900 297
1901
    /**
1902
     * Transforms $condition defined in array format (as described in {@see Query::where()} to instance of
1903 297
     *
1904
     * {@see \Yiisoft\Db\Condition\ConditionInterface|ConditionInterface} according to {@see conditionClasses}
1905
     * map.
1906
     *
1907 102
     * @param string|array $condition
1908
     * {@see conditionClasses}
1909
     *
1910
     * @throws InvalidArgumentException
1911
     *
1912
     * @return ConditionInterface
1913
     */
1914
    public function createConditionFromArray($condition): ConditionInterface
1915
    {
1916
        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
1917 3
            $operator = strtoupper(array_shift($condition));
0 ignored issues
show
Bug introduced by
It seems like $condition can also be of type string; however, parameter $array of array_shift() 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

1917
            $operator = strtoupper(array_shift(/** @scrutinizer ignore-type */ $condition));
Loading history...
1918
1919 3
            if (isset($this->conditionClasses[$operator])) {
1920
                $className = $this->conditionClasses[$operator];
1921
            } else {
1922
                $className = SimpleCondition::class;
1923
            }
1924
1925
            /** @var ConditionInterface $className */
1926
            return $className::fromArrayDefinition($operator, $condition);
1927
        }
1928
1929
        // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
1930 334
        return new HashCondition($condition);
1931
    }
1932 334
1933 334
    /**
1934
     * Creates a SELECT EXISTS() SQL statement.
1935 334
     *
1936
     * @param string $rawSql the subquery in a raw form to select from.
1937
     *
1938
     * @return string the SELECT EXISTS() SQL statement.
1939
     */
1940
    public function selectExists(string $rawSql): string
1941
    {
1942
        return 'SELECT EXISTS(' . $rawSql . ')';
1943
    }
1944
1945 159
    /**
1946
     * Helper method to add $value to $params array using {@see PARAM_PREFIX}.
1947 159
     *
1948 18
     * @param string|int|null $value
1949
     * @param array $params passed by reference
1950
     *
1951 144
     * @return string the placeholder name in $params array
1952
     */
1953
    public function bindParam($value, array &$params = []): string
1954
    {
1955
        $phName = self::PARAM_PREFIX . \count($params);
1956
        $params[$phName] = $value;
1957
1958
        return $phName;
1959
    }
1960
1961
    /**
1962
     * Extracts table alias if there is one or returns false
1963
     *
1964
     * @param $table
1965
     *
1966
     * @return bool|array
1967
     */
1968
    protected function extractAlias($table)
1969
    {
1970
        if (\preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) {
1971
            return $matches;
1972
        }
1973
1974
        return false;
1975
    }
1976
1977
    public function buildWith($withs, &$params): string
1978
    {
1979
        if (empty($withs)) {
1980 476
            return '';
1981
        }
1982 476
1983 476
        $recursive = false;
1984
        $result = [];
1985
1986 6
        foreach ($withs as $i => $with) {
1987 6
            if ($with['recursive']) {
1988
                $recursive = true;
1989 6
            }
1990 6
1991 3
            $query = $with['query'];
1992
1993
            if ($query instanceof Query) {
1994 6
                [$with['query'], $params] = $this->build($query, $params);
1995 6
            }
1996 6
1997
            $result[] = $with['alias'] . ' AS (' . $with['query'] . ')';
1998
        }
1999 6
2000
        return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . \implode(', ', $result);
2001
    }
2002 6
2003
    public function buildWithQueries($withs, &$params): string
2004
    {
2005
        if (empty($withs)) {
2006
            return '';
2007
        }
2008 298
2009
        $recursive = false;
2010 298
        $result = [];
2011
2012
        foreach ($withs as $i => $with) {
2013
            if ($with['recursive']) {
2014
                $recursive = true;
2015
            }
2016
2017
            $query = $with['query'];
2018
            if ($query instanceof Query) {
2019
                [$with['query'], $params] = $this->build($query, $params);
2020
            }
2021
2022
            $result[] = $with['alias'] . ' AS (' . $with['query'] . ')';
2023
        }
2024
2025 1
        return 'WITH ' . ($recursive ? 'RECURSIVE ' : '') . \implode(', ', $result);
2026
    }
2027
2028
    /**
2029
     * @return Connection|null the database connection.
2030
     */
2031
    public function getDb(): ?Connection
2032
    {
2033
        return $this->db;
2034
    }
2035
2036
    /**
2037
     * @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...
2038
     *
2039
     * Defaults to an empty space. This is mainly used by {@see build()} when generating a SQL statement.
2040
     *
2041
     * @return void
2042
     */
2043
    public function setSeparator(string $separator): void
2044
    {
2045
        $this->separator = $separator;
2046
    }
2047
}
2048