Completed
Push — 2.1 ( 9e830b...25ac76 )
by Carsten
09:40 queued 03:44
created

QueryBuilder::build()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 25
ccs 17
cts 17
cp 1
rs 8.8571
cc 3
eloc 16
nc 4
nop 2
crap 3
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
 * @author Qiang Xue <[email protected]>
22
 * @since 2.0
23
 */
24
class QueryBuilder extends \yii\base\Object
25
{
26
    /**
27
     * The prefix for automatically generated query binding parameters.
28
     */
29
    const PARAM_PREFIX = ':qp';
30
31
    /**
32
     * @var Connection the database connection.
33
     */
34
    public $db;
35
    /**
36
     * @var string the separator between different fragments of a SQL statement.
37
     * Defaults to an empty space. This is mainly used by [[build()]] when generating a SQL statement.
38
     */
39
    public $separator = ' ';
40
    /**
41
     * @var array the abstract column types mapped to physical column types.
42
     * This is mainly used to support creating/modifying tables using DB-independent data type specifications.
43
     * Child classes should override this property to declare supported type mappings.
44
     */
45
    public $typeMap = [];
46
47
    /**
48
     * @var array map of query condition to builder methods.
49
     * These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
50
     */
51
    protected $conditionBuilders = [
52
        'NOT' => 'buildNotCondition',
53
        'AND' => 'buildAndCondition',
54
        'OR' => 'buildAndCondition',
55
        'BETWEEN' => 'buildBetweenCondition',
56
        'NOT BETWEEN' => 'buildBetweenCondition',
57
        'IN' => 'buildInCondition',
58
        'NOT IN' => 'buildInCondition',
59
        'LIKE' => 'buildLikeCondition',
60
        'NOT LIKE' => 'buildLikeCondition',
61
        'OR LIKE' => 'buildLikeCondition',
62
        'OR NOT LIKE' => 'buildLikeCondition',
63
        'EXISTS' => 'buildExistsCondition',
64
        'NOT EXISTS' => 'buildExistsCondition',
65
    ];
66
67
68
    /**
69
     * Constructor.
70
     * @param Connection $connection the database connection.
71
     * @param array $config name-value pairs that will be used to initialize the object properties
72
     */
73 696
    public function __construct($connection, $config = [])
74
    {
75 696
        $this->db = $connection;
76 696
        parent::__construct($config);
77 696
    }
78
79
    /**
80
     * Generates a SELECT SQL statement from a [[Query]] object.
81
     * @param Query $query the [[Query]] object from which the SQL statement will be generated.
82
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
83
     * be included in the result with the additional parameters generated during the query building process.
84
     * @return array the generated SQL statement (the first array element) and the corresponding
85
     * parameters to be bound to the SQL statement (the second array element). The parameters returned
86
     * include those provided in `$params`.
87
     */
88 488
    public function build($query, $params = [])
89
    {
90 488
        $query = $query->prepare($this);
91
92 488
        $params = empty($params) ? $query->params : array_merge($params, $query->params);
93
94
        $clauses = [
95 488
            $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
96 488
            $this->buildFrom($query->from, $params),
97 488
            $this->buildJoin($query->join, $params),
98 488
            $this->buildWhere($query->where, $params),
99 488
            $this->buildGroupBy($query->groupBy, $params),
100 488
            $this->buildHaving($query->having, $params),
101 488
        ];
102
103 488
        $sql = implode($this->separator, array_filter($clauses));
104 488
        $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset, $params);
105
106 488
        $union = $this->buildUnion($query->union, $params);
107 488
        if ($union !== '') {
108 8
            $sql = "($sql){$this->separator}$union";
109 8
        }
110
111 488
        return [$sql, $params];
112
    }
113
114
    /**
115
     * Creates an INSERT SQL statement.
116
     * For example,
117
     *
118
     * ```php
119
     * $sql = $queryBuilder->insert('user', [
120
     *     'name' => 'Sam',
121
     *     'age' => 30,
122
     * ], $params);
123
     * ```
124
     *
125
     * The method will properly escape the table and column names.
126
     *
127
     * @param string $table the table that new rows will be inserted into.
128
     * @param array $columns the column data (name => value) to be inserted into the table.
129
     * @param array $params the binding parameters that will be generated by this method.
130
     * They should be bound to the DB command later.
131
     * @return string the INSERT SQL
132
     */
133 75
    public function insert($table, $columns, &$params)
134
    {
135 75
        $schema = $this->db->getSchema();
136 75
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
137 72
            $columnSchemas = $tableSchema->columns;
138 72
        } else {
139 12
            $columnSchemas = [];
140
        }
141 75
        $names = [];
142 75
        $placeholders = [];
143 75
        foreach ($columns as $name => $value) {
144 73
            $names[] = $schema->quoteColumnName($name);
145 73
            if ($value instanceof Expression) {
146 4
                $placeholders[] = $value->expression;
147 4
                foreach ($value->params as $n => $v) {
148
                    $params[$n] = $v;
149 4
                }
150 4
            } else {
151 71
                $phName = self::PARAM_PREFIX . count($params);
152 71
                $placeholders[] = $phName;
153 71
                $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
154
            }
155 75
        }
156
157 75
        return 'INSERT INTO ' . $schema->quoteTableName($table)
158 75
            . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
159 75
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : ' DEFAULT VALUES');
160
    }
161
162
    /**
163
     * Generates a batch INSERT SQL statement.
164
     * For example,
165
     *
166
     * ```php
167
     * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
168
     *     ['Tom', 30],
169
     *     ['Jane', 20],
170
     *     ['Linda', 25],
171
     * ]);
172
     * ```
173
     *
174
     * Note that the values in each row must match the corresponding column names.
175
     *
176
     * The method will properly escape the column names, and quote the values to be inserted.
177
     *
178
     * @param string $table the table that new rows will be inserted into.
179
     * @param array $columns the column names
180
     * @param array $rows the rows to be batch inserted into the table
181
     * @return string the batch INSERT SQL statement
182
     */
183 2
    public function batchInsert($table, $columns, $rows)
184
    {
185 2
        $schema = $this->db->getSchema();
186 2
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
187 2
            $columnSchemas = $tableSchema->columns;
188 2
        } else {
189
            $columnSchemas = [];
190
        }
191
192 2
        $values = [];
193 2
        foreach ($rows as $row) {
194 2
            $vs = [];
195 2
            foreach ($row as $i => $value) {
196 2
                if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
197 2
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
198 2
                }
199 2
                if (is_string($value)) {
200 2
                    $value = $schema->quoteValue($value);
201 2
                } elseif ($value === false) {
202
                    $value = 0;
203 2
                } elseif ($value === null) {
204 2
                    $value = 'NULL';
205 2
                }
206 2
                $vs[] = $value;
207 2
            }
208 2
            $values[] = '(' . implode(', ', $vs) . ')';
209 2
        }
210
211 2
        foreach ($columns as $i => $name) {
212 2
            $columns[$i] = $schema->quoteColumnName($name);
213 2
        }
214
215 2
        return 'INSERT INTO ' . $schema->quoteTableName($table)
216 2
        . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
217
    }
218
219
    /**
220
     * Creates an UPDATE SQL statement.
221
     * For example,
222
     *
223
     * ```php
224
     * $params = [];
225
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
226
     * ```
227
     *
228
     * The method will properly escape the table and column names.
229
     *
230
     * @param string $table the table to be updated.
231
     * @param array $columns the column data (name => value) to be updated.
232
     * @param array|string $condition the condition that will be put in the WHERE part. Please
233
     * refer to [[Query::where()]] on how to specify condition.
234
     * @param array $params the binding parameters that will be modified by this method
235
     * so that they can be bound to the DB command later.
236
     * @return string the UPDATE SQL
237
     */
238 65
    public function update($table, $columns, $condition, &$params)
239
    {
240 65
        if (($tableSchema = $this->db->getTableSchema($table)) !== null) {
241 65
            $columnSchemas = $tableSchema->columns;
242 65
        } else {
243
            $columnSchemas = [];
244
        }
245
246 65
        $lines = [];
247 65
        foreach ($columns as $name => $value) {
248 65
            if ($value instanceof Expression) {
249 6
                $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression;
250 6
                foreach ($value->params as $n => $v) {
251 6
                    $params[$n] = $v;
252 6
                }
253 6
            } else {
254 59
                $phName = self::PARAM_PREFIX . count($params);
255 59
                $lines[] = $this->db->quoteColumnName($name) . '=' . $phName;
256 59
                $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
257
            }
258 65
        }
259
260 65
        $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
261 65
        $where = $this->buildWhere($condition, $params);
262
263 65
        return $where === '' ? $sql : $sql . ' ' . $where;
264
    }
265
266
    /**
267
     * Creates a DELETE SQL statement.
268
     * For example,
269
     *
270
     * ```php
271
     * $sql = $queryBuilder->delete('user', 'status = 0');
272
     * ```
273
     *
274
     * The method will properly escape the table and column names.
275
     *
276
     * @param string $table the table where the data will be deleted from.
277
     * @param array|string $condition the condition that will be put in the WHERE part. Please
278
     * refer to [[Query::where()]] on how to specify condition.
279
     * @param array $params the binding parameters that will be modified by this method
280
     * so that they can be bound to the DB command later.
281
     * @return string the DELETE SQL
282
     */
283 116
    public function delete($table, $condition, &$params)
284
    {
285 116
        $sql = 'DELETE FROM ' . $this->db->quoteTableName($table);
286 116
        $where = $this->buildWhere($condition, $params);
287
288 116
        return $where === '' ? $sql : $sql . ' ' . $where;
289
    }
290
291
    /**
292
     * Builds a SQL statement for creating a new DB table.
293
     *
294
     * The columns in the new  table should be specified as name-definition pairs (e.g. 'name' => 'string'),
295
     * where name stands for a column name which will be properly quoted by the method, and definition
296
     * stands for the column type which can contain an abstract DB type.
297
     * The [[getColumnType()]] method will be invoked to convert any abstract type into a physical one.
298
     *
299
     * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly
300
     * inserted into the generated SQL.
301
     *
302
     * For example,
303
     *
304
     * ```php
305
     * $sql = $queryBuilder->createTable('user', [
306
     *  'id' => 'pk',
307
     *  'name' => 'string',
308
     *  'age' => 'integer',
309
     * ]);
310
     * ```
311
     *
312
     * @param string $table the name of the table to be created. The name will be properly quoted by the method.
313
     * @param array $columns the columns (name => definition) in the new table.
314
     * @param string $options additional SQL fragment that will be appended to the generated SQL.
315
     * @return string the SQL statement for creating a new DB table.
316
     */
317 39
    public function createTable($table, $columns, $options = null)
318
    {
319 39
        $cols = [];
320 39
        foreach ($columns as $name => $type) {
321 39
            if (is_string($name)) {
322 39
                $cols[] = "\t" . $this->db->quoteColumnName($name) . ' ' . $this->getColumnType($type);
323 39
            } else {
324 1
                $cols[] = "\t" . $type;
325
            }
326 39
        }
327 39
        $sql = 'CREATE TABLE ' . $this->db->quoteTableName($table) . " (\n" . implode(",\n", $cols) . "\n)";
328
329 39
        return $options === null ? $sql : $sql . ' ' . $options;
330
    }
331
332
    /**
333
     * Builds a SQL statement for renaming a DB table.
334
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
335
     * @param string $newName the new table name. The name will be properly quoted by the method.
336
     * @return string the SQL statement for renaming a DB table.
337
     */
338 1
    public function renameTable($oldName, $newName)
339
    {
340 1
        return 'RENAME TABLE ' . $this->db->quoteTableName($oldName) . ' TO ' . $this->db->quoteTableName($newName);
341
    }
342
343
    /**
344
     * Builds a SQL statement for dropping a DB table.
345
     * @param string $table the table to be dropped. The name will be properly quoted by the method.
346
     * @return string the SQL statement for dropping a DB table.
347
     */
348 8
    public function dropTable($table)
349
    {
350 8
        return 'DROP TABLE ' . $this->db->quoteTableName($table);
351
    }
352
353
    /**
354
     * Builds a SQL statement for adding a primary key constraint to an existing table.
355
     * @param string $name the name of the primary key constraint.
356
     * @param string $table the table that the primary key constraint will be added to.
357
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
358
     * @return string the SQL statement for adding a primary key constraint to an existing table.
359
     */
360 2
    public function addPrimaryKey($name, $table, $columns)
361
    {
362 2
        if (is_string($columns)) {
363 2
            $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
364 2
        }
365
366 2
        foreach ($columns as $i => $col) {
367 2
            $columns[$i] = $this->db->quoteColumnName($col);
368 2
        }
369
370 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
371 2
            . $this->db->quoteColumnName($name) . '  PRIMARY KEY ('
372 2
            . implode(', ', $columns). ' )';
373
    }
374
375
    /**
376
     * Builds a SQL statement for removing a primary key constraint to an existing table.
377
     * @param string $name the name of the primary key constraint to be removed.
378
     * @param string $table the table that the primary key constraint will be removed from.
379
     * @return string the SQL statement for removing a primary key constraint from an existing table.
380
     */
381 2
    public function dropPrimaryKey($name, $table)
382 1
    {
383 1
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
384 2
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
385
    }
386
387
    /**
388
     * Builds a SQL statement for truncating a DB table.
389
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
390
     * @return string the SQL statement for truncating a DB table.
391
     */
392 5
    public function truncateTable($table)
393
    {
394 5
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table);
395
    }
396
397
    /**
398
     * Builds a SQL statement for adding a new DB column.
399
     * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method.
400
     * @param string $column the name of the new column. The name will be properly quoted by the method.
401
     * @param string $type the column type. The [[getColumnType()]] method will be invoked to convert abstract column type (if any)
402
     * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL.
403
     * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'.
404
     * @return string the SQL statement for adding a new column.
405
     */
406 4
    public function addColumn($table, $column, $type)
407
    {
408 4
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
409 4
            . ' ADD ' . $this->db->quoteColumnName($column) . ' '
410 4
            . $this->getColumnType($type);
411
    }
412
413
    /**
414
     * Builds a SQL statement for dropping a DB column.
415
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
416
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
417
     * @return string the SQL statement for dropping a DB column.
418
     */
419
    public function dropColumn($table, $column)
420
    {
421
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
422
            . ' DROP COLUMN ' . $this->db->quoteColumnName($column);
423
    }
424
425
    /**
426
     * Builds a SQL statement for renaming a column.
427
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
428
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
429
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
430
     * @return string the SQL statement for renaming a DB column.
431
     */
432
    public function renameColumn($table, $oldName, $newName)
433
    {
434
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
435
            . ' RENAME COLUMN ' . $this->db->quoteColumnName($oldName)
436
            . ' TO ' . $this->db->quoteColumnName($newName);
437
    }
438
439
    /**
440
     * Builds a SQL statement for changing the definition of a column.
441
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
442
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
443
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
444
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
445
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
446
     * will become 'varchar(255) not null'.
447
     * @return string the SQL statement for changing the definition of a column.
448
     */
449 1
    public function alterColumn($table, $column, $type)
450
    {
451 1
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' CHANGE '
452 1
            . $this->db->quoteColumnName($column) . ' '
453 1
            . $this->db->quoteColumnName($column) . ' '
454 1
            . $this->getColumnType($type);
455
    }
456
457
    /**
458
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
459
     * The method will properly quote the table and column names.
460
     * @param string $name the name of the foreign key constraint.
461
     * @param string $table the table that the foreign key constraint will be added to.
462
     * @param string|array $columns the name of the column to that the constraint will be added on.
463
     * If there are multiple columns, separate them with commas or use an array to represent them.
464
     * @param string $refTable the table that the foreign key references to.
465
     * @param string|array $refColumns the name of the column that the foreign key references to.
466
     * If there are multiple columns, separate them with commas or use an array to represent them.
467
     * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
468
     * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
469
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
470
     */
471
    public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
472
    {
473
        $sql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
474
            . ' ADD CONSTRAINT ' . $this->db->quoteColumnName($name)
475
            . ' FOREIGN KEY (' . $this->buildColumns($columns) . ')'
476
            . ' REFERENCES ' . $this->db->quoteTableName($refTable)
477
            . ' (' . $this->buildColumns($refColumns) . ')';
478
        if ($delete !== null) {
479
            $sql .= ' ON DELETE ' . $delete;
480
        }
481
        if ($update !== null) {
482
            $sql .= ' ON UPDATE ' . $update;
483
        }
484
485
        return $sql;
486
    }
487
488
    /**
489
     * Builds a SQL statement for dropping a foreign key constraint.
490
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
491
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
492
     * @return string the SQL statement for dropping a foreign key constraint.
493
     */
494
    public function dropForeignKey($name, $table)
495
    {
496
        return 'ALTER TABLE ' . $this->db->quoteTableName($table)
497
            . ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
498
    }
499
500
    /**
501
     * Builds a SQL statement for creating a new index.
502
     * @param string $name the name of the index. The name will be properly quoted by the method.
503
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method.
504
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
505
     * separate them with commas or use an array to represent them. Each column name will be properly quoted
506
     * by the method, unless a parenthesis is found in the name.
507
     * @param boolean $unique whether to add UNIQUE constraint on the created index.
508
     * @return string the SQL statement for creating a new index.
509
     */
510
    public function createIndex($name, $table, $columns, $unique = false)
511
    {
512
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
513
            . $this->db->quoteTableName($name) . ' ON '
514
            . $this->db->quoteTableName($table)
515
            . ' (' . $this->buildColumns($columns) . ')';
516
    }
517
518
    /**
519
     * Builds a SQL statement for dropping an index.
520
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
521
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
522
     * @return string the SQL statement for dropping an index.
523
     */
524
    public function dropIndex($name, $table)
525
    {
526
        return 'DROP INDEX ' . $this->db->quoteTableName($name) . ' ON ' . $this->db->quoteTableName($table);
527
    }
528
529
    /**
530
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
531
     * The sequence will be reset such that the primary key of the next new row inserted
532
     * will have the specified value or 1.
533
     * @param string $table the name of the table whose primary key sequence will be reset
534
     * @param array|string $value the value for the primary key of the next new row inserted. If this is not set,
535
     * the next new row's primary key will have a value 1.
536
     * @return string the SQL statement for resetting sequence
537
     * @throws NotSupportedException if this is not supported by the underlying DBMS
538
     */
539
    public function resetSequence($table, $value = null)
540
    {
541
        throw new NotSupportedException($this->db->getDriverName() . ' does not support resetting sequence.');
542
    }
543
544
    /**
545
     * Builds a SQL statement for enabling or disabling integrity check.
546
     * @param boolean $check whether to turn on or off the integrity check.
547
     * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
548
     * @param string $table the table name. Defaults to empty string, meaning that no table will be changed.
549
     * @return string the SQL statement for checking integrity
550
     * @throws NotSupportedException if this is not supported by the underlying DBMS
551
     */
552
    public function checkIntegrity($check = true, $schema = '', $table = '')
553
    {
554
        throw new NotSupportedException($this->db->getDriverName() . ' does not support enabling/disabling integrity check.');
555
    }
556
557
    /**
558
     * Builds a SQL command for adding comment to column
559
     *
560
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
561
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the method.
562
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
563
     * @return string the SQL statement for adding comment on column
564
     * @since 2.0.8
565
     */
566 1
    public function addCommentOnColumn($table, $column, $comment)
567
    {
568
569 1
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . ' IS ' . $this->db->quoteValue($comment);
570
    }
571
572
    /**
573
     * Builds a SQL command for adding comment to table
574
     *
575
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
576
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
577
     * @return string the SQL statement for adding comment on table
578
     * @since 2.0.8
579
     */
580 1
    public function addCommentOnTable($table, $comment)
581
    {
582 1
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS ' . $this->db->quoteValue($comment);
583
    }
584
585
    /**
586
     * Builds a SQL command for adding comment to column
587
     *
588
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
589
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the method.
590
     * @return string the SQL statement for adding comment on column
591
     * @since 2.0.8
592
     */
593 1
    public function dropCommentFromColumn($table, $column)
594
    {
595 1
        return 'COMMENT ON COLUMN ' . $this->db->quoteTableName($table) . '.' . $this->db->quoteColumnName($column) . ' IS NULL';
596
    }
597
598
    /**
599
     * Builds a SQL command for adding comment to table
600
     *
601
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
602
     * @return string the SQL statement for adding comment on column
603
     * @since 2.0.8
604
     */
605 1
    public function dropCommentFromTable($table)
606
    {
607 1
        return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . ' IS NULL';
608
    }
609
610
    /**
611
     * Converts an abstract column type into a physical column type.
612
     * The conversion is done using the type map specified in [[typeMap]].
613
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
614
     * physical types):
615
     *
616
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY"
617
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY"
618
     * - `unsignedpk`: an unsigned auto-incremental primary key type, will be converted into "int(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY"
619
     * - `char`: char type, will be converted into "char(1)"
620
     * - `string`: string type, will be converted into "varchar(255)"
621
     * - `text`: a long string type, will be converted into "text"
622
     * - `smallint`: a small integer type, will be converted into "smallint(6)"
623
     * - `integer`: integer type, will be converted into "int(11)"
624
     * - `bigint`: a big integer type, will be converted into "bigint(20)"
625
     * - `boolean`: boolean type, will be converted into "tinyint(1)"
626
     * - `float``: float number type, will be converted into "float"
627
     * - `decimal`: decimal number type, will be converted into "decimal"
628
     * - `datetime`: datetime type, will be converted into "datetime"
629
     * - `timestamp`: timestamp type, will be converted into "timestamp"
630
     * - `time`: time type, will be converted into "time"
631
     * - `date`: date type, will be converted into "date"
632
     * - `money`: money type, will be converted into "decimal(19,4)"
633
     * - `binary`: binary data type, will be converted into "blob"
634
     *
635
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only
636
     * the first part will be converted, and the rest of the parts will be appended to the converted result.
637
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
638
     *
639
     * For some of the abstract types you can also specify a length or precision constraint
640
     * by appending it in round brackets directly to the type.
641
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database.
642
     * If the underlying DBMS does not support these kind of constraints for a type it will
643
     * be ignored.
644
     *
645
     * If a type cannot be found in [[typeMap]], it will be returned without any change.
646
     * @param string|ColumnSchemaBuilder $type abstract column type
647
     * @return string physical column type.
648
     */
649 43
    public function getColumnType($type)
650
    {
651 43
        if ($type instanceof ColumnSchemaBuilder) {
652 4
            $type = $type->__toString();
653 4
        }
654
655 43
        if (isset($this->typeMap[$type])) {
656 40
            return $this->typeMap[$type];
657 26
        } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
658 17
            if (isset($this->typeMap[$matches[1]])) {
659 7
                return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3];
660
            }
661 26
        } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
662 17
            if (isset($this->typeMap[$matches[1]])) {
663 17
                return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
664
            }
665
        }
666
667 10
        return $type;
668
    }
669
670
    /**
671
     * @param array $columns
672
     * @param array $params the binding parameters to be populated
673
     * @param boolean $distinct
674
     * @param string $selectOption
675
     * @return string the SELECT clause built from [[Query::$select]].
676
     */
677 691
    public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
678
    {
679 691
        $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
680 691
        if ($selectOption !== null) {
681
            $select .= ' ' . $selectOption;
682
        }
683
684 691
        if (empty($columns)) {
685 586
            return $select . ' *';
686
        }
687
688 270
        foreach ($columns as $i => $column) {
689 270
            if ($column instanceof Expression) {
690 6
                if (is_int($i)) {
691 6
                    $columns[$i] = $column->expression;
692 6
                } else {
693 3
                    $columns[$i] = $column->expression . ' AS ' . $this->db->quoteColumnName($i);
694
                }
695 6
                $params = array_merge($params, $column->params);
696 270
            } elseif ($column instanceof Query) {
697 3
                list($sql, $params) = $this->build($column, $params);
698 3
                $columns[$i] = "($sql) AS " . $this->db->quoteColumnName($i);
699 267
            } elseif (is_string($i)) {
700 9
                if (strpos($column, '(') === false) {
701 9
                    $column = $this->db->quoteColumnName($column);
702 9
                }
703 9
                $columns[$i] = "$column AS " . $this->db->quoteColumnName($i);
704 267
            } elseif (strpos($column, '(') === false) {
705 205
                if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
706 3
                    $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]);
707 3
                } else {
708 205
                    $columns[$i] = $this->db->quoteColumnName($column);
709
                }
710 205
            }
711 270
        }
712
713 270
        return $select . ' ' . implode(', ', $columns);
714
    }
715
716
    /**
717
     * @param array $tables
718
     * @param array $params the binding parameters to be populated
719
     * @return string the FROM clause built from [[Query::$from]].
720
     */
721 691
    public function buildFrom($tables, &$params)
722
    {
723 691
        if (empty($tables)) {
724 265
            return '';
725
        }
726
727 445
        $tables = $this->quoteTableNames($tables, $params);
728
729 445
        return 'FROM ' . implode(', ', $tables);
730
    }
731
732
    /**
733
     * @param array $joins
734
     * @param array $params the binding parameters to be populated
735
     * @return string the JOIN clause built from [[Query::$join]].
736
     * @throws Exception if the $joins parameter is not in proper format
737
     */
738 691
    public function buildJoin($joins, &$params)
739
    {
740 691
        if (empty($joins)) {
741 688
            return '';
742
        }
743
744 39
        foreach ($joins as $i => $join) {
745 39
            if (!is_array($join) || !isset($join[0], $join[1])) {
746
                throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.');
747
            }
748
            // 0:join type, 1:join table, 2:on-condition (optional)
749 39
            list ($joinType, $table) = $join;
750 39
            $tables = $this->quoteTableNames((array) $table, $params);
751 39
            $table = reset($tables);
752 39
            $joins[$i] = "$joinType $table";
753 39
            if (isset($join[2])) {
754 39
                $condition = $this->buildCondition($join[2], $params);
755 39
                if ($condition !== '') {
756 39
                    $joins[$i] .= ' ON ' . $condition;
757 39
                }
758 39
            }
759 39
        }
760
761 39
        return implode($this->separator, $joins);
762
    }
763
764
    /**
765
     * Quotes table names passed
766
     *
767
     * @param array $tables
768
     * @param array $params
769
     * @return array
770
     */
771 445
    private function quoteTableNames($tables, &$params)
772
    {
773 445
        foreach ($tables as $i => $table) {
774 445
            if ($table instanceof Query) {
775 10
                list($sql, $params) = $this->build($table, $params);
776 10
                $tables[$i] = "($sql) " . $this->db->quoteTableName($i);
777 445
            } elseif (is_string($i)) {
778 36
                if (strpos($table, '(') === false) {
779 30
                    $table = $this->db->quoteTableName($table);
780 30
                }
781 36
                $tables[$i] = "$table " . $this->db->quoteTableName($i);
782 445
            } elseif (strpos($table, '(') === false) {
783 437
                if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias
784 18
                    $tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]);
785 18
                } else {
786 422
                    $tables[$i] = $this->db->quoteTableName($table);
787
                }
788 437
            }
789 445
        }
790 445
        return $tables;
791
    }
792
793
    /**
794
     * @param string|array $condition
795
     * @param array $params the binding parameters to be populated
796
     * @return string the WHERE clause built from [[Query::$where]].
797
     */
798 706
    public function buildWhere($condition, &$params)
799
    {
800 706
        $where = $this->buildCondition($condition, $params);
801
802 706
        return $where === '' ? '' : 'WHERE ' . $where;
803
    }
804
805
    /**
806
     * @param array $columns
807
     * @param array $params the binding parameters to be populated
808
     * @return string the GROUP BY clause
809
     */
810 691
    public function buildGroupBy($columns, &$params)
811
    {
812 691
        if (empty($columns)) {
813 685
            return '';
814
        }
815 9
        foreach ($columns as $i => $column) {
816 9
            if ($column instanceof Expression) {
817 3
                $columns[$i] = $column->expression;
818 3
                $params = array_merge($params, $column->params);
819 9
            } elseif (strpos($column, '(') === false) {
820 9
                $columns[$i] = $this->db->quoteColumnName($column);
821 9
            }
822 9
        }
823 9
        return 'GROUP BY ' . implode(', ', $columns);
824
    }
825
826
    /**
827
     * @param string|array $condition
828
     * @param array $params the binding parameters to be populated
829
     * @return string the HAVING clause built from [[Query::$having]].
830
     */
831 691
    public function buildHaving($condition, &$params)
832
    {
833 691
        $having = $this->buildCondition($condition, $params);
834
835 691
        return $having === '' ? '' : 'HAVING ' . $having;
836
    }
837
838
    /**
839
     * Builds the ORDER BY and LIMIT/OFFSET clauses and appends them to the given SQL.
840
     * @param string $sql the existing SQL (without ORDER BY/LIMIT/OFFSET)
841
     * @param array $orderBy the order by columns. See [[Query::orderBy]] for more details on how to specify this parameter.
842
     * @param integer $limit the limit number. See [[Query::limit]] for more details.
843
     * @param integer $offset the offset number. See [[Query::offset]] for more details.
844
     * @param array $params the binding parameters to be populated
845
     * @return string the SQL completed with ORDER BY/LIMIT/OFFSET (if any)
846
     */
847 691
    public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset, &$params)
848
    {
849 691
        $orderBy = $this->buildOrderBy($orderBy, $params);
850 691
        if ($orderBy !== '') {
851 119
            $sql .= $this->separator . $orderBy;
852 119
        }
853 691
        $limit = $this->buildLimit($limit, $offset);
854 691
        if ($limit !== '') {
855 43
            $sql .= $this->separator . $limit;
856 43
        }
857 691
        return $sql;
858
    }
859
860
    /**
861
     * @param array $columns
862
     * @param array $params the binding parameters to be populated
863
     * @return string the ORDER BY clause built from [[Query::$orderBy]].
864
     */
865 691
    public function buildOrderBy($columns, &$params)
866
    {
867 691
        if (empty($columns)) {
868 670
            return '';
869
        }
870 119
        $orders = [];
871 119
        foreach ($columns as $name => $direction) {
872 119
            if ($direction instanceof Expression) {
873 3
                $orders[] = $direction->expression;
874 3
                $params = array_merge($params, $column->params);
0 ignored issues
show
Bug introduced by
The variable $column does not exist. Did you mean $columns?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
875
            } else {
876 119
                $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
877
            }
878 119
        }
879
880 119
        return 'ORDER BY ' . implode(', ', $orders);
881
    }
882
883
    /**
884
     * @param integer $limit
885
     * @param integer $offset
886
     * @return string the LIMIT and OFFSET clauses
887
     */
888 202
    public function buildLimit($limit, $offset)
889
    {
890 202
        $sql = '';
891 202
        if ($this->hasLimit($limit)) {
892 12
            $sql = 'LIMIT ' . $limit;
893 12
        }
894 202
        if ($this->hasOffset($offset)) {
895 2
            $sql .= ' OFFSET ' . $offset;
896 2
        }
897
898 202
        return ltrim($sql);
899
    }
900
901
    /**
902
     * Checks to see if the given limit is effective.
903
     * @param mixed $limit the given limit
904
     * @return boolean whether the limit is effective
905
     */
906 691
    protected function hasLimit($limit)
907
    {
908 691
        return ctype_digit((string) $limit);
909
    }
910
911
    /**
912
     * Checks to see if the given offset is effective.
913
     * @param mixed $offset the given offset
914
     * @return boolean whether the offset is effective
915
     */
916 691
    protected function hasOffset($offset)
917
    {
918 691
        $offset = (string) $offset;
919 691
        return ctype_digit($offset) && $offset !== '0';
920
    }
921
922
    /**
923
     * @param array $unions
924
     * @param array $params the binding parameters to be populated
925
     * @return string the UNION clause built from [[Query::$union]].
926
     */
927 488
    public function buildUnion($unions, &$params)
928
    {
929 488
        if (empty($unions)) {
930 488
            return '';
931
        }
932
933 8
        $result = '';
934
935 8
        foreach ($unions as $i => $union) {
936 8
            $query = $union['query'];
937 8
            if ($query instanceof Query) {
938 8
                list($unions[$i]['query'], $params) = $this->build($query, $params);
939 8
            }
940
941 8
            $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
942 8
        }
943
944 8
        return trim($result);
945
    }
946
947
    /**
948
     * Processes columns and properly quotes them if necessary.
949
     * It will join all columns into a string with comma as separators.
950
     * @param string|array $columns the columns to be processed
951
     * @return string the processing result
952
     */
953
    public function buildColumns($columns)
954
    {
955
        if (!is_array($columns)) {
956
            if (strpos($columns, '(') !== false) {
957
                return $columns;
958
            } else {
959
                $columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
960
            }
961
        }
962
        foreach ($columns as $i => $column) {
963
            if ($column instanceof Expression) {
964
                $columns[$i] = $column->expression;
965
            } elseif (strpos($column, '(') === false) {
966
                $columns[$i] = $this->db->quoteColumnName($column);
967
            }
968
        }
969
970
        return is_array($columns) ? implode(', ', $columns) : $columns;
971
    }
972
973
    /**
974
     * Parses the condition specification and generates the corresponding SQL expression.
975
     * @param string|array|Expression $condition the condition specification. Please refer to [[Query::where()]]
976
     * on how to specify a condition.
977
     * @param array $params the binding parameters to be populated
978
     * @return string the generated SQL expression
979
     */
980 706
    public function buildCondition($condition, &$params)
981
    {
982 706
        if ($condition instanceof Expression) {
983 3
            foreach ($condition->params as $n => $v) {
984 3
                $params[$n] = $v;
985 3
            }
986 3
            return $condition->expression;
987 706
        } elseif (!is_array($condition)) {
988 704
            return (string) $condition;
989 566
        } elseif (empty($condition)) {
990
            return '';
991
        }
992
993 566
        if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
994 352
            $operator = strtoupper($condition[0]);
995 352
            if (isset($this->conditionBuilders[$operator])) {
996 322
                $method = $this->conditionBuilders[$operator];
997 322
            } else {
998 30
                $method = 'buildSimpleCondition';
999
            }
1000 352
            array_shift($condition);
1001 352
            return $this->$method($operator, $condition, $params);
1002
        } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
1003 358
            return $this->buildHashCondition($condition, $params);
1004
        }
1005
    }
1006
1007
    /**
1008
     * Creates a condition based on column-value pairs.
1009
     * @param array $condition the condition specification.
1010
     * @param array $params the binding parameters to be populated
1011
     * @return string the generated SQL expression
1012
     */
1013 358
    public function buildHashCondition($condition, &$params)
1014
    {
1015 358
        $parts = [];
1016 358
        foreach ($condition as $column => $value) {
1017 358
            if (ArrayHelper::isTraversable($value) || $value instanceof Query) {
1018
                // IN condition
1019 54
                $parts[] = $this->buildInCondition('IN', [$column, $value], $params);
1020 54
            } else {
1021 349
                if (strpos($column, '(') === false) {
1022 349
                    $column = $this->db->quoteColumnName($column);
1023 349
                }
1024 349
                if ($value === null) {
1025 12
                    $parts[] = "$column IS NULL";
1026 349
                } elseif ($value instanceof Expression) {
1027 77
                    $parts[] = "$column=" . $value->expression;
1028 77
                    foreach ($value->params as $n => $v) {
1029
                        $params[$n] = $v;
1030 77
                    }
1031 77
                } else {
1032 349
                    $phName = self::PARAM_PREFIX . count($params);
1033 349
                    $parts[] = "$column=$phName";
1034 349
                    $params[$phName] = $value;
1035
                }
1036
            }
1037 358
        }
1038 358
        return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
1039
    }
1040
1041
    /**
1042
     * Connects two or more SQL expressions with the `AND` or `OR` operator.
1043
     * @param string $operator the operator to use for connecting the given operands
1044
     * @param array $operands the SQL expressions to connect.
1045
     * @param array $params the binding parameters to be populated
1046
     * @return string the generated SQL expression
1047
     */
1048 98
    public function buildAndCondition($operator, $operands, &$params)
1049
    {
1050 98
        $parts = [];
1051 98
        foreach ($operands as $operand) {
1052 98
            if (is_array($operand)) {
1053 74
                $operand = $this->buildCondition($operand, $params);
1054 74
            }
1055 98
            if ($operand instanceof Expression) {
1056 6
                foreach ($operand->params as $n => $v) {
1057 6
                    $params[$n] = $v;
1058 6
                }
1059 6
                $operand = $operand->expression;
1060 6
            }
1061 98
            if ($operand !== '') {
1062 98
                $parts[] = $operand;
1063 98
            }
1064 98
        }
1065 98
        if (!empty($parts)) {
1066 98
            return '(' . implode(") $operator (", $parts) . ')';
1067
        } else {
1068
            return '';
1069
        }
1070
    }
1071
1072
    /**
1073
     * Inverts an SQL expressions with `NOT` operator.
1074
     * @param string $operator the operator to use for connecting the given operands
1075
     * @param array $operands the SQL expressions to connect.
1076
     * @param array $params the binding parameters to be populated
1077
     * @return string the generated SQL expression
1078
     * @throws InvalidParamException if wrong number of operands have been given.
1079
     */
1080 3
    public function buildNotCondition($operator, $operands, &$params)
1081
    {
1082 3
        if (count($operands) !== 1) {
1083
            throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
1084
        }
1085
1086 3
        $operand = reset($operands);
1087 3
        if (is_array($operand)) {
1088
            $operand = $this->buildCondition($operand, $params);
1089
        }
1090 3
        if ($operand === '') {
1091
            return '';
1092
        }
1093
1094 3
        return "$operator ($operand)";
1095
    }
1096
1097
    /**
1098
     * Creates an SQL expressions with the `BETWEEN` operator.
1099
     * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
1100
     * @param array $operands the first operand is the column name. The second and third operands
1101
     * describe the interval that column value should be in.
1102
     * @param array $params the binding parameters to be populated
1103
     * @return string the generated SQL expression
1104
     * @throws InvalidParamException if wrong number of operands have been given.
1105
     */
1106 21
    public function buildBetweenCondition($operator, $operands, &$params)
1107
    {
1108 21
        if (!isset($operands[0], $operands[1], $operands[2])) {
1109
            throw new InvalidParamException("Operator '$operator' requires three operands.");
1110
        }
1111
1112 21
        list($column, $value1, $value2) = $operands;
1113
1114 21
        if (strpos($column, '(') === false) {
1115 21
            $column = $this->db->quoteColumnName($column);
1116 21
        }
1117 21
        if ($value1 instanceof Expression) {
1118 12
            foreach ($value1->params as $n => $v) {
1119
                $params[$n] = $v;
1120 12
            }
1121 12
            $phName1 = $value1->expression;
1122 12
        } else {
1123 9
            $phName1 = self::PARAM_PREFIX . count($params);
1124 9
            $params[$phName1] = $value1;
1125
        }
1126 21
        if ($value2 instanceof Expression) {
1127 6
            foreach ($value2->params as $n => $v) {
1128
                $params[$n] = $v;
1129 6
            }
1130 6
            $phName2 = $value2->expression;
1131 6
        } else {
1132 15
            $phName2 = self::PARAM_PREFIX . count($params);
1133 15
            $params[$phName2] = $value2;
1134
        }
1135
1136 21
        return "$column $operator $phName1 AND $phName2";
1137
    }
1138
1139
    /**
1140
     * Creates an SQL expressions with the `IN` operator.
1141
     * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
1142
     * @param array $operands the first operand is the column name. If it is an array
1143
     * a composite IN condition will be generated.
1144
     * The second operand is an array of values that column value should be among.
1145
     * If it is an empty array the generated expression will be a `false` value if
1146
     * operator is `IN` and empty if operator is `NOT IN`.
1147
     * @param array $params the binding parameters to be populated
1148
     * @return string the generated SQL expression
1149
     * @throws Exception if wrong number of operands have been given.
1150
     */
1151 189
    public function buildInCondition($operator, $operands, &$params)
1152
    {
1153 189
        if (!isset($operands[0], $operands[1])) {
1154
            throw new Exception("Operator '$operator' requires two operands.");
1155
        }
1156
1157 189
        list($column, $values) = $operands;
1158
1159 189
        if ($column === []) {
1160
            return $operator === 'IN' ? '0=1' : '';
1161
        }
1162
1163 189
        if ($values instanceof Query) {
1164 14
            return $this->buildSubqueryInCondition($operator, $column, $values, $params);
1165
        }
1166
1167 175
        if ($column instanceof \Traversable || count($column) > 1) {
1168 15
            return $this->buildCompositeInCondition($operator, $column, $values, $params);
1169
        }
1170
1171 163
        if (is_array($column)) {
1172 115
            $column = reset($column);
1173 115
        }
1174
1175 163
        $sqlValues = [];
1176 163
        foreach ($values as $i => $value) {
1177 163
            if (is_array($value) || $value instanceof \ArrayAccess) {
1178
                $value = isset($value[$column]) ? $value[$column] : null;
1179
            }
1180 163
            if ($value === null) {
1181
                $sqlValues[$i] = 'NULL';
1182 163
            } elseif ($value instanceof Expression) {
1183
                $sqlValues[$i] = $value->expression;
1184
                foreach ($value->params as $n => $v) {
1185
                    $params[$n] = $v;
1186
                }
1187
            } else {
1188 163
                $phName = self::PARAM_PREFIX . count($params);
1189 163
                $params[$phName] = $value;
1190 163
                $sqlValues[$i] = $phName;
1191
            }
1192 163
        }
1193
1194 163
        if (empty($sqlValues)) {
1195 15
            return $operator === 'IN' ? '0=1' : '';
1196
        }
1197
1198 163
        if (strpos($column, '(') === false) {
1199 163
            $column = $this->db->quoteColumnName($column);
1200 163
        }
1201
1202 163
        if (count($sqlValues) > 1) {
1203 118
            return "$column $operator (" . implode(', ', $sqlValues) . ')';
1204
        } else {
1205 109
            $operator = $operator === 'IN' ? '=' : '<>';
1206 109
            return $column . $operator . reset($sqlValues);
1207
        }
1208
    }
1209
1210
    /**
1211
     * Builds SQL for IN condition
1212
     *
1213
     * @param string $operator
1214
     * @param array $columns
1215
     * @param Query $values
1216
     * @param array $params
1217
     * @return string SQL
1218
     */
1219 14
    protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
1220
    {
1221 14
        list($sql, $params) = $this->build($values, $params);
1222 14
        if (is_array($columns)) {
1223 4
            foreach ($columns as $i => $col) {
1224 4
                if (strpos($col, '(') === false) {
1225 4
                    $columns[$i] = $this->db->quoteColumnName($col);
1226 4
                }
1227 4
            }
1228 4
            return '(' . implode(', ', $columns) . ") $operator ($sql)";
1229
        } else {
1230 10
            if (strpos($columns, '(') === false) {
1231 10
                $columns = $this->db->quoteColumnName($columns);
1232 10
            }
1233 10
            return "$columns $operator ($sql)";
1234
        }
1235
    }
1236
1237
    /**
1238
     * Builds SQL for IN condition
1239
     *
1240
     * @param string $operator
1241
     * @param array|\Traversable $columns
1242
     * @param array $values
1243
     * @param array $params
1244
     * @return string SQL
1245
     */
1246 10
    protected function buildCompositeInCondition($operator, $columns, $values, &$params)
1247
    {
1248 10
        $vss = [];
1249 10
        foreach ($values as $value) {
1250 10
            $vs = [];
1251 10
            foreach ($columns as $column) {
1252 10
                if (isset($value[$column])) {
1253 10
                    $phName = self::PARAM_PREFIX . count($params);
1254 10
                    $params[$phName] = $value[$column];
1255 10
                    $vs[] = $phName;
1256 10
                } else {
1257
                    $vs[] = 'NULL';
1258
                }
1259 10
            }
1260 10
            $vss[] = '(' . implode(', ', $vs) . ')';
1261 10
        }
1262
1263 10
        if (empty($vss)) {
1264
            return $operator === 'IN' ? '0=1' : '';
1265
        };
1266
1267 10
        $sqlColumns = [];
1268 10
        foreach ($columns as $i => $column) {
1269 10
            $sqlColumns[] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
1270 10
        }
1271
1272 10
        return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
1273
    }
1274
1275
    /**
1276
     * Creates an SQL expressions with the `LIKE` operator.
1277
     * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
1278
     * @param array $operands an array of two or three operands
1279
     *
1280
     * - The first operand is the column name.
1281
     * - The second operand is a single value or an array of values that column value
1282
     *   should be compared with. If it is an empty array the generated expression will
1283
     *   be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
1284
     *   is `NOT LIKE` or `OR NOT LIKE`.
1285
     * - An optional third operand can also be provided to specify how to escape special characters
1286
     *   in the value(s). The operand should be an array of mappings from the special characters to their
1287
     *   escaped counterparts. If this operand is not provided, a default escape mapping will be used.
1288
     *   You may use `false` or an empty array to indicate the values are already escaped and no escape
1289
     *   should be applied. Note that when using an escape mapping (or the third operand is not provided),
1290
     *   the values will be automatically enclosed within a pair of percentage characters.
1291
     * @param array $params the binding parameters to be populated
1292
     * @return string the generated SQL expression
1293
     * @throws InvalidParamException if wrong number of operands have been given.
1294
     */
1295 72
    public function buildLikeCondition($operator, $operands, &$params)
1296
    {
1297 72
        if (!isset($operands[0], $operands[1])) {
1298
            throw new InvalidParamException("Operator '$operator' requires two operands.");
1299
        }
1300
1301 72
        $escape = isset($operands[2]) ? $operands[2] : ['%' => '\%', '_' => '\_', '\\' => '\\\\'];
1302 72
        unset($operands[2]);
1303
1304 72
        if (!preg_match('/^(AND |OR |)(((NOT |))I?LIKE)/', $operator, $matches)) {
1305
            throw new InvalidParamException("Invalid operator '$operator'.");
1306
        }
1307 72
        $andor = ' ' . (!empty($matches[1]) ? $matches[1] : 'AND ');
1308 72
        $not = !empty($matches[3]);
1309 72
        $operator = $matches[2];
1310
1311 72
        list($column, $values) = $operands;
1312
1313 72
        if (!is_array($values)) {
1314 28
            $values = [$values];
1315 28
        }
1316
1317 72
        if (empty($values)) {
1318 16
            return $not ? '' : '0=1';
1319
        }
1320
1321 56
        if (strpos($column, '(') === false) {
1322 56
            $column = $this->db->quoteColumnName($column);
1323 56
        }
1324
1325 56
        $parts = [];
1326 56
        foreach ($values as $value) {
1327 56
            if ($value instanceof Expression) {
1328 24
                foreach ($value->params as $n => $v) {
1329
                    $params[$n] = $v;
1330 24
                }
1331 24
                $phName = $value->expression;
1332 24
            } else {
1333 44
                $phName = self::PARAM_PREFIX . count($params);
1334 44
                $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
1335
            }
1336 56
            $parts[] = "$column $operator $phName";
1337 56
        }
1338
1339 56
        return implode($andor, $parts);
1340
    }
1341
1342
    /**
1343
     * Creates an SQL expressions with the `EXISTS` operator.
1344
     * @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
1345
     * @param array $operands contains only one element which is a [[Query]] object representing the sub-query.
1346
     * @param array $params the binding parameters to be populated
1347
     * @return string the generated SQL expression
1348
     * @throws InvalidParamException if the operand is not a [[Query]] object.
1349
     */
1350 18
    public function buildExistsCondition($operator, $operands, &$params)
1351
    {
1352 18
        if ($operands[0] instanceof Query) {
1353 18
            list($sql, $params) = $this->build($operands[0], $params);
1354 18
            return "$operator ($sql)";
1355
        } else {
1356
            throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.');
1357
        }
1358
    }
1359
1360
    /**
1361
     * Creates an SQL expressions like `"column" operator value`.
1362
     * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
1363
     * @param array $operands contains two column names.
1364
     * @param array $params the binding parameters to be populated
1365
     * @return string the generated SQL expression
1366
     * @throws InvalidParamException if wrong number of operands have been given.
1367
     */
1368 30
    public function buildSimpleCondition($operator, $operands, &$params)
1369
    {
1370 30
        if (count($operands) !== 2) {
1371
            throw new InvalidParamException("Operator '$operator' requires two operands.");
1372
        }
1373
1374 30
        list($column, $value) = $operands;
1375
1376 30
        if (strpos($column, '(') === false) {
1377 30
            $column = $this->db->quoteColumnName($column);
1378 30
        }
1379
1380 30
        if ($value === null) {
1381
            return "$column $operator NULL";
1382 30
        } elseif ($value instanceof Expression) {
1383 6
            foreach ($value->params as $n => $v) {
1384 3
                $params[$n] = $v;
1385 6
            }
1386 6
            return "$column $operator {$value->expression}";
1387 24
        } elseif ($value instanceof Query) {
1388 3
            list($sql, $params) = $this->build($value, $params);
1389 3
            return "$column $operator ($sql)";
1390
        } else {
1391 21
            $phName = self::PARAM_PREFIX . count($params);
1392 21
            $params[$phName] = $value;
1393 21
            return "$column $operator $phName";
1394
        }
1395
    }
1396
1397
    /**
1398
     * Creates a SELECT EXISTS() SQL statement.
1399
     * @param string $rawSql the subquery in a raw form to select from.
1400
     * @return string the SELECT EXISTS() SQL statement.
1401
     * @since 2.0.8
1402
     */
1403 45
    public function selectExists($rawSql)
1404
    {
1405 45
        return 'SELECT EXISTS(' . $rawSql . ')';
1406
    }
1407
}
1408