Completed
Push — master ( e0dde8...a7d2aa )
by Carsten
09:47
created

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