Completed
Push — master ( 03299e...cda308 )
by Carsten
15:04
created

QueryBuilder::dropTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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