Completed
Push — master ( 6db6a2...db4ef8 )
by Dmitry
26:49 queued 23:25
created

QueryBuilder::buildHaving()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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