Completed
Pull Request — 2.1 (#15718)
by Alex
17:00
created

QueryBuilder::build()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 25
ccs 15
cts 15
cp 1
rs 8.8571
cc 3
eloc 16
nc 4
nop 2
crap 3
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db\sqlite;
9
10
use yii\base\InvalidArgumentException;
11
use yii\base\NotSupportedException;
12
use yii\db\Connection;
13
use yii\db\Constraint;
14
use yii\db\Expression;
15
use yii\db\Query;
16
use yii\helpers\StringHelper;
17
18
/**
19
 * QueryBuilder is the query builder for SQLite databases.
20
 *
21
 * @author Qiang Xue <[email protected]>
22
 * @since 2.0
23
 */
24
class QueryBuilder extends \yii\db\QueryBuilder
25
{
26
    /**
27
     * @var array mapping from abstract column types (keys) to physical column types (values).
28
     */
29
    public $typeMap = [
30
        Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
31
        Schema::TYPE_UPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
32
        Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
33
        Schema::TYPE_UBIGPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
34
        Schema::TYPE_CHAR => 'char(1)',
35
        Schema::TYPE_STRING => 'varchar(255)',
36
        Schema::TYPE_TEXT => 'text',
37
        Schema::TYPE_TINYINT => 'tinyint',
38
        Schema::TYPE_SMALLINT => 'smallint',
39
        Schema::TYPE_INTEGER => 'integer',
40
        Schema::TYPE_BIGINT => 'bigint',
41
        Schema::TYPE_FLOAT => 'float',
42
        Schema::TYPE_DOUBLE => 'double',
43
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
44
        Schema::TYPE_DATETIME => 'datetime',
45
        Schema::TYPE_TIMESTAMP => 'timestamp',
46
        Schema::TYPE_TIME => 'time',
47
        Schema::TYPE_DATE => 'date',
48
        Schema::TYPE_BINARY => 'blob',
49
        Schema::TYPE_BOOLEAN => 'boolean',
50
        Schema::TYPE_MONEY => 'decimal(19,4)',
51
    ];
52
53
    /**
54
     * {@inheritdoc}
55
     */
56 400
    protected function defaultExpressionBuilders()
57
    {
58 400
        return array_merge(parent::defaultExpressionBuilders(), [
59 400
            \yii\db\conditions\LikeCondition::class => conditions\LikeConditionBuilder::class,
60
            \yii\db\conditions\InCondition::class => conditions\InConditionBuilder::class,
61
        ]);
62
    }
63
64
    /**
65
     * @inheritdoc
66
     * @see https://stackoverflow.com/questions/15277373/sqlite-upsert-update-or-insert/15277374#15277374
67
     */
68 22
    public function upsert($table, $insertColumns, $updateColumns, &$params)
69
    {
70
        /** @var Constraint[] $constraints */
71 22
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
0 ignored issues
show
Bug introduced by
The variable $uniqueNames does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $insertNames does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $updateNames does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
72 22
        if (empty($uniqueNames)) {
73 3
            return $this->insert($table, $insertColumns, $params);
74
        }
75
76 19
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
0 ignored issues
show
Bug introduced by
The variable $placeholders does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $values does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
77 19
        $insertSql = 'INSERT OR IGNORE INTO ' . $this->db->quoteTableName($table)
78 19
            . (!empty($insertNames) ? ' (' . implode(', ', $insertNames) . ')' : '')
79 19
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
80 19
        if ($updateColumns === false) {
81 4
            return $insertSql;
82
        }
83
84 15
        $updateCondition = ['or'];
85 15
        $quotedTableName = $this->db->quoteTableName($table);
86 15
        foreach ($constraints as $constraint) {
87 15
            $constraintCondition = ['and'];
88 15
            foreach ($constraint->columnNames as $name) {
0 ignored issues
show
Bug introduced by
The expression $constraint->columnNames of type array<integer,string>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
89 15
                $quotedName = $this->db->quoteColumnName($name);
90 15
                $constraintCondition[] = "$quotedTableName.$quotedName=(SELECT $quotedName FROM `EXCLUDED`)";
91
            }
92 15
            $updateCondition[] = $constraintCondition;
93
        }
94 15
        if ($updateColumns === true) {
95 9
            $updateColumns = [];
96 9
            foreach ($updateNames as $name) {
97 9
                $quotedName = $this->db->quoteColumnName($name);
98 9
                if (strrpos($quotedName, '.') === false) {
99 9
                    $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)";
100
                }
101 9
                $updateColumns[$name] = new Expression($quotedName);
102
            }
103
        }
104 15
        $updateSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames)
105 15
            . ') AS (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') '
106 15
            . $this->update($table, $updateColumns, $updateCondition, $params);
107 15
        return "$updateSql; $insertSql;";
108
    }
109
110
    /**
111
     * Generates a batch INSERT SQL statement.
112
     *
113
     * For example,
114
     *
115
     * ```php
116
     * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
117
     *     ['Tom', 30],
118
     *     ['Jane', 20],
119
     *     ['Linda', 25],
120
     * ])->execute();
121
     * ```
122
     *
123
     * Note that the values in each row must match the corresponding column names.
124
     *
125
     * @param string $table the table that new rows will be inserted into.
126
     * @param array $columns the column names
127
     * @param array|\Generator $rows the rows to be batch inserted into the table
128
     * @return string the batch INSERT SQL statement
129
     */
130 14
    public function batchInsert($table, $columns, $rows)
131
    {
132 14
        if (empty($rows)) {
133 2
            return '';
134
        }
135
136
        // SQLite supports batch insert natively since 3.7.11
137
        // http://www.sqlite.org/releaselog/3_7_11.html
138 13
        $this->db->open(); // ensure pdo is not null
139 13
        if (version_compare($this->db->getServerVersion(), '3.7.11', '>=')) {
140 13
            return parent::batchInsert($table, $columns, $rows);
141
        }
142
143
        $schema = $this->db->getSchema();
144
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
145
            $columnSchemas = $tableSchema->columns;
146
        } else {
147
            $columnSchemas = [];
148
        }
149
150
        $values = [];
151
        foreach ($rows as $row) {
152
            $vs = [];
153
            foreach ($row as $i => $value) {
154
                if (isset($columnSchemas[$columns[$i]])) {
155
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
156
                }
157
                if (is_string($value)) {
158
                    $value = $schema->quoteValue($value);
159
                } elseif (is_float($value)) {
160
                    // ensure type cast always has . as decimal separator in all locales
161
                    $value = StringHelper::floatToString($value);
162
                } elseif ($value === false) {
163
                    $value = 0;
164
                } elseif ($value === null) {
165
                    $value = 'NULL';
166
                }
167
                $vs[] = $value;
168
            }
169
            $values[] = implode(', ', $vs);
170
        }
171
        if (empty($values)) {
172
            return '';
173
        }
174
175
        foreach ($columns as $i => $name) {
176
            $columns[$i] = $schema->quoteColumnName($name);
177
        }
178
179
        return 'INSERT INTO ' . $schema->quoteTableName($table)
180
        . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION SELECT ', $values);
181
    }
182
183
    /**
184
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
185
     * The sequence will be reset such that the primary key of the next new row inserted
186
     * will have the specified value or 1.
187
     * @param string $tableName the name of the table whose primary key sequence will be reset
188
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
189
     * the next new row's primary key will have a value 1.
190
     * @return string the SQL statement for resetting sequence
191
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
192
     */
193 5
    public function resetSequence($tableName, $value = null)
194
    {
195 5
        $db = $this->db;
196 5
        $table = $db->getTableSchema($tableName);
197 5
        if ($table !== null && $table->sequenceName !== null) {
198 5
            $tableName = $db->quoteTableName($tableName);
199 5
            if ($value === null) {
200 1
                $key = $this->db->quoteColumnName(reset($table->primaryKey));
0 ignored issues
show
Security Bug introduced by
It seems like reset($table->primaryKey) targeting reset() can also be of type false; however, yii\db\Connection::quoteColumnName() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
201 1
                $value = $this->db->useMaster(function (Connection $db) use ($key, $tableName) {
202 1
                    return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar();
203 1
                });
204
            } else {
205 5
                $value = (int) $value - 1;
206
            }
207
208 5
            return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->name}'";
209
        } elseif ($table === null) {
210
            throw new InvalidArgumentException("Table not found: $tableName");
211
        }
212
213
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
214
    }
215
216
    /**
217
     * Enables or disables integrity check.
218
     * @param bool $check whether to turn on or off the integrity check.
219
     * @param string $schema the schema of the tables. Meaningless for SQLite.
220
     * @param string $table the table name. Meaningless for SQLite.
221
     * @return string the SQL statement for checking integrity
222
     * @throws NotSupportedException this is not supported by SQLite
223
     */
224
    public function checkIntegrity($check = true, $schema = '', $table = '')
225
    {
226
        return 'PRAGMA foreign_keys=' . (int) $check;
227
    }
228
229
    /**
230
     * Builds a SQL statement for truncating a DB table.
231
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
232
     * @return string the SQL statement for truncating a DB table.
233
     */
234 1
    public function truncateTable($table)
235
    {
236 1
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
237
    }
238
239
    /**
240
     * Builds a SQL statement for dropping an index.
241
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
242
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
243
     * @return string the SQL statement for dropping an index.
244
     */
245 2
    public function dropIndex($name, $table)
246
    {
247 2
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
248
    }
249
250
    /**
251
     * Builds a SQL statement for dropping a DB column.
252
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
253
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
254
     * @return string the SQL statement for dropping a DB column.
255
     * @throws NotSupportedException this is not supported by SQLite
256
     */
257
    public function dropColumn($table, $column)
258
    {
259
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
260
    }
261
262
    /**
263
     * Builds a SQL statement for renaming a column.
264
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
265
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
266
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
267
     * @return string the SQL statement for renaming a DB column.
268
     * @throws NotSupportedException this is not supported by SQLite
269
     */
270
    public function renameColumn($table, $oldName, $newName)
271
    {
272
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
273
    }
274
275
    /**
276
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
277
     * The method will properly quote the table and column names.
278
     * @param string $name the name of the foreign key constraint.
279
     * @param string $table the table that the foreign key constraint will be added to.
280
     * @param string|array $columns the name of the column to that the constraint will be added on.
281
     * If there are multiple columns, separate them with commas or use an array to represent them.
282
     * @param string $refTable the table that the foreign key references to.
283
     * @param string|array $refColumns the name of the column that the foreign key references to.
284
     * If there are multiple columns, separate them with commas or use an array to represent them.
285
     * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
286
     * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
287
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
288
     * @throws NotSupportedException this is not supported by SQLite
289
     */
290
    public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
291
    {
292
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
293
    }
294
295
    /**
296
     * Builds a SQL statement for dropping a foreign key constraint.
297
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
298
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
299
     * @return string the SQL statement for dropping a foreign key constraint.
300
     * @throws NotSupportedException this is not supported by SQLite
301
     */
302
    public function dropForeignKey($name, $table)
303
    {
304
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
305
    }
306
307
    /**
308
     * Builds a SQL statement for renaming a DB table.
309
     *
310
     * @param string $table the table to be renamed. The name will be properly quoted by the method.
311
     * @param string $newName the new table name. The name will be properly quoted by the method.
312
     * @return string the SQL statement for renaming a DB table.
313
     */
314 2
    public function renameTable($table, $newName)
315
    {
316 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName);
317
    }
318
319
    /**
320
     * Builds a SQL statement for changing the definition of a column.
321
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
322
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
323
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
324
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
325
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
326
     * will become 'varchar(255) not null'.
327
     * @return string the SQL statement for changing the definition of a column.
328
     * @throws NotSupportedException this is not supported by SQLite
329
     */
330
    public function alterColumn($table, $column, $type)
331
    {
332
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
333
    }
334
335
    /**
336
     * Builds a SQL statement for adding a primary key constraint to an existing table.
337
     * @param string $name the name of the primary key constraint.
338
     * @param string $table the table that the primary key constraint will be added to.
339
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
340
     * @return string the SQL statement for adding a primary key constraint to an existing table.
341
     * @throws NotSupportedException this is not supported by SQLite
342
     */
343
    public function addPrimaryKey($name, $table, $columns)
344
    {
345
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
346
    }
347
348
    /**
349
     * Builds a SQL statement for removing a primary key constraint to an existing table.
350
     * @param string $name the name of the primary key constraint to be removed.
351
     * @param string $table the table that the primary key constraint will be removed from.
352
     * @return string the SQL statement for removing a primary key constraint from an existing table.
353
     * @throws NotSupportedException this is not supported by SQLite
354
     */
355
    public function dropPrimaryKey($name, $table)
356
    {
357
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
358
    }
359
360
    /**
361
     * @inheritDoc
362
     * @throws NotSupportedException this is not supported by SQLite.
363
     */
364
    public function addUnique($name, $table, $columns)
365
    {
366
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
367
    }
368
369
    /**
370
     * @inheritDoc
371
     * @throws NotSupportedException this is not supported by SQLite.
372
     */
373
    public function dropUnique($name, $table)
374
    {
375
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
376
    }
377
378
    /**
379
     * @inheritDoc
380
     * @throws NotSupportedException this is not supported by SQLite.
381
     */
382
    public function addCheck($name, $table, $expression)
383
    {
384
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
385
    }
386
387
    /**
388
     * @inheritDoc
389
     * @throws NotSupportedException this is not supported by SQLite.
390
     */
391
    public function dropCheck($name, $table)
392
    {
393
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
394
    }
395
396
    /**
397
     * @inheritDoc
398
     * @throws NotSupportedException this is not supported by SQLite.
399
     */
400
    public function addDefaultValue($name, $table, $column, $value)
401
    {
402
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
403
    }
404
405
    /**
406
     * @inheritDoc
407
     * @throws NotSupportedException this is not supported by SQLite.
408
     */
409
    public function dropDefaultValue($name, $table)
410
    {
411
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
412
    }
413
414
    /**
415
     * {@inheritdoc}
416
     * @throws NotSupportedException
417
     * @since 2.0.8
418
     */
419
    public function addCommentOnColumn($table, $column, $comment)
420
    {
421
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     * @throws NotSupportedException
427
     * @since 2.0.8
428
     */
429
    public function addCommentOnTable($table, $comment)
430
    {
431
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
432
    }
433
434
    /**
435
     * {@inheritdoc}
436
     * @throws NotSupportedException
437
     * @since 2.0.8
438
     */
439
    public function dropCommentFromColumn($table, $column)
440
    {
441
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
442
    }
443
444
    /**
445
     * {@inheritdoc}
446
     * @throws NotSupportedException
447
     * @since 2.0.8
448
     */
449
    public function dropCommentFromTable($table)
450
    {
451
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
452
    }
453
454
    /**
455
     * {@inheritdoc}
456
     */
457 339
    public function buildLimit($limit, $offset)
458
    {
459 339
        $sql = '';
460 339
        if ($this->hasLimit($limit)) {
461 32
            $sql = 'LIMIT ' . $limit;
462 32
            if ($this->hasOffset($offset)) {
463 32
                $sql .= ' OFFSET ' . $offset;
464
            }
465 332
        } elseif ($this->hasOffset($offset)) {
466
            // limit is not optional in SQLite
467
            // http://www.sqlite.org/syntaxdiagrams.html#select-stmt
468 2
            $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1
469
        }
470
471 339
        return $sql;
472
    }
473
474
    /**
475
     * {@inheritdoc}
476
     */
477 339
    public function build($query, $params = [])
478
    {
479 339
        $query = $query->prepare($this);
480
481 339
        $params = empty($params) ? $query->params : array_merge($params, $query->params);
482
483
        $clauses = [
484 339
            $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
485 339
            $this->buildFrom($query->from, $params),
486 339
            $this->buildJoin($query->join, $params),
487 339
            $this->buildWhere($query->where, $params),
488 339
            $this->buildGroupBy($query->groupBy, $params),
489 339
            $this->buildHaving($query->having, $params),
490
        ];
491
492 339
        $sql = implode($this->separator, array_filter($clauses));
493 339
        $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset, $params);
494
495 339
        $union = $this->buildUnion($query->union, $params);
496 339
        if ($union !== '') {
497 2
            $sql = "$sql{$this->separator}$union";
498
        }
499
500 339
        return [$sql, $params];
501
    }
502
503
    /**
504
     * {@inheritdoc}
505
     */
506 339
    public function buildUnion($unions, &$params)
507
    {
508 339
        if (empty($unions)) {
509 339
            return '';
510
        }
511
512 2
        $result = '';
513
514 2
        foreach ($unions as $i => $union) {
515 2
            $query = $union['query'];
516 2
            if ($query instanceof Query) {
517 2
                [$unions[$i]['query'], $params] = $this->build($query, $params);
518
            }
519
520 2
            $result .= ' UNION ' . ($union['all'] ? 'ALL ' : '') . ' ' . $unions[$i]['query'];
521
        }
522
523 2
        return trim($result);
524
    }
525
}
526