Completed
Push — unique-validator-fix ( 963824...d331b7 )
by Alexander
29:51 queued 14:14
created

QueryBuilder::insert()   C

Complexity

Conditions 11
Paths 16

Size

Total Lines 36
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 11.0077

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 24
cts 25
cp 0.96
rs 5.2653
c 0
b 0
f 0
cc 11
eloc 28
nc 16
nop 3
crap 11.0077

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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