GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 66e815...df733d )
by Robert
16:26
created

QueryBuilder::buildHashCondition()   D

Complexity

Conditions 9
Paths 16

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 9.0076

Importance

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1190 181
        } elseif (is_array($column)) {
1191 130
            $column = reset($column);
1192 130
        }
1193
1194 181
        $sqlValues = [];
1195 181
        foreach ($values as $i => $value) {
1196 181
            if (is_array($value) || $value instanceof \ArrayAccess) {
1197
                $value = isset($value[$column]) ? $value[$column] : null;
1198
            }
1199 181
            if ($value === null) {
1200
                $sqlValues[$i] = 'NULL';
1201 181
            } elseif ($value instanceof Expression) {
1202
                $sqlValues[$i] = $value->expression;
1203
                foreach ($value->params as $n => $v) {
1204
                    $params[$n] = $v;
1205
                }
1206
            } else {
1207 181
                $phName = self::PARAM_PREFIX . count($params);
1208 181
                $params[$phName] = $value;
1209 181
                $sqlValues[$i] = $phName;
1210
            }
1211 181
        }
1212
1213 181
        if (empty($sqlValues)) {
1214 18
            return $operator === 'IN' ? '0=1' : '';
1215
        }
1216
1217 181
        if (strpos($column, '(') === false) {
1218 181
            $column = $this->db->quoteColumnName($column);
1219 181
        }
1220
1221 181
        if (count($sqlValues) > 1) {
1222 126
            return "$column $operator (" . implode(', ', $sqlValues) . ')';
1223
        } else {
1224 127
            $operator = $operator === 'IN' ? '=' : '<>';
1225 127
            return $column . $operator . reset($sqlValues);
1226
        }
1227
    }
1228
1229
    /**
1230
     * Builds SQL for IN condition
1231
     *
1232
     * @param string $operator
1233
     * @param array $columns
1234
     * @param Query $values
1235
     * @param array $params
1236
     * @return string SQL
1237
     */
1238 14
    protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
1239
    {
1240 14
        list($sql, $params) = $this->build($values, $params);
1241 14
        if (is_array($columns)) {
1242 4
            foreach ($columns as $i => $col) {
1243 4
                if (strpos($col, '(') === false) {
1244 4
                    $columns[$i] = $this->db->quoteColumnName($col);
1245 4
                }
1246 4
            }
1247 4
            return '(' . implode(', ', $columns) . ") $operator ($sql)";
1248
        } else {
1249 10
            if (strpos($columns, '(') === false) {
1250 10
                $columns = $this->db->quoteColumnName($columns);
1251 10
            }
1252 10
            return "$columns $operator ($sql)";
1253
        }
1254
    }
1255
1256
    /**
1257
     * Builds SQL for IN condition
1258
     *
1259
     * @param string $operator
1260
     * @param array|\Traversable $columns
1261
     * @param array $values
1262
     * @param array $params
1263
     * @return string SQL
1264
     */
1265 10
    protected function buildCompositeInCondition($operator, $columns, $values, &$params)
1266
    {
1267 10
        $vss = [];
1268 10
        foreach ($values as $value) {
1269 10
            $vs = [];
1270 10
            foreach ($columns as $column) {
1271 10
                if (isset($value[$column])) {
1272 10
                    $phName = self::PARAM_PREFIX . count($params);
1273 10
                    $params[$phName] = $value[$column];
1274 10
                    $vs[] = $phName;
1275 10
                } else {
1276
                    $vs[] = 'NULL';
1277
                }
1278 10
            }
1279 10
            $vss[] = '(' . implode(', ', $vs) . ')';
1280 10
        }
1281
1282 10
        if (empty($vss)) {
1283
            return $operator === 'IN' ? '0=1' : '';
1284
        }
1285
1286 10
        $sqlColumns = [];
1287 10
        foreach ($columns as $i => $column) {
1288 10
            $sqlColumns[] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
1289 10
        }
1290
1291 10
        return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
1292
    }
1293
1294
    /**
1295
     * Creates an SQL expressions with the `LIKE` operator.
1296
     * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
1297
     * @param array $operands an array of two or three operands
1298
     *
1299
     * - The first operand is the column name.
1300
     * - The second operand is a single value or an array of values that column value
1301
     *   should be compared with. If it is an empty array the generated expression will
1302
     *   be a `false` value if operator is `LIKE` or `OR LIKE`, and empty if operator
1303
     *   is `NOT LIKE` or `OR NOT LIKE`.
1304
     * - An optional third operand can also be provided to specify how to escape special characters
1305
     *   in the value(s). The operand should be an array of mappings from the special characters to their
1306
     *   escaped counterparts. If this operand is not provided, a default escape mapping will be used.
1307
     *   You may use `false` or an empty array to indicate the values are already escaped and no escape
1308
     *   should be applied. Note that when using an escape mapping (or the third operand is not provided),
1309
     *   the values will be automatically enclosed within a pair of percentage characters.
1310
     * @param array $params the binding parameters to be populated
1311
     * @return string the generated SQL expression
1312
     * @throws InvalidParamException if wrong number of operands have been given.
1313
     */
1314 72
    public function buildLikeCondition($operator, $operands, &$params)
1315
    {
1316 72
        if (!isset($operands[0], $operands[1])) {
1317
            throw new InvalidParamException("Operator '$operator' requires two operands.");
1318
        }
1319
1320 72
        $escape = isset($operands[2]) ? $operands[2] : ['%' => '\%', '_' => '\_', '\\' => '\\\\'];
1321 72
        unset($operands[2]);
1322
1323 72
        if (!preg_match('/^(AND |OR |)(((NOT |))I?LIKE)/', $operator, $matches)) {
1324
            throw new InvalidParamException("Invalid operator '$operator'.");
1325
        }
1326 72
        $andor = ' ' . (!empty($matches[1]) ? $matches[1] : 'AND ');
1327 72
        $not = !empty($matches[3]);
1328 72
        $operator = $matches[2];
1329
1330 72
        list($column, $values) = $operands;
1331
1332 72
        if (!is_array($values)) {
1333 28
            $values = [$values];
1334 28
        }
1335
1336 72
        if (empty($values)) {
1337 16
            return $not ? '' : '0=1';
1338
        }
1339
1340 56
        if (strpos($column, '(') === false) {
1341 56
            $column = $this->db->quoteColumnName($column);
1342 56
        }
1343
1344 56
        $parts = [];
1345 56
        foreach ($values as $value) {
1346 56
            if ($value instanceof Expression) {
1347 24
                foreach ($value->params as $n => $v) {
1348
                    $params[$n] = $v;
1349 24
                }
1350 24
                $phName = $value->expression;
1351 24
            } else {
1352 44
                $phName = self::PARAM_PREFIX . count($params);
1353 44
                $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
1354
            }
1355 56
            $parts[] = "$column $operator $phName";
1356 56
        }
1357
1358 56
        return implode($andor, $parts);
1359
    }
1360
1361
    /**
1362
     * Creates an SQL expressions with the `EXISTS` operator.
1363
     * @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
1364
     * @param array $operands contains only one element which is a [[Query]] object representing the sub-query.
1365
     * @param array $params the binding parameters to be populated
1366
     * @return string the generated SQL expression
1367
     * @throws InvalidParamException if the operand is not a [[Query]] object.
1368
     */
1369 18
    public function buildExistsCondition($operator, $operands, &$params)
1370
    {
1371 18
        if ($operands[0] instanceof Query) {
1372 18
            list($sql, $params) = $this->build($operands[0], $params);
1373 18
            return "$operator ($sql)";
1374
        } else {
1375
            throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.');
1376
        }
1377
    }
1378
1379
    /**
1380
     * Creates an SQL expressions like `"column" operator value`.
1381
     * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
1382
     * @param array $operands contains two column names.
1383
     * @param array $params the binding parameters to be populated
1384
     * @return string the generated SQL expression
1385
     * @throws InvalidParamException if wrong number of operands have been given.
1386
     */
1387 30
    public function buildSimpleCondition($operator, $operands, &$params)
1388
    {
1389 30
        if (count($operands) !== 2) {
1390
            throw new InvalidParamException("Operator '$operator' requires two operands.");
1391
        }
1392
1393 30
        list($column, $value) = $operands;
1394
1395 30
        if (strpos($column, '(') === false) {
1396 30
            $column = $this->db->quoteColumnName($column);
1397 30
        }
1398
1399 30
        if ($value === null) {
1400
            return "$column $operator NULL";
1401 30
        } elseif ($value instanceof Expression) {
1402 6
            foreach ($value->params as $n => $v) {
1403 3
                $params[$n] = $v;
1404 6
            }
1405 6
            return "$column $operator {$value->expression}";
1406 24
        } elseif ($value instanceof Query) {
1407 3
            list($sql, $params) = $this->build($value, $params);
1408 3
            return "$column $operator ($sql)";
1409
        } else {
1410 21
            $phName = self::PARAM_PREFIX . count($params);
1411 21
            $params[$phName] = $value;
1412 21
            return "$column $operator $phName";
1413
        }
1414
    }
1415
1416
    /**
1417
     * Creates a SELECT EXISTS() SQL statement.
1418
     * @param string $rawSql the subquery in a raw form to select from.
1419
     * @return string the SELECT EXISTS() SQL statement.
1420
     * @since 2.0.8
1421
     */
1422 51
    public function selectExists($rawSql)
1423
    {
1424 51
        return 'SELECT EXISTS(' . $rawSql . ')';
1425
    }
1426
}
1427