Passed
Push — master ( 4352b8...76d634 )
by Paweł
10:08
created

QueryBuilder::dropDefaultValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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