Completed
Push — batch-query-test ( c73b11 )
by Carsten
09:50
created

QueryBuilder::alterColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

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