GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 1501c6...0e2b6c )
by Robert
12:51
created

QueryBuilder::quoteTableNames()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 6.9811
c 0
b 0
f 0
ccs 14
cts 14
cp 1
cc 7
eloc 15
nc 7
nop 2
crap 7
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db;
9
10
use yii\base\InvalidParamException;
11
use yii\base\NotSupportedException;
12
use yii\helpers\ArrayHelper;
13
14
/**
15
 * QueryBuilder builds a SELECT SQL statement based on the specification given as a [[Query]] object.
16
 *
17
 * SQL statements are created from [[Query]] objects using the [[build()]]-method.
18
 *
19
 * QueryBuilder is also used by [[Command]] to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE.
20
 *
21
 * For more details and usage information on QueryBuilder, see the [guide article on query builders](guide:db-query-builder).
22
 *
23
 * @author Qiang Xue <[email protected]>
24
 * @since 2.0
25
 */
26
class QueryBuilder extends \yii\base\BaseObject
27
{
28
    /**
29
     * The prefix for automatically generated query binding parameters.
30
     */
31
    const PARAM_PREFIX = ':qp';
32
33
    /**
34
     * @var Connection the database connection.
35
     */
36
    public $db;
37
    /**
38
     * @var string the separator between different fragments of a SQL statement.
39
     * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
40
     */
41
    public $separator = ' ';
42
    /**
43
     * @var array the abstract column types mapped to physical column types.
44
     * This is mainly used to support creating/modifying tables using DB-independent data type specifications.
45
     * Child classes should override this property to declare supported type mappings.
46
     */
47
    public $typeMap = [];
48
49
    /**
50
     * @var array map of query condition to builder methods.
51
     * These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
52
     */
53
    protected $conditionBuilders = [
54
        'NOT' => 'buildNotCondition',
55
        'AND' => 'buildAndCondition',
56
        'OR' => 'buildAndCondition',
57
        'BETWEEN' => 'buildBetweenCondition',
58
        'NOT BETWEEN' => 'buildBetweenCondition',
59
        'IN' => 'buildInCondition',
60
        'NOT IN' => 'buildInCondition',
61
        'LIKE' => 'buildLikeCondition',
62
        'NOT LIKE' => 'buildLikeCondition',
63
        'OR LIKE' => 'buildLikeCondition',
64
        'OR NOT LIKE' => 'buildLikeCondition',
65
        'EXISTS' => 'buildExistsCondition',
66
        'NOT EXISTS' => 'buildExistsCondition',
67
    ];
68
    /**
69
     * @var array map of chars to their replacements in LIKE conditions.
70
     * By default it's configured to escape `%`, `_` and `\` with `\`.
71
     * @since 2.0.12.
72
     */
73
    protected $likeEscapingReplacements = [
74
        '%' => '\%',
75
        '_' => '\_',
76
        '\\' => '\\\\',
77
    ];
78
    /**
79
     * @var string|null character used to escape special characters in LIKE conditions.
80
     * By default it's assumed to be `\`.
81
     * @since 2.0.12
82
     */
83
    protected $likeEscapeCharacter;
84
85
86
    /**
87
     * Constructor.
88
     * @param Connection $connection the database connection.
89
     * @param array $config name-value pairs that will be used to initialize the object properties
90
     */
91 1163
    public function __construct($connection, $config = [])
92
    {
93 1163
        $this->db = $connection;
94 1163
        parent::__construct($config);
95 1163
    }
96
97
    /**
98
     * Generates a SELECT SQL statement from a [[Query]] object.
99
     * @param Query $query the [[Query]] object from which the SQL statement will be generated.
100
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
101
     * be included in the result with the additional parameters generated during the query building process.
102
     * @return array the generated SQL statement (the first array element) and the corresponding
103
     * parameters to be bound to the SQL statement (the second array element). The parameters returned
104
     * include those provided in `$params`.
105
     */
106 653
    public function build($query, $params = [])
107
    {
108 653
        $query = $query->prepare($this);
109
110 653
        $params = empty($params) ? $query->params : array_merge($params, $query->params);
111
112
        $clauses = [
113 653
            $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
114 653
            $this->buildFrom($query->from, $params),
115 653
            $this->buildJoin($query->join, $params),
116 653
            $this->buildWhere($query->where, $params),
117 653
            $this->buildGroupBy($query->groupBy),
118 653
            $this->buildHaving($query->having, $params),
119
        ];
120
121 653
        $sql = implode($this->separator, array_filter($clauses));
122 653
        $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset);
0 ignored issues
show
Bug introduced by
It seems like $query->limit can also be of type object<yii\db\Expression>; however, yii\db\QueryBuilder::buildOrderByAndLimit() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $query->offset can also be of type object<yii\db\Expression>; however, yii\db\QueryBuilder::buildOrderByAndLimit() does only seem to accept integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
123
124 653
        if (!empty($query->orderBy)) {
125 90
            foreach ($query->orderBy as $expression) {
126 90
                if ($expression instanceof Expression) {
127 90
                    $params = array_merge($params, $expression->params);
128
                }
129
            }
130
        }
131 653
        if (!empty($query->groupBy)) {
132 16
            foreach ($query->groupBy as $expression) {
133 16
                if ($expression instanceof Expression) {
134 16
                    $params = array_merge($params, $expression->params);
135
                }
136
            }
137
        }
138
139 653
        $union = $this->buildUnion($query->union, $params);
140 653
        if ($union !== '') {
141 8
            $sql = "($sql){$this->separator}$union";
142
        }
143
144 653
        return [$sql, $params];
145
    }
146
147
    /**
148
     * Creates an INSERT SQL statement.
149
     *
150
     * For example,
151
     *
152
     * ```php
153
     * $sql = $queryBuilder->insert('user', [
154
     *     'name' => 'Sam',
155
     *     'age' => 30,
156
     * ], $params);
157
     * ```
158
     *
159
     * The method will properly escape the table and column names.
160
     *
161
     * @param string $table the table that new rows will be inserted into.
162
     * @param array|\yii\db\Query $columns the column data (name => value) to be inserted into the table or instance
163
     * of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement.
164
     * Passing of [[yii\db\Query|Query]] is available since version 2.0.11.
165
     * @param array $params the binding parameters that will be generated by this method.
166
     * They should be bound to the DB command later.
167
     * @return string the INSERT SQL
168
     */
169 263
    public function insert($table, $columns, &$params)
170
    {
171 263
        $schema = $this->db->getSchema();
172 263
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
173 263
            $columnSchemas = $tableSchema->columns;
174
        } else {
175
            $columnSchemas = [];
176
        }
177 263
        $names = [];
178 263
        $placeholders = [];
179 263
        $values = ' DEFAULT VALUES';
180 263
        if ($columns instanceof \yii\db\Query) {
181 10
            list($names, $values, $params) = $this->prepareInsertSelectSubQuery($columns, $schema);
182
        } else {
183 257
            foreach ($columns as $name => $value) {
184 255
                $names[] = $schema->quoteColumnName($name);
185 255
                if ($value instanceof Expression) {
186 5
                    $placeholders[] = $value->expression;
187 5
                    foreach ($value->params as $n => $v) {
188 5
                        $params[$n] = $v;
189
                    }
190 252
                } elseif ($value instanceof \yii\db\Query) {
191 2
                    list($sql, $params) = $this->build($value, $params);
192 2
                    $placeholders[] = "($sql)";
193
                } else {
194 252
                    $phName = self::PARAM_PREFIX . count($params);
195 252
                    $placeholders[] = $phName;
196 255
                    $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
197
                }
198
            }
199
        }
200
201 257
        return 'INSERT INTO ' . $schema->quoteTableName($table)
202 257
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
203 257
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
204
    }
205
206
    /**
207
     * Prepare select-subquery and field names for INSERT INTO ... SELECT SQL statement.
208
     *
209
     * @param \yii\db\Query $columns Object, which represents select query.
210
     * @param \yii\db\Schema $schema Schema object to quote column name.
211
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
212
     * be included in the result with the additional parameters generated during the query building process.
213
     * @return array
214
     * @throws InvalidParamException if query's select does not contain named parameters only.
215
     * @since 2.0.11
216
     */
217 15
    protected function prepareInsertSelectSubQuery($columns, $schema, $params = [])
218
    {
219 15
        if (!is_array($columns->select) || empty($columns->select) || in_array('*', $columns->select)) {
220 9
            throw new InvalidParamException('Expected select query object with enumerated (named) parameters');
221
        }
222
223 6
        list($values, $params) = $this->build($columns, $params);
224 6
        $names = [];
225 6
        $values = ' ' . $values;
226 6
        foreach ($columns->select as $title => $field) {
227 6
            if (is_string($title)) {
228 3
                $names[] = $schema->quoteColumnName($title);
229 3
            } elseif (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $field, $matches)) {
230 3
                $names[] = $schema->quoteColumnName($matches[2]);
231
            } else {
232 6
                $names[] = $schema->quoteColumnName($field);
233
            }
234
        }
235
236 6
        return [$names, $values, $params];
237
    }
238
239
    /**
240
     * Generates a batch INSERT SQL statement.
241
     *
242
     * For example,
243
     *
244
     * ```php
245
     * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
246
     *     ['Tom', 30],
247
     *     ['Jane', 20],
248
     *     ['Linda', 25],
249
     * ]);
250
     * ```
251
     *
252
     * Note that the values in each row must match the corresponding column names.
253
     *
254
     * The method will properly escape the column names, and quote the values to be inserted.
255
     *
256
     * @param string $table the table that new rows will be inserted into.
257
     * @param array $columns the column names
258
     * @param array|\Generator $rows the rows to be batch inserted into the table
259
     * @return string the batch INSERT SQL statement
260
     */
261 22
    public function batchInsert($table, $columns, $rows)
262
    {
263 22
        if (empty($rows)) {
264 2
            return '';
265
        }
266
267 21
        $schema = $this->db->getSchema();
268 21
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
269 15
            $columnSchemas = $tableSchema->columns;
270
        } else {
271 6
            $columnSchemas = [];
272
        }
273
274 21
        $values = [];
275 21
        foreach ($rows as $row) {
276 19
            $vs = [];
277 19
            foreach ($row as $i => $value) {
278 19
                if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
279 10
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
280
                }
281 19
                if (is_string($value)) {
282 13
                    $value = $schema->quoteValue($value);
283 11
                } elseif (is_float($value)) {
284
                    // ensure type cast always has . as decimal separator in all locales
285 2
                    $value = str_replace(',', '.', (string) $value);
286 11
                } elseif ($value === false) {
287 4
                    $value = 0;
288 11
                } elseif ($value === null) {
289 6
                    $value = 'NULL';
290
                }
291 19
                $vs[] = $value;
292
            }
293 19
            $values[] = '(' . implode(', ', $vs) . ')';
294
        }
295 21
        if (empty($values)) {
296 2
            return '';
297
        }
298
299 19
        foreach ($columns as $i => $name) {
300 17
            $columns[$i] = $schema->quoteColumnName($name);
301
        }
302
303 19
        return 'INSERT INTO ' . $schema->quoteTableName($table)
304 19
        . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
305
    }
306
307
    /**
308
     * Creates an UPDATE SQL statement.
309
     *
310
     * For example,
311
     *
312
     * ```php
313
     * $params = [];
314
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
315
     * ```
316
     *
317
     * The method will properly escape the table and column names.
318
     *
319
     * @param string $table the table to be updated.
320
     * @param array $columns the column data (name => value) to be updated.
321
     * @param array|string $condition the condition that will be put in the WHERE part. Please
322
     * refer to [[Query::where()]] on how to specify condition.
323
     * @param array $params the binding parameters that will be modified by this method
324
     * so that they can be bound to the DB command later.
325
     * @return string the UPDATE SQL
326
     */
327 92
    public function update($table, $columns, $condition, &$params)
328
    {
329 92
        if (($tableSchema = $this->db->getTableSchema($table)) !== null) {
330 92
            $columnSchemas = $tableSchema->columns;
331
        } else {
332
            $columnSchemas = [];
333
        }
334
335 92
        $lines = [];
336 92
        foreach ($columns as $name => $value) {
337 92
            if ($value instanceof Expression) {
338 6
                $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression;
339 6
                foreach ($value->params as $n => $v) {
340 6
                    $params[$n] = $v;
341
                }
342
            } else {
343 86
                $phName = self::PARAM_PREFIX . count($params);
344 86
                $lines[] = $this->db->quoteColumnName($name) . '=' . $phName;
345 92
                $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
346
            }
347
        }
348
349 92
        $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
350 92
        $where = $this->buildWhere($condition, $params);
351
352 92
        return $where === '' ? $sql : $sql . ' ' . $where;
353
    }
354
355
    /**
356
     * Creates a DELETE SQL statement.
357
     *
358
     * For example,
359
     *
360
     * ```php
361
     * $sql = $queryBuilder->delete('user', 'status = 0');
362
     * ```
363
     *
364
     * The method will properly escape the table and column names.
365
     *
366
     * @param string $table the table where the data will be deleted from.
367
     * @param array|string $condition the condition that will be put in the WHERE part. Please
368
     * refer to [[Query::where()]] on how to specify condition.
369
     * @param array $params the binding parameters that will be modified by this method
370
     * so that they can be bound to the DB command later.
371
     * @return string the DELETE SQL
372
     */
373 303
    public function delete($table, $condition, &$params)
374
    {
375 303
        $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
376 303
        $where = $this->buildWhere($condition, $params);
377
378 303
        return $where === '' ? $sql : $sql . ' ' . $where;
379
    }
380
381
    /**
382
     * Builds a SQL statement for creating a new DB table.
383
     *
384
     * The columns in the new  table should be specified as name-definition pairs (e.g. 'name' => 'string'),
385
     * where name stands for a column name which will be properly quoted by the method, and definition
386
     * stands for the column type which can contain an abstract DB type.
387
     * The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one.
388
     *
389
     * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly
390
     * inserted into the generated SQL.
391
     *
392
     * For example,
393
     *
394
     * ```php
395
     * $sql = $queryBuilder->createTable('user', [
396
     *  'id' => 'pk',
397
     *  'name' => 'string',
398
     *  'age' => 'integer',
399
     * ]);
400
     * ```
401
     *
402
     * @param string $table the name of the table to be created. The name will be properly quoted by the method.
403
     * @param array $columns the columns (name => definition) in the new table.
404
     * @param string $options additional SQL fragment that will be appended to the generated SQL.
405
     * @return string the SQL statement for creating a new DB table.
406
     */
407 110
    public function createTable($table, $columns, $options = null)
408
    {
409 110
        $cols = [];
410 110
        foreach ($columns as $name => $type) {
411 110
            if (is_string($name)) {
412 110
                $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
413
            } else {
414 110
                $cols[] = "\t" . $type;
415
            }
416
        }
417 110
        $sql = 'CREATE TABLE ' . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)";
418
419 110
        return $options === null ? $sql : $sql . ' ' . $options;
420
    }
421
422
    /**
423
     * Builds a SQL statement for renaming a DB table.
424
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
425
     * @param string $newName the new table name. The name will be properly quoted by the method.
426
     * @return string the SQL statement for renaming a DB table.
427
     */
428 1
    public function renameTable($oldName, $newName)
429
    {
430 1
        return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName);
431
    }
432
433
    /**
434
     * Builds a SQL statement for dropping a DB table.
435
     * @param string $table the table to be dropped. The name will be properly quoted by the method.
436
     * @return string the SQL statement for dropping a DB table.
437
     */
438 19
    public function dropTable($table)
439
    {
440 19
        return 'DROP TABLE ' . $this->db->quoteTableName($table);
441
    }
442
443
    /**
444
     * Builds a SQL statement for adding a primary key constraint to an existing table.
445
     * @param string $name the name of the primary key constraint.
446
     * @param string $table the table that the primary key constraint will be added to.
447
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
448
     * @return string the SQL statement for adding a primary key constraint to an existing table.
449
     */
450 6
    public function addPrimaryKey($name, $table, $columns)
451
    {
452 6
        if (is_string($columns)) {
453 4
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
454
        }
455
456 6
        foreach ($columns as $i => $col) {
457 6
            $columns[$i] = $this->db->quoteColumnName($col);
458
        }
459
460 6
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
461 6
            . $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
462 6
            . implode(', ', $columns) . ')';
463
    }
464
465
    /**
466
     * Builds a SQL statement for removing a primary key constraint to an existing table.
467
     * @param string $name the name of the primary key constraint to be removed.
468
     * @param string $table the table that the primary key constraint will be removed from.
469
     * @return string the SQL statement for removing a primary key constraint from an existing table.
470
     */
471 2
    public function dropPrimaryKey($name, $table)
472
    {
473 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
474 2
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
475
    }
476
477
    /**
478
     * Builds a SQL statement for truncating a DB table.
479
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
480
     * @return string the SQL statement for truncating a DB table.
481
     */
482 9
    public function truncateTable($table)
483
    {
484 9
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table);
485
    }
486
487
    /**
488
     * Builds a SQL statement for adding a new DB column.
489
     * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method.
490
     * @param string $column the name of the new column. The name will be properly quoted by the method.
491
     * @param string $type the column type. The [[getColumnType()]] method will be invoked to convert abstract column type (if any)
492
     * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
493
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
494
     * @return string the SQL statement for adding a new column.
495
     */
496 4
    public function addColumn($table, $column, $type)
497
    {
498 4
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
499 4
            . ' ADD ' . $this->db->quoteColumnName($column) . ' '
500 4
            . $this->getColumnType($type);
501
    }
502
503
    /**
504
     * Builds a SQL statement for dropping a DB column.
505
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
506
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
507
     * @return string the SQL statement for dropping a DB column.
508
     */
509
    public function dropColumn($table, $column)
510
    {
511
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
512
            . ' DROP COLUMN ' . $this->db->quoteColumnName($column);
513
    }
514
515
    /**
516
     * Builds a SQL statement for renaming a column.
517
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
518
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
519
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
520
     * @return string the SQL statement for renaming a DB column.
521
     */
522
    public function renameColumn($table, $oldName, $newName)
523
    {
524
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
525
            . ' RENAME COLUMN ' . $this->db->quoteColumnName($oldName)
526
            . ' TO ' . $this->db->quoteColumnName($newName);
527
    }
528
529
    /**
530
     * Builds a SQL statement for changing the definition of a column.
531
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
532
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
533
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
534
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
535
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
536
     * will become 'varchar(255) not null'.
537
     * @return string the SQL statement for changing the definition of a column.
538
     */
539 1
    public function alterColumn($table, $column, $type)
540
    {
541 1
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
542 1
            . $this->db->quoteColumnName($column) . ' '
543 1
            . $this->db->quoteColumnName($column) . ' '
544 1
            . $this->getColumnType($type);
545
    }
546
547
    /**
548
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
549
     * The method will properly quote the table and column names.
550
     * @param string $name the name of the foreign key constraint.
551
     * @param string $table the table that the foreign key constraint will be added to.
552
     * @param string|array $columns the name of the column to that the constraint will be added on.
553
     * If there are multiple columns, separate them with commas or use an array to represent them.
554
     * @param string $refTable the table that the foreign key references to.
555
     * @param string|array $refColumns the name of the column that the foreign key references to.
556
     * If there are multiple columns, separate them with commas or use an array to represent them.
557
     * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
558
     * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
559
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
560
     */
561 8
    public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
562
    {
563 8
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
564 8
            . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
565 8
            . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
566 8
            . ' REFERENCES ' . $this->db->quoteTableName($refTable)
567 8
            . ' (' . $this->buildColumns($refColumns) . ')';
568 8
        if ($delete !== null) {
569 4
            $sql .= ' ON DELETE ' . $delete;
570
        }
571 8
        if ($update !== null) {
572 4
            $sql .= ' ON UPDATE ' . $update;
573
        }
574
575 8
        return $sql;
576
    }
577
578
    /**
579
     * Builds a SQL statement for dropping a foreign key constraint.
580
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
581
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
582
     * @return string the SQL statement for dropping a foreign key constraint.
583
     */
584 3
    public function dropForeignKey($name, $table)
585
    {
586 3
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
587 3
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
588
    }
589
590
    /**
591
     * Builds a SQL statement for creating a new index.
592
     * @param string $name the name of the index. The name will be properly quoted by the method.
593
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method.
594
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
595
     * separate them with commas or use an array to represent them. Each column name will be properly quoted
596
     * by the method, unless a parenthesis is found in the name.
597
     * @param bool $unique whether to add UNIQUE constraint on the created index.
598
     * @return string the SQL statement for creating a new index.
599
     */
600 6
    public function createIndex($name, $table, $columns, $unique = false)
601
    {
602 6
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
603 6
            . $this->db->quoteTableName($name) . ' ON '
604 6
            . $this->db->quoteTableName($table)
605 6
            . ' (' . $this->buildColumns($columns) . ')';
606
    }
607
608
    /**
609
     * Builds a SQL statement for dropping an index.
610
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
611
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
612
     * @return string the SQL statement for dropping an index.
613
     */
614 4
    public function dropIndex($name, $table)
615
    {
616 4
        return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
617
    }
618
619
    /**
620
     * Creates a SQL command for adding an unique constraint to an existing table.
621
     * @param string $name the name of the unique constraint.
622
     * The name will be properly quoted by the method.
623
     * @param string $table the table that the unique constraint will be added to.
624
     * The name will be properly quoted by the method.
625
     * @param string|array $columns the name of the column to that the constraint will be added on.
626
     * If there are multiple columns, separate them with commas.
627
     * The name will be properly quoted by the method.
628
     * @return string the SQL statement for adding an unique constraint to an existing table.
629
     * @since 2.0.13
630
     */
631 6
    public function addUnique($name, $table, $columns)
632
    {
633 6
        if (is_string($columns)) {
634 4
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
635
        }
636 6
        foreach ($columns as $i => $col) {
637 6
            $columns[$i] = $this->db->quoteColumnName($col);
638
        }
639
640 6
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
641 6
            . $this->db->quoteColumnName($name) . ' UNIQUE ('
642 6
            . implode(', ', $columns) . ')';
643
    }
644
645
    /**
646
     * Creates a SQL command for dropping an unique constraint.
647
     * @param string $name the name of the unique constraint to be dropped.
648
     * The name will be properly quoted by the method.
649
     * @param string $table the table whose unique constraint is to be dropped.
650
     * The name will be properly quoted by the method.
651
     * @return string the SQL statement for dropping an unique constraint.
652
     * @since 2.0.13
653
     */
654 2
    public function dropUnique($name, $table)
655
    {
656 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
657 2
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
658
    }
659
660
    /**
661
     * Creates a SQL command for adding a check constraint to an existing table.
662
     * @param string $name the name of the check constraint.
663
     * The name will be properly quoted by the method.
664
     * @param string $table the table that the check constraint will be added to.
665
     * The name will be properly quoted by the method.
666
     * @param string $expression the SQL of the `CHECK` constraint.
667
     * @return string the SQL statement for adding a check constraint to an existing table.
668
     * @since 2.0.13
669
     */
670 2
    public function addCheck($name, $table, $expression)
671
    {
672 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
673 2
            . $this->db->quoteColumnName($name) . ' CHECK (' . $this->db->quoteSql($expression) . ')';
674
    }
675
676
    /**
677
     * Creates a SQL command for dropping a check constraint.
678
     * @param string $name the name of the check constraint to be dropped.
679
     * The name will be properly quoted by the method.
680
     * @param string $table the table whose check constraint is to be dropped.
681
     * The name will be properly quoted by the method.
682
     * @return string the SQL statement for dropping a check constraint.
683
     * @since 2.0.13
684
     */
685 2
    public function dropCheck($name, $table)
686
    {
687 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
688 2
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
689
    }
690
691
    /**
692
     * Creates a SQL command for adding a default value constraint to an existing table.
693
     * @param string $name the name of the default value constraint.
694
     * The name will be properly quoted by the method.
695
     * @param string $table the table that the default value constraint will be added to.
696
     * The name will be properly quoted by the method.
697
     * @param string $column the name of the column to that the constraint will be added on.
698
     * The name will be properly quoted by the method.
699
     * @param mixed $value default value.
700
     * @return string the SQL statement for adding a default value constraint to an existing table.
701
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
702
     * @since 2.0.13
703
     */
704
    public function addDefaultValue($name, $table, $column, $value)
705
    {
706
        throw new NotSupportedException($this->db->getDriverName() . ' does not support adding default value constraints.');
707
    }
708
709
    /**
710
     * Creates a SQL command for dropping a default value constraint.
711
     * @param string $name the name of the default value constraint to be dropped.
712
     * The name will be properly quoted by the method.
713
     * @param string $table the table whose default value constraint is to be dropped.
714
     * The name will be properly quoted by the method.
715
     * @return string the SQL statement for dropping a default value constraint.
716
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
717
     * @since 2.0.13
718
     */
719
    public function dropDefaultValue($name, $table)
720
    {
721
        throw new NotSupportedException($this->db->getDriverName() . ' does not support dropping default value constraints.');
722
    }
723
724
    /**
725
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
726
     * The sequence will be reset such that the primary key of the next new row inserted
727
     * will have the specified value or 1.
728
     * @param string $table the name of the table whose primary key sequence will be reset
729
     * @param array|string $value the value for the primary key of the next new row inserted. If this is not set,
730
     * the next new row's primary key will have a value 1.
731
     * @return string the SQL statement for resetting sequence
732
     * @throws NotSupportedException if this is not supported by the underlying DBMS
733
     */
734
    public function resetSequence($table, $value = null)
735
    {
736
        throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.');
737
    }
738
739
    /**
740
     * Builds a SQL statement for enabling or disabling integrity check.
741
     * @param bool $check whether to turn on or off the integrity check.
742
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
743
     * @param string $table the table name. Defaults to empty string, meaning that no table will be changed.
744
     * @return string the SQL statement for checking integrity
745
     * @throws NotSupportedException if this is not supported by the underlying DBMS
746
     */
747
    public function checkIntegrity($check = true, $schema = '', $table = '')
748
    {
749
        throw new NotSupportedException($this->db->getDriverName() . ' does not support enabling/disabling integrity check.');
750
    }
751
752
    /**
753
     * Builds a SQL command for adding comment to column.
754
     *
755
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
756
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the method.
757
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
758
     * @return string the SQL statement for adding comment on column
759
     * @since 2.0.8
760
     */
761 2
    public function addCommentOnColumn($table, $column, $comment)
762
    {
763 2
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . ' IS ' . $this->db->quoteValue($comment);
764
    }
765
766
    /**
767
     * Builds a SQL command for adding comment to table.
768
     *
769
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
770
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
771
     * @return string the SQL statement for adding comment on table
772
     * @since 2.0.8
773
     */
774 1
    public function addCommentOnTable($table, $comment)
775
    {
776 1
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS ' . $this->db->quoteValue($comment);
777
    }
778
779
    /**
780
     * Builds a SQL command for adding comment to column.
781
     *
782
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
783
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the method.
784
     * @return string the SQL statement for adding comment on column
785
     * @since 2.0.8
786
     */
787 2
    public function dropCommentFromColumn($table, $column)
788
    {
789 2
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . ' IS NULL';
790
    }
791
792
    /**
793
     * Builds a SQL command for adding comment to table.
794
     *
795
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
796
     * @return string the SQL statement for adding comment on column
797
     * @since 2.0.8
798
     */
799 1
    public function dropCommentFromTable($table)
800
    {
801 1
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS NULL';
802
    }
803
804
    /**
805
     * Converts an abstract column type into a physical column type.
806
     *
807
     * The conversion is done using the type map specified in [[typeMap]].
808
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
809
     * physical types):
810
     *
811
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY"
812
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY"
813
     * - `upk`: an unsigned auto-incremental primary key type, will be converted into "int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY"
814
     * - `char`: char type, will be converted into "char(1)"
815
     * - `string`: string type, will be converted into "varchar(255)"
816
     * - `text`: a long string type, will be converted into "text"
817
     * - `smallint`: a small integer type, will be converted into "smallint(6)"
818
     * - `integer`: integer type, will be converted into "int(11)"
819
     * - `bigint`: a big integer type, will be converted into "bigint(20)"
820
     * - `boolean`: boolean type, will be converted into "tinyint(1)"
821
     * - `float``: float number type, will be converted into "float"
822
     * - `decimal`: decimal number type, will be converted into "decimal"
823
     * - `datetime`: datetime type, will be converted into "datetime"
824
     * - `timestamp`: timestamp type, will be converted into "timestamp"
825
     * - `time`: time type, will be converted into "time"
826
     * - `date`: date type, will be converted into "date"
827
     * - `money`: money type, will be converted into "decimal(19,4)"
828
     * - `binary`: binary data type, will be converted into "blob"
829
     *
830
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only
831
     * the first part will be converted, and the rest of the parts will be appended to the converted result.
832
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
833
     *
834
     * For some of the abstract types you can also specify a length or precision constraint
835
     * by appending it in round brackets directly to the type.
836
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database.
837
     * If the underlying DBMS does not support these kind of constraints for a type it will
838
     * be ignored.
839
     *
840
     * If a type cannot be found in [[typeMap]], it will be returned without any change.
841
     * @param string|ColumnSchemaBuilder $type abstract column type
842
     * @return string physical column type.
843
     */
844 114
    public function getColumnType($type)
845
    {
846 114
        if ($type instanceof ColumnSchemaBuilder) {
847 13
            $type = $type->__toString();
848
        }
849
850 114
        if (isset($this->typeMap[$type])) {
851 99
            return $this->typeMap[$type];
852 55
        } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
853 31
            if (isset($this->typeMap[$matches[1]])) {
854 31
                return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3];
855
            }
856 32
        } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
857 29
            if (isset($this->typeMap[$matches[1]])) {
858 29
                return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
859
            }
860
        }
861
862 24
        return $type;
863
    }
864
865
    /**
866
     * @param array $columns
867
     * @param array $params the binding parameters to be populated
868
     * @param bool $distinct
869
     * @param string $selectOption
870
     * @return string the SELECT clause built from [[Query::$select]].
871
     */
872 949
    public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
873
    {
874 949
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
875 949
        if ($selectOption !== null) {
876
            $select .= ' ' . $selectOption;
877
        }
878
879 949
        if (empty($columns)) {
880 764
            return $select . ' *';
881
        }
882
883 435
        foreach ($columns as $i => $column) {
884 435
            if ($column instanceof Expression) {
885 6
                if (is_int($i)) {
886 6
                    $columns[$i] = $column->expression;
887
                } else {
888 3
                    $columns[$i] = $column->expression . ' AS ' . $this->db->quoteColumnName($i);
889
                }
890 6
                $params = array_merge($params, $column->params);
891 432
            } elseif ($column instanceof Query) {
892 3
                list($sql, $params) = $this->build($column, $params);
893 3
                $columns[$i] = "($sql) AS " . $this->db->quoteColumnName($i);
894 432
            } elseif (is_string($i)) {
895 21
                if (strpos($column, '(') === false) {
896 21
                    $column = $this->db->quoteColumnName($column);
897
                }
898 21
                $columns[$i] = "$column AS " . $this->db->quoteColumnName($i);
899 427
            } elseif (strpos($column, '(') === false) {
900 346
                if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
901 6
                    $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]);
902
                } else {
903 435
                    $columns[$i] = $this->db->quoteColumnName($column);
904
                }
905
            }
906
        }
907
908 435
        return $select . ' ' . implode(', ', $columns);
909
    }
910
911
    /**
912
     * @param array $tables
913
     * @param array $params the binding parameters to be populated
914
     * @return string the FROM clause built from [[Query::$from]].
915
     */
916 949
    public function buildFrom($tables, &$params)
917
    {
918 949
        if (empty($tables)) {
919 274
            return '';
920
        }
921
922 694
        $tables = $this->quoteTableNames($tables, $params);
923
924 694
        return 'FROM ' . implode(', ', $tables);
925
    }
926
927
    /**
928
     * @param array $joins
929
     * @param array $params the binding parameters to be populated
930
     * @return string the JOIN clause built from [[Query::$join]].
931
     * @throws Exception if the $joins parameter is not in proper format
932
     */
933 949
    public function buildJoin($joins, &$params)
934
    {
935 949
        if (empty($joins)) {
936 937
            return '';
937
        }
938
939 51
        foreach ($joins as $i => $join) {
940 51
            if (!is_array($join) || !isset($join[0], $join[1])) {
941
                throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.');
942
            }
943
            // 0:join type, 1:join table, 2:on-condition (optional)
944 51
            list($joinType, $table) = $join;
945 51
            $tables = $this->quoteTableNames((array) $table, $params);
946 51
            $table = reset($tables);
947 51
            $joins[$i] = "$joinType $table";
948 51
            if (isset($join[2])) {
949 51
                $condition = $this->buildCondition($join[2], $params);
950 51
                if ($condition !== '') {
951 51
                    $joins[$i] .= ' ON ' . $condition;
952
                }
953
            }
954
        }
955
956 51
        return implode($this->separator, $joins);
957
    }
958
959
    /**
960
     * Quotes table names passed.
961
     *
962
     * @param array $tables
963
     * @param array $params
964
     * @return array
965
     */
966 694
    private function quoteTableNames($tables, &$params)
967
    {
968 694
        foreach ($tables as $i => $table) {
969 694
            if ($table instanceof Query) {
970 10
                list($sql, $params) = $this->build($table, $params);
971 10
                $tables[$i] = "($sql) " . $this->db->quoteTableName($i);
972 694
            } elseif (is_string($i)) {
973 74
                if (strpos($table, '(') === false) {
974 65
                    $table = $this->db->quoteTableName($table);
975
                }
976 74
                $tables[$i] = "$table " . $this->db->quoteTableName($i);
977 676
            } elseif (strpos($table, '(') === false) {
978 672
                if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias
979 21
                    $tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]);
980
                } else {
981 694
                    $tables[$i] = $this->db->quoteTableName($table);
982
                }
983
            }
984
        }
985
986 694
        return $tables;
987
    }
988
989
    /**
990
     * @param string|array $condition
991
     * @param array $params the binding parameters to be populated
992
     * @return string the WHERE clause built from [[Query::$where]].
993
     */
994 1012
    public function buildWhere($condition, &$params)
995
    {
996 1012
        $where = $this->buildCondition($condition, $params);
997
998 1012
        return $where === '' ? '' : 'WHERE ' . $where;
999
    }
1000
1001
    /**
1002
     * @param array $columns
1003
     * @return string the GROUP BY clause
1004
     */
1005 949
    public function buildGroupBy($columns)
1006
    {
1007 949
        if (empty($columns)) {
1008 943
            return '';
1009
        }
1010 19
        foreach ($columns as $i => $column) {
1011 19
            if ($column instanceof Expression) {
1012 3
                $columns[$i] = $column->expression;
1013 19
            } elseif (strpos($column, '(') === false) {
1014 19
                $columns[$i] = $this->db->quoteColumnName($column);
1015
            }
1016
        }
1017
1018 19
        return 'GROUP BY ' . implode(', ', $columns);
1019
    }
1020
1021
    /**
1022
     * @param string|array $condition
1023
     * @param array $params the binding parameters to be populated
1024
     * @return string the HAVING clause built from [[Query::$having]].
1025
     */
1026 949
    public function buildHaving($condition, &$params)
1027
    {
1028 949
        $having = $this->buildCondition($condition, $params);
1029
1030 949
        return $having === '' ? '' : 'HAVING ' . $having;
1031
    }
1032
1033
    /**
1034
     * Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
1035
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
1036
     * @param array $orderBy the order by columns. See [[Query::orderBy]] for more details on how to specify this parameter.
1037
     * @param int $limit the limit number. See [[Query::limit]] for more details.
1038
     * @param int $offset the offset number. See [[Query::offset]] for more details.
1039
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
1040
     */
1041 949
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
1042
    {
1043 949
        $orderBy = $this->buildOrderBy($orderBy);
1044 949
        if ($orderBy !== '') {
1045 152
            $sql .= $this->separator . $orderBy;
1046
        }
1047 949
        $limit = $this->buildLimit($limit, $offset);
1048 949
        if ($limit !== '') {
1049 65
            $sql .= $this->separator . $limit;
1050
        }
1051
1052 949
        return $sql;
1053
    }
1054
1055
    /**
1056
     * @param array $columns
1057
     * @return string the ORDER BY clause built from [[Query::$orderBy]].
1058
     */
1059 949
    public function buildOrderBy($columns)
1060
    {
1061 949
        if (empty($columns)) {
1062 916
            return '';
1063
        }
1064 152
        $orders = [];
1065 152
        foreach ($columns as $name => $direction) {
1066 152
            if ($direction instanceof Expression) {
1067 3
                $orders[] = $direction->expression;
1068
            } else {
1069 152
                $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
1070
            }
1071
        }
1072
1073 152
        return 'ORDER BY ' . implode(', ', $orders);
1074
    }
1075
1076
    /**
1077
     * @param int $limit
1078
     * @param int $offset
1079
     * @return string the LIMIT and OFFSET clauses
1080
     */
1081 317
    public function buildLimit($limit, $offset)
1082
    {
1083 317
        $sql = '';
1084 317
        if ($this->hasLimit($limit)) {
1085 16
            $sql = 'LIMIT ' . $limit;
1086
        }
1087 317
        if ($this->hasOffset($offset)) {
1088 3
            $sql .= ' OFFSET ' . $offset;
1089
        }
1090
1091 317
        return ltrim($sql);
1092
    }
1093
1094
    /**
1095
     * Checks to see if the given limit is effective.
1096
     * @param mixed $limit the given limit
1097
     * @return bool whether the limit is effective
1098
     */
1099 613
    protected function hasLimit($limit)
1100
    {
1101 613
        return ($limit instanceof Expression) || ctype_digit((string) $limit);
1102
    }
1103
1104
    /**
1105
     * Checks to see if the given offset is effective.
1106
     * @param mixed $offset the given offset
1107
     * @return bool whether the offset is effective
1108
     */
1109 613
    protected function hasOffset($offset)
1110
    {
1111 613
        return ($offset instanceof Expression) || ctype_digit((string) $offset) && (string) $offset !== '0';
1112
    }
1113
1114
    /**
1115
     * @param array $unions
1116
     * @param array $params the binding parameters to be populated
1117
     * @return string the UNION clause built from [[Query::$union]].
1118
     */
1119 653
    public function buildUnion($unions, &$params)
1120
    {
1121 653
        if (empty($unions)) {
1122 653
            return '';
1123
        }
1124
1125 8
        $result = '';
1126
1127 8
        foreach ($unions as $i => $union) {
1128 8
            $query = $union['query'];
1129 8
            if ($query instanceof Query) {
1130 8
                list($unions[$i]['query'], $params) = $this->build($query, $params);
1131
            }
1132
1133 8
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
1134
        }
1135
1136 8
        return trim($result);
1137
    }
1138
1139
    /**
1140
     * Processes columns and properly quotes them if necessary.
1141
     * It will join all columns into a string with comma as separators.
1142
     * @param string|array $columns the columns to be processed
1143
     * @return string the processing result
1144
     */
1145 32
    public function buildColumns($columns)
1146
    {
1147 32
        if (!is_array($columns)) {
1148 27
            if (strpos($columns, '(') !== false) {
1149
                return $columns;
1150
            }
1151
1152 27
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
1153
        }
1154 32
        foreach ($columns as $i => $column) {
1155 32
            if ($column instanceof Expression) {
1156
                $columns[$i] = $column->expression;
1157 32
            } elseif (strpos($column, '(') === false) {
1158 32
                $columns[$i] = $this->db->quoteColumnName($column);
1159
            }
1160
        }
1161
1162 32
        return is_array($columns) ? implode(', ', $columns) : $columns;
1163
    }
1164
1165
    /**
1166
     * Parses the condition specification and generates the corresponding SQL expression.
1167
     * @param string|array|Expression $condition the condition specification. Please refer to [[Query::where()]]
1168
     * on how to specify a condition.
1169
     * @param array $params the binding parameters to be populated
1170
     * @return string the generated SQL expression
1171
     */
1172 1012
    public function buildCondition($condition, &$params)
1173
    {
1174 1012
        if ($condition instanceof Expression) {
1175 9
            foreach ($condition->params as $n => $v) {
1176 9
                $params[$n] = $v;
1177
            }
1178
1179 9
            return $condition->expression;
1180 1012
        } elseif (!is_array($condition)) {
1181 1006
            return (string) $condition;
1182
        } elseif (empty($condition)) {
1183
            return '';
1184
        }
1185
1186 786
        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
1187 476
            $operator = strtoupper($condition[0]);
1188 476
            if (isset($this->conditionBuilders[$operator])) {
1189 446
                $method = $this->conditionBuilders[$operator];
1190
            } else {
1191 36
                $method = 'buildSimpleCondition';
1192
            }
1193 476
            array_shift($condition);
1194 476
            return $this->$method($operator, $condition, $params);
1195
        }
1196
1197
        // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
1198 563
        return $this->buildHashCondition($condition, $params);
1199
    }
1200
1201
    /**
1202
     * Creates a condition based on column-value pairs.
1203
     * @param array $condition the condition specification.
1204
     * @param array $params the binding parameters to be populated
1205
     * @return string the generated SQL expression
1206
     */
1207 563
    public function buildHashCondition($condition, &$params)
1208
    {
1209 563
        $parts = [];
1210 563
        foreach ($condition as $column => $value) {
1211 563
            if (ArrayHelper::isTraversable($value) || $value instanceof Query) {
1212
                // IN condition
1213 71
                $parts[] = $this->buildInCondition('IN', [$column, $value], $params);
1214
            } else {
1215 548
                if (strpos($column, '(') === false) {
1216 542
                    $column = $this->db->quoteColumnName($column);
1217
                }
1218 548
                if ($value === null) {
1219 12
                    $parts[] = "$column IS NULL";
1220 548
                } elseif ($value instanceof Expression) {
1221 118
                    $parts[] = "$column=" . $value->expression;
1222 118
                    foreach ($value->params as $n => $v) {
1223 118
                        $params[$n] = $v;
1224
                    }
1225
                } else {
1226 548
                    $phName = self::PARAM_PREFIX . count($params);
1227 548
                    $parts[] = "$column=$phName";
1228 563
                    $params[$phName] = $value;
1229
                }
1230
            }
1231
        }
1232
1233 563
        return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
1234
    }
1235
1236
    /**
1237
     * Connects two or more SQL expressions with the `AND` or `OR` operator.
1238
     * @param string $operator the operator to use for connecting the given operands
1239
     * @param array $operands the SQL expressions to connect.
1240
     * @param array $params the binding parameters to be populated
1241
     * @return string the generated SQL expression
1242
     */
1243 197
    public function buildAndCondition($operator, $operands, &$params)
1244
    {
1245 197
        $parts = [];
1246 197
        foreach ($operands as $operand) {
1247 197
            if (is_array($operand)) {
1248 172
                $operand = $this->buildCondition($operand, $params);
1249
            }
1250 197
            if ($operand instanceof Expression) {
1251 6
                foreach ($operand->params as $n => $v) {
1252 6
                    $params[$n] = $v;
1253
                }
1254 6
                $operand = $operand->expression;
1255
            }
1256 197
            if ($operand !== '') {
1257 197
                $parts[] = $operand;
1258
            }
1259
        }
1260 197
        if (!empty($parts)) {
1261 197
            return '(' . implode(") $operator (", $parts) . ')';
1262
        }
1263
1264
        return '';
1265
    }
1266
1267
    /**
1268
     * Inverts an SQL expressions with `NOT` operator.
1269
     * @param string $operator the operator to use for connecting the given operands
1270
     * @param array $operands the SQL expressions to connect.
1271
     * @param array $params the binding parameters to be populated
1272
     * @return string the generated SQL expression
1273
     * @throws InvalidParamException if wrong number of operands have been given.
1274
     */
1275 6
    public function buildNotCondition($operator, $operands, &$params)
1276
    {
1277 6
        if (count($operands) !== 1) {
1278
            throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
1279
        }
1280
1281 6
        $operand = reset($operands);
1282 6
        if (is_array($operand) || $operand instanceof Expression) {
1283 3
            $operand = $this->buildCondition($operand, $params);
1284
        }
1285 6
        if ($operand === '') {
1286
            return '';
1287
        }
1288
1289 6
        return "$operator ($operand)";
1290
    }
1291
1292
    /**
1293
     * Creates an SQL expressions with the `BETWEEN` operator.
1294
     * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
1295
     * @param array $operands the first operand is the column name. The second and third operands
1296
     * describe the interval that column value should be in.
1297
     * @param array $params the binding parameters to be populated
1298
     * @return string the generated SQL expression
1299
     * @throws InvalidParamException if wrong number of operands have been given.
1300
     */
1301 21
    public function buildBetweenCondition($operator, $operands, &$params)
1302
    {
1303 21
        if (!isset($operands[0], $operands[1], $operands[2])) {
1304
            throw new InvalidParamException("Operator '$operator' requires three operands.");
1305
        }
1306
1307 21
        list($column, $value1, $value2) = $operands;
1308
1309 21
        if (strpos($column, '(') === false) {
1310 21
            $column = $this->db->quoteColumnName($column);
1311
        }
1312 21
        if ($value1 instanceof Expression) {
1313 12
            foreach ($value1->params as $n => $v) {
1314
                $params[$n] = $v;
1315
            }
1316 12
            $phName1 = $value1->expression;
1317
        } else {
1318 9
            $phName1 = self::PARAM_PREFIX . count($params);
1319 9
            $params[$phName1] = $value1;
1320
        }
1321 21
        if ($value2 instanceof Expression) {
1322 6
            foreach ($value2->params as $n => $v) {
1323
                $params[$n] = $v;
1324
            }
1325 6
            $phName2 = $value2->expression;
1326
        } else {
1327 15
            $phName2 = self::PARAM_PREFIX . count($params);
1328 15
            $params[$phName2] = $value2;
1329
        }
1330
1331 21
        return "$column $operator $phName1 AND $phName2";
1332
    }
1333
1334
    /**
1335
     * Creates an SQL expressions with the `IN` operator.
1336
     * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
1337
     * @param array $operands the first operand is the column name. If it is an array
1338
     * a composite IN condition will be generated.
1339
     * The second operand is an array of values that column value should be among.
1340
     * If it is an empty array the generated expression will be a `false` value if
1341
     * operator is `IN` and empty if operator is `NOT IN`.
1342
     * @param array $params the binding parameters to be populated
1343
     * @return string the generated SQL expression
1344
     * @throws Exception if wrong number of operands have been given.
1345
     */
1346 228
    public function buildInCondition($operator, $operands, &$params)
1347
    {
1348 228
        if (!isset($operands[0], $operands[1])) {
1349
            throw new Exception("Operator '$operator' requires two operands.");
1350
        }
1351
1352 228
        list($column, $values) = $operands;
1353
1354 228
        if ($column === []) {
1355
            // no columns to test against
1356
            return $operator === 'IN' ? '0=1' : '';
1357
        }
1358
1359 228
        if ($values instanceof Query) {
1360 14
            return $this->buildSubqueryInCondition($operator, $column, $values, $params);
1361
        }
1362 214
        if (!is_array($values) && !$values instanceof \Traversable) {
1363
            // ensure values is an array
1364 3
            $values = (array) $values;
1365
        }
1366
1367 214
        if ($column instanceof \Traversable || ((is_array($column) || $column instanceof \Countable) && count($column) > 1)) {
1368 15
            return $this->buildCompositeInCondition($operator, $column, $values, $params);
0 ignored issues
show
Bug introduced by
It seems like $column can also be of type object<Countable>; however, yii\db\QueryBuilder::buildCompositeInCondition() does only seem to accept array|object<Traversable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $values can also be of type object<Traversable>; however, yii\db\QueryBuilder::buildCompositeInCondition() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1369 202
        } elseif (is_array($column)) {
1370 133
            $column = reset($column);
1371
        }
1372
1373 202
        $sqlValues = [];
1374 202
        foreach ($values as $i => $value) {
1375 202
            if (is_array($value) || $value instanceof \ArrayAccess) {
1376
                $value = isset($value[$column]) ? $value[$column] : null;
1377
            }
1378 202
            if ($value === null) {
1379
                $sqlValues[$i] = 'NULL';
1380 202
            } elseif ($value instanceof Expression) {
1381
                $sqlValues[$i] = $value->expression;
1382
                foreach ($value->params as $n => $v) {
1383
                    $params[$n] = $v;
1384
                }
1385
            } else {
1386 202
                $phName = self::PARAM_PREFIX . count($params);
1387 202
                $params[$phName] = $value;
1388 202
                $sqlValues[$i] = $phName;
1389
            }
1390
        }
1391
1392 202
        if (empty($sqlValues)) {
1393 18
            return $operator === 'IN' ? '0=1' : '';
1394
        }
1395
1396 202
        if (strpos($column, '(') === false) {
1397 202
            $column = $this->db->quoteColumnName($column);
1398
        }
1399
1400 202
        if (count($sqlValues) > 1) {
1401 139
            return "$column $operator (" . implode(', ', $sqlValues) . ')';
1402
        }
1403
1404 136
        $operator = $operator === 'IN' ? '=' : '<>';
1405 136
        return $column . $operator . reset($sqlValues);
1406
    }
1407
1408
    /**
1409
     * Builds SQL for IN condition.
1410
     *
1411
     * @param string $operator
1412
     * @param array $columns
1413
     * @param Query $values
1414
     * @param array $params
1415
     * @return string SQL
1416
     */
1417 14
    protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
1418
    {
1419 14
        list($sql, $params) = $this->build($values, $params);
1420 14
        if (is_array($columns)) {
1421 4
            foreach ($columns as $i => $col) {
1422 4
                if (strpos($col, '(') === false) {
1423 4
                    $columns[$i] = $this->db->quoteColumnName($col);
1424
                }
1425
            }
1426
1427 4
            return '(' . implode(', ', $columns) . ") $operator ($sql)";
1428
        }
1429
1430 10
        if (strpos($columns, '(') === false) {
1431 10
            $columns = $this->db->quoteColumnName($columns);
1432
        }
1433
1434 10
        return "$columns $operator ($sql)";
1435
    }
1436
1437
    /**
1438
     * Builds SQL for IN condition.
1439
     *
1440
     * @param string $operator
1441
     * @param array|\Traversable $columns
1442
     * @param array $values
1443
     * @param array $params
1444
     * @return string SQL
1445
     */
1446 10
    protected function buildCompositeInCondition($operator, $columns, $values, &$params)
1447
    {
1448 10
        $vss = [];
1449 10
        foreach ($values as $value) {
1450 10
            $vs = [];
1451 10
            foreach ($columns as $column) {
1452 10
                if (isset($value[$column])) {
1453 10
                    $phName = self::PARAM_PREFIX . count($params);
1454 10
                    $params[$phName] = $value[$column];
1455 10
                    $vs[] = $phName;
1456
                } else {
1457 10
                    $vs[] = 'NULL';
1458
                }
1459
            }
1460 10
            $vss[] = '(' . implode(', ', $vs) . ')';
1461
        }
1462
1463 10
        if (empty($vss)) {
1464
            return $operator === 'IN' ? '0=1' : '';
1465
        }
1466
1467 10
        $sqlColumns = [];
1468 10
        foreach ($columns as $i => $column) {
1469 10
            $sqlColumns[] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
1470
        }
1471
1472 10
        return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
1473
    }
1474
1475
    /**
1476
     * Creates an SQL expressions with the `LIKE` operator.
1477
     * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
1478
     * @param array $operands an array of two or three operands
1479
     *
1480
     * - The first operand is the column name.
1481
     * - The second operand is a single value or an array of values that column value
1482
     *   should be compared with. If it is an empty array the generated expression will
1483
     *   be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
1484
     *   is `NOT LIKE` or `OR NOT LIKE`.
1485
     * - An optional third operand can also be provided to specify how to escape special characters
1486
     *   in the value(s). The operand should be an array of mappings from the special characters to their
1487
     *   escaped counterparts. If this operand is not provided, a default escape mapping will be used.
1488
     *   You may use `false` or an empty array to indicate the values are already escaped and no escape
1489
     *   should be applied. Note that when using an escape mapping (or the third operand is not provided),
1490
     *   the values will be automatically enclosed within a pair of percentage characters.
1491
     * @param array $params the binding parameters to be populated
1492
     * @return string the generated SQL expression
1493
     * @throws InvalidParamException if wrong number of operands have been given.
1494
     */
1495 75
    public function buildLikeCondition($operator, $operands, &$params)
1496
    {
1497 75
        if (!isset($operands[0], $operands[1])) {
1498
            throw new InvalidParamException("Operator '$operator' requires two operands.");
1499
        }
1500
1501 75
        $escape = isset($operands[2]) ? $operands[2] : $this->likeEscapingReplacements;
1502 75
        unset($operands[2]);
1503
1504 75
        if (!preg_match('/^(AND |OR |)(((NOT |))I?LIKE)/', $operator, $matches)) {
1505
            throw new InvalidParamException("Invalid operator '$operator'.");
1506
        }
1507 75
        $andor = ' ' . (!empty($matches[1]) ? $matches[1] : 'AND ');
1508 75
        $not = !empty($matches[3]);
1509 75
        $operator = $matches[2];
1510
1511 75
        list($column, $values) = $operands;
1512
1513 75
        if (!is_array($values)) {
1514 31
            $values = [$values];
1515
        }
1516
1517 75
        if (empty($values)) {
1518 16
            return $not ? '' : '0=1';
1519
        }
1520
1521 59
        if (strpos($column, '(') === false) {
1522 59
            $column = $this->db->quoteColumnName($column);
1523
        }
1524
1525 59
        $parts = [];
1526 59
        foreach ($values as $value) {
1527 59
            if ($value instanceof Expression) {
1528 24
                foreach ($value->params as $n => $v) {
1529
                    $params[$n] = $v;
1530
                }
1531 24
                $phName = $value->expression;
1532
            } else {
1533 47
                $phName = self::PARAM_PREFIX . count($params);
1534 47
                $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
1535
            }
1536 59
            $escapeSql = '';
1537 59
            if ($this->likeEscapeCharacter !== null) {
1538 17
                $escapeSql = " ESCAPE '{$this->likeEscapeCharacter}'";
1539
            }
1540 59
            $parts[] = "{$column} {$operator} {$phName}{$escapeSql}";
1541
        }
1542
1543 59
        return implode($andor, $parts);
1544
    }
1545
1546
    /**
1547
     * Creates an SQL expressions with the `EXISTS` operator.
1548
     * @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
1549
     * @param array $operands contains only one element which is a [[Query]] object representing the sub-query.
1550
     * @param array $params the binding parameters to be populated
1551
     * @return string the generated SQL expression
1552
     * @throws InvalidParamException if the operand is not a [[Query]] object.
1553
     */
1554 18
    public function buildExistsCondition($operator, $operands, &$params)
1555
    {
1556 18
        if ($operands[0] instanceof Query) {
1557 18
            list($sql, $params) = $this->build($operands[0], $params);
1558 18
            return "$operator ($sql)";
1559
        }
1560
1561
        throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.');
1562
    }
1563
1564
    /**
1565
     * Creates an SQL expressions like `"column" operator value`.
1566
     * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
1567
     * @param array $operands contains two column names.
1568
     * @param array $params the binding parameters to be populated
1569
     * @return string the generated SQL expression
1570
     * @throws InvalidParamException if wrong number of operands have been given.
1571
     */
1572 36
    public function buildSimpleCondition($operator, $operands, &$params)
1573
    {
1574 36
        if (count($operands) !== 2) {
1575
            throw new InvalidParamException("Operator '$operator' requires two operands.");
1576
        }
1577
1578 36
        list($column, $value) = $operands;
1579
1580 36
        if (strpos($column, '(') === false) {
1581 36
            $column = $this->db->quoteColumnName($column);
1582
        }
1583
1584 36
        if ($value === null) {
1585
            return "$column $operator NULL";
1586 36
        } elseif ($value instanceof Expression) {
1587 6
            foreach ($value->params as $n => $v) {
1588 3
                $params[$n] = $v;
1589
            }
1590
1591 6
            return "$column $operator {$value->expression}";
1592 30
        } elseif ($value instanceof Query) {
1593 3
            list($sql, $params) = $this->build($value, $params);
1594 3
            return "$column $operator ($sql)";
1595
        }
1596
1597 27
        $phName = self::PARAM_PREFIX . count($params);
1598 27
        $params[$phName] = $value;
1599 27
        return "$column $operator $phName";
1600
    }
1601
1602
    /**
1603
     * Creates a SELECT EXISTS() SQL statement.
1604
     * @param string $rawSql the subquery in a raw form to select from.
1605
     * @return string the SELECT EXISTS() SQL statement.
1606
     * @since 2.0.8
1607
     */
1608 60
    public function selectExists($rawSql)
1609
    {
1610 60
        return 'SELECT EXISTS(' . $rawSql . ')';
1611
    }
1612
}
1613