Completed
Push — 2.1 ( 7c8525...0afc41 )
by Alexander
21:05 queued 16:02
created

QueryBuilder::buildAndCondition()   C

Complexity

Conditions 7
Paths 18

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.0178

Importance

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