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 ( c73de3...97c43c )
by Robert
13:22
created

QueryBuilder::batchInsert()   C

Complexity

Conditions 11
Paths 41

Size

Total Lines 39
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 11

Importance

Changes 0
Metric Value
dl 0
loc 39
rs 5.2653
c 0
b 0
f 0
ccs 31
cts 31
cp 1
cc 11
eloc 26
nc 41
nop 3
crap 11

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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