Passed
Pull Request — master (#163)
by Wilmer
17:48 queued 02:54
created

QueryBuilder::buildSelect()   C

Complexity

Conditions 13
Paths 40

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 15.2812

Importance

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

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

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

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

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

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

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

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

1773
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
1774
        }
1775 323
1776
        if ($this->hasOffset($offset)) {
1777
            $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

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

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

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