Completed
Push — 2.1 ( 1e24d9...76e6a2 )
by Dmitry
66:20 queued 62:56
created

QueryBuilder::buildCondition()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 26
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 7.0071

Importance

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