Completed
Push — readme-redesign ( e2fd40...17eb05 )
by Alexander
108:51 queued 68:52
created

QueryBuilder::dropCommentFromColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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