Completed
Push — group-order-expression ( 5a462d )
by Carsten
13:40
created

QueryBuilder::buildOrderBy()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

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