Completed
Push — master ( 01636a...5e01dd )
by Dmitry
70:16 queued 67:10
created

QueryBuilder::alterColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 2
cp 0
cc 1
eloc 2
nc 1
nop 3
crap 2
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\ExpressionInterface;
16
use yii\db\Query;
17
use yii\helpers\StringHelper;
18
19
/**
20
 * QueryBuilder is the query builder for SQLite databases.
21
 *
22
 * @author Qiang Xue <[email protected]>
23
 * @since 2.0
24
 */
25
class QueryBuilder extends \yii\db\QueryBuilder
26
{
27
    /**
28
     * @var array mapping from abstract column types (keys) to physical column types (values).
29
     */
30
    public $typeMap = [
31
        Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
32
        Schema::TYPE_UPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
33
        Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
34
        Schema::TYPE_UBIGPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
35
        Schema::TYPE_CHAR => 'char(1)',
36
        Schema::TYPE_STRING => 'varchar(255)',
37
        Schema::TYPE_TEXT => 'text',
38
        Schema::TYPE_TINYINT => 'tinyint',
39
        Schema::TYPE_SMALLINT => 'smallint',
40
        Schema::TYPE_INTEGER => 'integer',
41
        Schema::TYPE_BIGINT => 'bigint',
42
        Schema::TYPE_FLOAT => 'float',
43
        Schema::TYPE_DOUBLE => 'double',
44
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
45
        Schema::TYPE_DATETIME => 'datetime',
46
        Schema::TYPE_TIMESTAMP => 'timestamp',
47
        Schema::TYPE_TIME => 'time',
48
        Schema::TYPE_DATE => 'date',
49
        Schema::TYPE_BINARY => 'blob',
50
        Schema::TYPE_BOOLEAN => 'boolean',
51
        Schema::TYPE_MONEY => 'decimal(19,4)',
52
    ];
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 400
    protected function defaultExpressionBuilders()
58
    {
59 400
        return array_merge(parent::defaultExpressionBuilders(), [
60 400
            'yii\db\conditions\LikeCondition' => 'yii\db\sqlite\conditions\LikeConditionBuilder',
61
            'yii\db\conditions\InCondition' => 'yii\db\sqlite\conditions\InConditionBuilder',
62
        ]);
63
    }
64
65
    /**
66
     * @inheritdoc
67
     * @see https://stackoverflow.com/questions/15277373/sqlite-upsert-update-or-insert/15277374#15277374
68
     */
69 22
    public function upsert($table, $insertColumns, $updateColumns, &$params)
70
    {
71
        /** @var Constraint[] $constraints */
72 22
        list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
73 22
        if (empty($uniqueNames)) {
74 3
            return $this->insert($table, $insertColumns, $params);
75
        }
76
77 19
        list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
78 19
        $insertSql = 'INSERT OR IGNORE INTO ' . $this->db->quoteTableName($table)
79 19
            . (!empty($insertNames) ? ' (' . implode(', ', $insertNames) . ')' : '')
80 19
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
81 19
        if ($updateColumns === false) {
82 4
            return $insertSql;
83
        }
84
85 15
        $updateCondition = ['or'];
86 15
        $quotedTableName = $this->db->quoteTableName($table);
87 15
        foreach ($constraints as $constraint) {
88 15
            $constraintCondition = ['and'];
89 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...
90 15
                $quotedName = $this->db->quoteColumnName($name);
91 15
                $constraintCondition[] = "$quotedTableName.$quotedName=(SELECT $quotedName FROM `EXCLUDED`)";
92
            }
93 15
            $updateCondition[] = $constraintCondition;
94
        }
95 15
        if ($updateColumns === true) {
96 9
            $updateColumns = [];
97 9
            foreach ($updateNames as $name) {
98 9
                $quotedName = $this->db->quoteColumnName($name);
99 9
                if (strrpos($quotedName, '.') === false) {
100 9
                    $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)";
101
                }
102 9
                $updateColumns[$name] = new Expression($quotedName);
103
            }
104
        }
105 15
        $updateSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames)
106 15
            . ') AS (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') '
107 15
            . $this->update($table, $updateColumns, $updateCondition, $params);
108 15
        return "$updateSql; $insertSql;";
109
    }
110
111
    /**
112
     * Generates a batch INSERT SQL statement.
113
     *
114
     * For example,
115
     *
116
     * ```php
117
     * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
118
     *     ['Tom', 30],
119
     *     ['Jane', 20],
120
     *     ['Linda', 25],
121
     * ])->execute();
122
     * ```
123
     *
124
     * Note that the values in each row must match the corresponding column names.
125
     *
126
     * @param string $table the table that new rows will be inserted into.
127
     * @param array $columns the column names
128
     * @param array|\Generator $rows the rows to be batch inserted into the table
129
     * @return string the batch INSERT SQL statement
130
     */
131 14
    public function batchInsert($table, $columns, $rows, &$params = [])
132
    {
133 14
        if (empty($rows)) {
134 2
            return '';
135
        }
136
137
        // SQLite supports batch insert natively since 3.7.11
138
        // http://www.sqlite.org/releaselog/3_7_11.html
139 13
        $this->db->open(); // ensure pdo is not null
140 13
        if (version_compare($this->db->getServerVersion(), '3.7.11', '>=')) {
141 13
            return parent::batchInsert($table, $columns, $rows, $params);
142
        }
143
144
        $schema = $this->db->getSchema();
145
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
146
            $columnSchemas = $tableSchema->columns;
147
        } else {
148
            $columnSchemas = [];
149
        }
150
151
        $values = [];
152
        foreach ($rows as $row) {
153
            $vs = [];
154
            foreach ($row as $i => $value) {
155
                if (isset($columnSchemas[$columns[$i]])) {
156
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
157
                }
158
                if (is_string($value)) {
159
                    $value = $schema->quoteValue($value);
160
                } elseif (is_float($value)) {
161
                    // ensure type cast always has . as decimal separator in all locales
162
                    $value = StringHelper::floatToString($value);
163
                } elseif ($value === false) {
164
                    $value = 0;
165
                } elseif ($value === null) {
166
                    $value = 'NULL';
167
                } elseif ($value instanceof ExpressionInterface) {
168
                    $value = $this->buildExpression($value, $params);
169
                }
170
                $vs[] = $value;
171
            }
172
            $values[] = implode(', ', $vs);
173
        }
174
        if (empty($values)) {
175
            return '';
176
        }
177
178
        foreach ($columns as $i => $name) {
179
            $columns[$i] = $schema->quoteColumnName($name);
180
        }
181
182
        return 'INSERT INTO ' . $schema->quoteTableName($table)
183
        . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION SELECT ', $values);
184
    }
185
186
    /**
187
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
188
     * The sequence will be reset such that the primary key of the next new row inserted
189
     * will have the specified value or 1.
190
     * @param string $tableName the name of the table whose primary key sequence will be reset
191
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
192
     * the next new row's primary key will have a value 1.
193
     * @return string the SQL statement for resetting sequence
194
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
195
     */
196 5
    public function resetSequence($tableName, $value = null)
197
    {
198 5
        $db = $this->db;
199 5
        $table = $db->getTableSchema($tableName);
200 5
        if ($table !== null && $table->sequenceName !== null) {
201 5
            $tableName = $db->quoteTableName($tableName);
202 5
            if ($value === null) {
203 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...
204 1
                $value = $this->db->useMaster(function (Connection $db) use ($key, $tableName) {
205 1
                    return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar();
206 1
                });
207
            } else {
208 5
                $value = (int) $value - 1;
209
            }
210
211 5
            return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->name}'";
212
        } elseif ($table === null) {
213
            throw new InvalidArgumentException("Table not found: $tableName");
214
        }
215
216
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
217
    }
218
219
    /**
220
     * Enables or disables integrity check.
221
     * @param bool $check whether to turn on or off the integrity check.
222
     * @param string $schema the schema of the tables. Meaningless for SQLite.
223
     * @param string $table the table name. Meaningless for SQLite.
224
     * @return string the SQL statement for checking integrity
225
     * @throws NotSupportedException this is not supported by SQLite
226
     */
227
    public function checkIntegrity($check = true, $schema = '', $table = '')
228
    {
229
        return 'PRAGMA foreign_keys=' . (int) $check;
230
    }
231
232
    /**
233
     * Builds a SQL statement for truncating a DB table.
234
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
235
     * @return string the SQL statement for truncating a DB table.
236
     */
237 1
    public function truncateTable($table)
238
    {
239 1
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
240
    }
241
242
    /**
243
     * Builds a SQL statement for dropping an index.
244
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
245
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
246
     * @return string the SQL statement for dropping an index.
247
     */
248 2
    public function dropIndex($name, $table)
249
    {
250 2
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
251
    }
252
253
    /**
254
     * Builds a SQL statement for dropping a DB column.
255
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
256
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
257
     * @return string the SQL statement for dropping a DB column.
258
     * @throws NotSupportedException this is not supported by SQLite
259
     */
260
    public function dropColumn($table, $column)
261
    {
262
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
263
    }
264
265
    /**
266
     * Builds a SQL statement for renaming a column.
267
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
268
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
269
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
270
     * @return string the SQL statement for renaming a DB column.
271
     * @throws NotSupportedException this is not supported by SQLite
272
     */
273
    public function renameColumn($table, $oldName, $newName)
274
    {
275
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
276
    }
277
278
    /**
279
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
280
     * The method will properly quote the table and column names.
281
     * @param string $name the name of the foreign key constraint.
282
     * @param string $table the table that the foreign key constraint will be added to.
283
     * @param string|array $columns the name of the column to that the constraint will be added on.
284
     * If there are multiple columns, separate them with commas or use an array to represent them.
285
     * @param string $refTable the table that the foreign key references to.
286
     * @param string|array $refColumns the name of the column that the foreign key references to.
287
     * If there are multiple columns, separate them with commas or use an array to represent them.
288
     * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
289
     * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
290
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
291
     * @throws NotSupportedException this is not supported by SQLite
292
     */
293
    public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
294
    {
295
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
296
    }
297
298
    /**
299
     * Builds a SQL statement for dropping a foreign key constraint.
300
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method.
301
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
302
     * @return string the SQL statement for dropping a foreign key constraint.
303
     * @throws NotSupportedException this is not supported by SQLite
304
     */
305
    public function dropForeignKey($name, $table)
306
    {
307
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
308
    }
309
310
    /**
311
     * Builds a SQL statement for renaming a DB table.
312
     *
313
     * @param string $table the table to be renamed. The name will be properly quoted by the method.
314
     * @param string $newName the new table name. The name will be properly quoted by the method.
315
     * @return string the SQL statement for renaming a DB table.
316
     */
317 2
    public function renameTable($table, $newName)
318
    {
319 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName);
320
    }
321
322
    /**
323
     * Builds a SQL statement for changing the definition of a column.
324
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method.
325
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
326
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
327
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
328
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
329
     * will become 'varchar(255) not null'.
330
     * @return string the SQL statement for changing the definition of a column.
331
     * @throws NotSupportedException this is not supported by SQLite
332
     */
333
    public function alterColumn($table, $column, $type)
334
    {
335
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
336
    }
337
338
    /**
339
     * Builds a SQL statement for adding a primary key constraint to an existing table.
340
     * @param string $name the name of the primary key constraint.
341
     * @param string $table the table that the primary key constraint will be added to.
342
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
343
     * @return string the SQL statement for adding a primary key constraint to an existing table.
344
     * @throws NotSupportedException this is not supported by SQLite
345
     */
346
    public function addPrimaryKey($name, $table, $columns)
347
    {
348
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
349
    }
350
351
    /**
352
     * Builds a SQL statement for removing a primary key constraint to an existing table.
353
     * @param string $name the name of the primary key constraint to be removed.
354
     * @param string $table the table that the primary key constraint will be removed from.
355
     * @return string the SQL statement for removing a primary key constraint from an existing table.
356
     * @throws NotSupportedException this is not supported by SQLite
357
     */
358
    public function dropPrimaryKey($name, $table)
359
    {
360
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
361
    }
362
363
    /**
364
     * @inheritDoc
365
     * @throws NotSupportedException this is not supported by SQLite.
366
     */
367
    public function addUnique($name, $table, $columns)
368
    {
369
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
370
    }
371
372
    /**
373
     * @inheritDoc
374
     * @throws NotSupportedException this is not supported by SQLite.
375
     */
376
    public function dropUnique($name, $table)
377
    {
378
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
379
    }
380
381
    /**
382
     * @inheritDoc
383
     * @throws NotSupportedException this is not supported by SQLite.
384
     */
385
    public function addCheck($name, $table, $expression)
386
    {
387
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
388
    }
389
390
    /**
391
     * @inheritDoc
392
     * @throws NotSupportedException this is not supported by SQLite.
393
     */
394
    public function dropCheck($name, $table)
395
    {
396
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
397
    }
398
399
    /**
400
     * @inheritDoc
401
     * @throws NotSupportedException this is not supported by SQLite.
402
     */
403
    public function addDefaultValue($name, $table, $column, $value)
404
    {
405
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
406
    }
407
408
    /**
409
     * @inheritDoc
410
     * @throws NotSupportedException this is not supported by SQLite.
411
     */
412
    public function dropDefaultValue($name, $table)
413
    {
414
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
415
    }
416
417
    /**
418
     * {@inheritdoc}
419
     * @throws NotSupportedException
420
     * @since 2.0.8
421
     */
422
    public function addCommentOnColumn($table, $column, $comment)
423
    {
424
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
425
    }
426
427
    /**
428
     * {@inheritdoc}
429
     * @throws NotSupportedException
430
     * @since 2.0.8
431
     */
432
    public function addCommentOnTable($table, $comment)
433
    {
434
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
435
    }
436
437
    /**
438
     * {@inheritdoc}
439
     * @throws NotSupportedException
440
     * @since 2.0.8
441
     */
442
    public function dropCommentFromColumn($table, $column)
443
    {
444
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     * @throws NotSupportedException
450
     * @since 2.0.8
451
     */
452
    public function dropCommentFromTable($table)
453
    {
454
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
455
    }
456
457
    /**
458
     * {@inheritdoc}
459
     */
460 339
    public function buildLimit($limit, $offset)
461
    {
462 339
        $sql = '';
463 339
        if ($this->hasLimit($limit)) {
464 32
            $sql = 'LIMIT ' . $limit;
465 32
            if ($this->hasOffset($offset)) {
466 32
                $sql .= ' OFFSET ' . $offset;
467
            }
468 332
        } elseif ($this->hasOffset($offset)) {
469
            // limit is not optional in SQLite
470
            // http://www.sqlite.org/syntaxdiagrams.html#select-stmt
471 2
            $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1
472
        }
473
474 339
        return $sql;
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480 339
    public function build($query, $params = [])
481
    {
482 339
        $query = $query->prepare($this);
483
484 339
        $params = empty($params) ? $query->params : array_merge($params, $query->params);
485
486
        $clauses = [
487 339
            $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
488 339
            $this->buildFrom($query->from, $params),
489 339
            $this->buildJoin($query->join, $params),
490 339
            $this->buildWhere($query->where, $params),
491 339
            $this->buildGroupBy($query->groupBy),
492 339
            $this->buildHaving($query->having, $params),
493
        ];
494
495 339
        $sql = implode($this->separator, array_filter($clauses));
496 339
        $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset);
497
498 339
        if (!empty($query->orderBy)) {
499 72
            foreach ($query->orderBy as $expression) {
500 72
                if ($expression instanceof ExpressionInterface) {
501 72
                    $this->buildExpression($expression, $params);
502
                }
503
            }
504
        }
505 339
        if (!empty($query->groupBy)) {
506 3
            foreach ($query->groupBy as $expression) {
507 3
                if ($expression instanceof ExpressionInterface) {
508 3
                    $this->buildExpression($expression, $params);
509
                }
510
            }
511
        }
512
513 339
        $union = $this->buildUnion($query->union, $params);
514 339
        if ($union !== '') {
515 2
            $sql = "$sql{$this->separator}$union";
516
        }
517
518 339
        return [$sql, $params];
519
    }
520
521
    /**
522
     * {@inheritdoc}
523
     */
524 339
    public function buildUnion($unions, &$params)
525
    {
526 339
        if (empty($unions)) {
527 339
            return '';
528
        }
529
530 2
        $result = '';
531
532 2
        foreach ($unions as $i => $union) {
533 2
            $query = $union['query'];
534 2
            if ($query instanceof Query) {
535 2
                list($unions[$i]['query'], $params) = $this->build($query, $params);
536
            }
537
538 2
            $result .= ' UNION ' . ($union['all'] ? 'ALL ' : '') . ' ' . $unions[$i]['query'];
539
        }
540
541 2
        return trim($result);
542
    }
543
}
544