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 ( ecd2dc...aafa57 )
by Robert
18:49
created

QueryBuilder::buildLikeCondition()   C

Complexity

Conditions 14
Paths 163

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 14.1132

Importance

Changes 0
Metric Value
dl 0
loc 50
ccs 33
cts 36
cp 0.9167
rs 5.0037
c 0
b 0
f 0
cc 14
eloc 31
nc 163
nop 3
crap 14.1132

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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