Completed
Push — tests-for-qb-insert-update-and... ( 0febaa )
by Alexander
13:11
created

QueryBuilder::buildInCondition()   D

Complexity

Conditions 24
Paths 326

Size

Total Lines 61
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 28.6079

Importance

Changes 0
Metric Value
dl 0
loc 61
ccs 28
cts 35
cp 0.8
rs 4.5601
c 0
b 0
f 0
cc 24
eloc 36
nc 326
nop 3
crap 28.6079

How to fix   Long Method    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\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 1182
    public function __construct($connection, $config = [])
92
    {
93 1182
        $this->db = $connection;
94 1182
        parent::__construct($config);
95 1182
    }
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 659
    public function build($query, $params = [])
107
    {
108 659
        $query = $query->prepare($this);
109
110 659
        $params = empty($params) ? $query->params : array_merge($params, $query->params);
111
112
        $clauses = [
113 659
            $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
114 659
            $this->buildFrom($query->from, $params),
115 659
            $this->buildJoin($query->join, $params),
116 659
            $this->buildWhere($query->where, $params),
117 659
            $this->buildGroupBy($query->groupBy),
118 659
            $this->buildHaving($query->having, $params),
119
        ];
120
121 659
        $sql = implode($this->separator, array_filter($clauses));
122 659
        $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset);
123
124 659
        if (!empty($query->orderBy)) {
125 92
            foreach ($query->orderBy as $expression) {
126 92
                if ($expression instanceof Expression) {
127 92
                    $params = array_merge($params, $expression->params);
128
                }
129
            }
130
        }
131 659
        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 659
        $union = $this->buildUnion($query->union, $params);
140 659
        if ($union !== '') {
141 8
            $sql = "($sql){$this->separator}$union";
142
        }
143
144 659
        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 270
    public function insert($table, $columns, &$params)
170
    {
171 270
        $schema = $this->db->getSchema();
172 270
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
173 268
            $columnSchemas = $tableSchema->columns;
174
        } else {
175 2
            $columnSchemas = [];
176
        }
177 270
        $names = [];
178 270
        $placeholders = [];
179 270
        $values = ' DEFAULT VALUES';
180 270
        if ($columns instanceof \yii\db\Query) {
181 10
            list($names, $values, $params) = $this->prepareInsertSelectSubQuery($columns, $schema);
182
        } else {
183 264
            foreach ($columns as $name => $value) {
184 262
                $names[] = $schema->quoteColumnName($name);
185 262
                if ($value instanceof Expression) {
186 7
                    $placeholders[] = $value->expression;
187 7
                    foreach ($value->params as $n => $v) {
188 7
                        $params[$n] = $v;
189
                    }
190 259
                } elseif ($value instanceof \yii\db\Query) {
191 2
                    list($sql, $params) = $this->build($value, $params);
192 2
                    $placeholders[] = "($sql)";
193
                } else {
194 259
                    $phName = self::PARAM_PREFIX . count($params);
195 259
                    $placeholders[] = $phName;
196 262
                    $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
197
                }
198
            }
199
        }
200
201 264
        return 'INSERT INTO ' . $schema->quoteTableName($table)
202 264
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
203 264
            . (!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 95
    public function update($table, $columns, $condition, &$params)
328
    {
329 95
        if (($tableSchema = $this->db->getTableSchema($table)) !== null) {
330 94
            $columnSchemas = $tableSchema->columns;
331
        } else {
332 1
            $columnSchemas = [];
333
        }
334
335 95
        $lines = [];
336 95
        foreach ($columns as $name => $value) {
337 95
            if ($value instanceof Expression) {
338 9
                $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression;
339 9
                foreach ($value->params as $n => $v) {
340 9
                    $params[$n] = $v;
341
                }
342
            } else {
343 89
                $phName = self::PARAM_PREFIX . count($params);
344 89
                $lines[] = $this->db->quoteColumnName($name) . '=' . $phName;
345 95
                $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
346
            }
347
        }
348
349 95
        $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
350 95
        $where = $this->buildWhere($condition, $params);
351
352 95
        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 311
    public function delete($table, $condition, &$params)
374
    {
375 311
        $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
376 311
        $where = $this->buildWhere($condition, $params);
377
378 311
        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 957
    public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
873
    {
874 957
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
875 957
        if ($selectOption !== null) {
876
            $select .= ' ' . $selectOption;
877
        }
878
879 957
        if (empty($columns)) {
880 772
            return $select . ' *';
881
        }
882
883 443
        foreach ($columns as $i => $column) {
884 443
            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 440
            } elseif ($column instanceof Query) {
892 3
                list($sql, $params) = $this->build($column, $params);
893 3
                $columns[$i] = "($sql) AS " . $this->db->quoteColumnName($i);
894 440
            } 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 435
            } elseif (strpos($column, '(') === false) {
900 354
                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 443
                    $columns[$i] = $this->db->quoteColumnName($column);
904
                }
905
            }
906
        }
907
908 443
        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 957
    public function buildFrom($tables, &$params)
917
    {
918 957
        if (empty($tables)) {
919 274
            return '';
920
        }
921
922 702
        $tables = $this->quoteTableNames($tables, $params);
923
924 702
        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 957
    public function buildJoin($joins, &$params)
934
    {
935 957
        if (empty($joins)) {
936 945
            return '';
937
        }
938
939 54
        foreach ($joins as $i => $join) {
940 54
            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 54
            list($joinType, $table) = $join;
945 54
            $tables = $this->quoteTableNames((array) $table, $params);
946 54
            $table = reset($tables);
947 54
            $joins[$i] = "$joinType $table";
948 54
            if (isset($join[2])) {
949 54
                $condition = $this->buildCondition($join[2], $params);
950 54
                if ($condition !== '') {
951 54
                    $joins[$i] .= ' ON ' . $condition;
952
                }
953
            }
954
        }
955
956 54
        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 702
    private function quoteTableNames($tables, &$params)
967
    {
968 702
        foreach ($tables as $i => $table) {
969 702
            if ($table instanceof Query) {
970 10
                list($sql, $params) = $this->build($table, $params);
971 10
                $tables[$i] = "($sql) " . $this->db->quoteTableName($i);
972 702
            } 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 684
            } elseif (strpos($table, '(') === false) {
978 680
                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 702
                    $tables[$i] = $this->db->quoteTableName($table);
982
                }
983
            }
984
        }
985
986 702
        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 1026
    public function buildWhere($condition, &$params)
995
    {
996 1026
        $where = $this->buildCondition($condition, $params);
997
998 1026
        return $where === '' ? '' : 'WHERE ' . $where;
999
    }
1000
1001
    /**
1002
     * @param array $columns
1003
     * @return string the GROUP BY clause
1004
     */
1005 957
    public function buildGroupBy($columns)
1006
    {
1007 957
        if (empty($columns)) {
1008 951
            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 957
    public function buildHaving($condition, &$params)
1027
    {
1028 957
        $having = $this->buildCondition($condition, $params);
1029
1030 957
        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 957
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset)
1042
    {
1043 957
        $orderBy = $this->buildOrderBy($orderBy);
1044 957
        if ($orderBy !== '') {
1045 155
            $sql .= $this->separator . $orderBy;
1046
        }
1047 957
        $limit = $this->buildLimit($limit, $offset);
1048 957
        if ($limit !== '') {
1049 65
            $sql .= $this->separator . $limit;
1050
        }
1051
1052 957
        return $sql;
1053
    }
1054
1055
    /**
1056
     * @param array $columns
1057
     * @return string the ORDER BY clause built from [[Query::$orderBy]].
1058
     */
1059 957
    public function buildOrderBy($columns)
1060
    {
1061 957
        if (empty($columns)) {
1062 924
            return '';
1063
        }
1064 155
        $orders = [];
1065 155
        foreach ($columns as $name => $direction) {
1066 155
            if ($direction instanceof Expression) {
1067 3
                $orders[] = $direction->expression;
1068
            } else {
1069 155
                $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
1070
            }
1071
        }
1072
1073 155
        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 320
    public function buildLimit($limit, $offset)
1082
    {
1083 320
        $sql = '';
1084 320
        if ($this->hasLimit($limit)) {
1085 16
            $sql = 'LIMIT ' . $limit;
1086
        }
1087 320
        if ($this->hasOffset($offset)) {
1088 3
            $sql .= ' OFFSET ' . $offset;
1089
        }
1090
1091 320
        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 618
    protected function hasLimit($limit)
1100
    {
1101 618
        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 618
    protected function hasOffset($offset)
1110
    {
1111 618
        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 659
    public function buildUnion($unions, &$params)
1120
    {
1121 659
        if (empty($unions)) {
1122 659
            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 1026
    public function buildCondition($condition, &$params)
1173
    {
1174 1026
        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 1026
        } elseif (!is_array($condition)) {
1181 1014
            return (string) $condition;
1182
        } elseif (empty($condition)) {
1183
            return '';
1184
        }
1185
1186 800
        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
1187 479
            $operator = strtoupper($condition[0]);
1188 479
            if (isset($this->conditionBuilders[$operator])) {
1189 449
                $method = $this->conditionBuilders[$operator];
1190
            } else {
1191 36
                $method = 'buildSimpleCondition';
1192
            }
1193 479
            array_shift($condition);
1194 479
            return $this->$method($operator, $condition, $params);
1195
        }
1196
1197
        // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
1198 574
        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 574
    public function buildHashCondition($condition, &$params)
1208
    {
1209 574
        $parts = [];
1210 574
        foreach ($condition as $column => $value) {
1211 574
            if (ArrayHelper::isTraversable($value) || $value instanceof Query) {
1212
                // IN condition
1213 71
                $parts[] = $this->buildInCondition('IN', [$column, $value], $params);
1214
            } else {
1215 559
                if (strpos($column, '(') === false) {
1216 553
                    $column = $this->db->quoteColumnName($column);
1217
                }
1218 559
                if ($value === null) {
1219 12
                    $parts[] = "$column IS NULL";
1220 559
                } elseif ($value instanceof Expression) {
1221 126
                    $parts[] = "$column=" . $value->expression;
1222 126
                    foreach ($value->params as $n => $v) {
1223 126
                        $params[$n] = $v;
1224
                    }
1225
                } else {
1226 559
                    $phName = self::PARAM_PREFIX . count($params);
1227 559
                    $parts[] = "$column=$phName";
1228 574
                    $params[$phName] = $value;
1229
                }
1230
            }
1231
        }
1232
1233 574
        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 231
    public function buildInCondition($operator, $operands, &$params)
1347
    {
1348 231
        if (!isset($operands[0], $operands[1])) {
1349
            throw new Exception("Operator '$operator' requires two operands.");
1350
        }
1351
1352 231
        list($column, $values) = $operands;
1353
1354 231
        if ($column === []) {
1355
            // no columns to test against
1356
            return $operator === 'IN' ? '0=1' : '';
1357
        }
1358
1359 231
        if ($values instanceof Query) {
1360 14
            return $this->buildSubqueryInCondition($operator, $column, $values, $params);
1361
        }
1362 217
        if (!is_array($values) && !$values instanceof \Traversable) {
1363
            // ensure values is an array
1364 3
            $values = (array) $values;
1365
        }
1366
1367 217
        if ($column instanceof \Traversable || ((is_array($column) || $column instanceof \Countable) && count($column) > 1)) {
1368 15
            return $this->buildCompositeInCondition($operator, $column, $values, $params);
1369 205
        } elseif (is_array($column)) {
1370 136
            $column = reset($column);
1371
        }
1372
1373 205
        $sqlValues = [];
1374 205
        foreach ($values as $i => $value) {
1375 205
            if (is_array($value) || $value instanceof \ArrayAccess) {
1376
                $value = isset($value[$column]) ? $value[$column] : null;
1377
            }
1378 205
            if ($value === null) {
1379
                $sqlValues[$i] = 'NULL';
1380 205
            } elseif ($value instanceof Expression) {
1381
                $sqlValues[$i] = $value->expression;
1382
                foreach ($value->params as $n => $v) {
1383
                    $params[$n] = $v;
1384
                }
1385
            } else {
1386 205
                $phName = self::PARAM_PREFIX . count($params);
1387 205
                $params[$phName] = $value;
1388 205
                $sqlValues[$i] = $phName;
1389
            }
1390
        }
1391
1392 205
        if (empty($sqlValues)) {
1393 18
            return $operator === 'IN' ? '0=1' : '';
1394
        }
1395
1396 205
        if (strpos($column, '(') === false) {
1397 205
            $column = $this->db->quoteColumnName($column);
1398
        }
1399
1400 205
        if (count($sqlValues) > 1) {
1401 142
            return "$column $operator (" . implode(', ', $sqlValues) . ')';
1402
        }
1403
1404 139
        $operator = $operator === 'IN' ? '=' : '<>';
1405 139
        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