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

QueryBuilder::dropView()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

1259
    public function resetSequence(/** @scrutinizer ignore-unused */ string $tableName, $value = null): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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

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

1278
    public function checkIntegrity(string $schema = '', string $table = '', /** @scrutinizer ignore-unused */ bool $check = true): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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

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

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

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

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

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

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

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

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

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

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

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2038
     *
2039
     * Defaults to an empty space. This is mainly used by {@see build()} when generating a SQL statement.
2040
     *
2041
     * @return void
2042
     */
2043
    public function setSeparator(string $separator): void
2044
    {
2045
        $this->separator = $separator;
2046
    }
2047
}
2048