Completed
Push — refactor-db-tests ( 8ba032...86f17c )
by Carsten
10:02
created

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