Completed
Push — 2.1 ( b44a46...4c2160 )
by
unknown
12:30
created

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