Completed
Push — fixes-14366-upgrade-php-72alph... ( 5b72b9 )
by
unknown
04:39
created

QueryBuilder::buildAndCondition()   C

Complexity

Conditions 7
Paths 18

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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