Passed
Push — master ( 9dbdd9...d5a428 )
by Alexander
04:15
created

framework/db/sqlite/Schema.php (2 issues)

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\NotSupportedException;
11
use yii\db\CheckConstraint;
12
use yii\db\ColumnSchema;
13
use yii\db\Constraint;
14
use yii\db\ConstraintFinderInterface;
15
use yii\db\ConstraintFinderTrait;
16
use yii\db\Expression;
17
use yii\db\ForeignKeyConstraint;
18
use yii\db\IndexConstraint;
19
use yii\db\SqlToken;
20
use yii\db\TableSchema;
21
use yii\db\Transaction;
22
use yii\helpers\ArrayHelper;
23
24
/**
25
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
26
 *
27
 * @property-write string $transactionIsolationLevel The transaction isolation level to use for this
28
 * transaction. This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]]. This
29
 * property is write-only.
30
 *
31
 * @author Qiang Xue <[email protected]>
32
 * @since 2.0
33
 */
34
class Schema extends \yii\db\Schema implements ConstraintFinderInterface
35
{
36
    use ConstraintFinderTrait;
37
38
    /**
39
     * @var array mapping from physical column types (keys) to abstract column types (values)
40
     */
41
    public $typeMap = [
42
        'tinyint' => self::TYPE_TINYINT,
43
        'bit' => self::TYPE_SMALLINT,
44
        'boolean' => self::TYPE_BOOLEAN,
45
        'bool' => self::TYPE_BOOLEAN,
46
        'smallint' => self::TYPE_SMALLINT,
47
        'mediumint' => self::TYPE_INTEGER,
48
        'int' => self::TYPE_INTEGER,
49
        'integer' => self::TYPE_INTEGER,
50
        'bigint' => self::TYPE_BIGINT,
51
        'float' => self::TYPE_FLOAT,
52
        'double' => self::TYPE_DOUBLE,
53
        'real' => self::TYPE_FLOAT,
54
        'decimal' => self::TYPE_DECIMAL,
55
        'numeric' => self::TYPE_DECIMAL,
56
        'tinytext' => self::TYPE_TEXT,
57
        'mediumtext' => self::TYPE_TEXT,
58
        'longtext' => self::TYPE_TEXT,
59
        'text' => self::TYPE_TEXT,
60
        'varchar' => self::TYPE_STRING,
61
        'string' => self::TYPE_STRING,
62
        'char' => self::TYPE_CHAR,
63
        'blob' => self::TYPE_BINARY,
64
        'datetime' => self::TYPE_DATETIME,
65
        'year' => self::TYPE_DATE,
66
        'date' => self::TYPE_DATE,
67
        'time' => self::TYPE_TIME,
68
        'timestamp' => self::TYPE_TIMESTAMP,
69
        'enum' => self::TYPE_STRING,
70
    ];
71
72
    /**
73
     * {@inheritdoc}
74
     */
75
    protected $tableQuoteCharacter = '`';
76
    /**
77
     * {@inheritdoc}
78
     */
79
    protected $columnQuoteCharacter = '`';
80
81
82
    /**
83
     * {@inheritdoc}
84
     */
85 6
    protected function findTableNames($schema = '')
86
    {
87 6
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
88 6
        return $this->db->createCommand($sql)->queryColumn();
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 407
    protected function loadTableSchema($name)
95
    {
96 407
        $table = new TableSchema();
97 407
        $table->name = $name;
98 407
        $table->fullName = $name;
99
100 407
        if ($this->findColumns($table)) {
101 311
            $this->findConstraints($table);
102 311
            return $table;
103
        }
104
105 134
        return null;
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 40
    protected function loadTablePrimaryKey($tableName)
112
    {
113 40
        return $this->loadTableConstraints($tableName, 'primaryKey');
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 3
    protected function loadTableForeignKeys($tableName)
120
    {
121 3
        $foreignKeys = $this->db->createCommand('PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
122 3
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
123 3
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
124 3
        ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
125 3
        $result = [];
126 3
        foreach ($foreignKeys as $table => $foreignKey) {
127 3
            $result[] = new ForeignKeyConstraint([
128 3
                'columnNames' => ArrayHelper::getColumn($foreignKey, 'from'),
129 3
                'foreignTableName' => $table,
130 3
                'foreignColumnNames' => ArrayHelper::getColumn($foreignKey, 'to'),
131 3
                'onDelete' => isset($foreignKey[0]['on_delete']) ? $foreignKey[0]['on_delete'] : null,
132 3
                'onUpdate' => isset($foreignKey[0]['on_update']) ? $foreignKey[0]['on_update'] : null,
133
            ]);
134
        }
135
136 3
        return $result;
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 10
    protected function loadTableIndexes($tableName)
143
    {
144 10
        return $this->loadTableConstraints($tableName, 'indexes');
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 12
    protected function loadTableUniques($tableName)
151
    {
152 12
        return $this->loadTableConstraints($tableName, 'uniques');
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 12
    protected function loadTableChecks($tableName)
159
    {
160 12
        $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
161 12
            ':tableName' => $tableName,
162 12
        ])->queryScalar();
163
        /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
164 12
        $code = (new SqlTokenizer($sql))->tokenize();
165 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
166 12
        if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $lastMatchIndex seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $firstMatchIndex seems to be never defined.
Loading history...
167
            return [];
168
        }
169
170 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
171 12
        $result = [];
172 12
        $offset = 0;
173 12
        while (true) {
174 12
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
175 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
176 12
                break;
177
            }
178
179 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
180 3
            $name = null;
181 3
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
182 3
            if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
183
                $name = $createTableToken[$firstMatchIndex - 1]->content;
184
            }
185 3
            $result[] = new CheckConstraint([
186 3
                'name' => $name,
187 3
                'expression' => $checkSql,
188
            ]);
189
        }
190
191 12
        return $result;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     * @throws NotSupportedException if this method is called.
197
     */
198 12
    protected function loadTableDefaultValues($tableName)
199
    {
200 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
201
    }
202
203
    /**
204
     * Creates a query builder for the MySQL database.
205
     * This method may be overridden by child classes to create a DBMS-specific query builder.
206
     * @return QueryBuilder query builder instance
207
     */
208 315
    public function createQueryBuilder()
209
    {
210 315
        return new QueryBuilder($this->db);
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     * @return ColumnSchemaBuilder column schema builder instance
216
     */
217 13
    public function createColumnSchemaBuilder($type, $length = null)
218
    {
219 13
        return new ColumnSchemaBuilder($type, $length);
220
    }
221
222
    /**
223
     * Collects the table column metadata.
224
     * @param TableSchema $table the table metadata
225
     * @return bool whether the table exists in the database
226
     */
227 407
    protected function findColumns($table)
228
    {
229 407
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->name) . ')';
230 407
        $columns = $this->db->createCommand($sql)->queryAll();
231 407
        if (empty($columns)) {
232 134
            return false;
233
        }
234
235 311
        foreach ($columns as $info) {
236 311
            $column = $this->loadColumnSchema($info);
237 311
            $table->columns[$column->name] = $column;
238 311
            if ($column->isPrimaryKey) {
239 311
                $table->primaryKey[] = $column->name;
240
            }
241
        }
242 311
        if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) {
243 246
            $table->sequenceName = '';
244 246
            $table->columns[$table->primaryKey[0]]->autoIncrement = true;
245
        }
246
247 311
        return true;
248
    }
249
250
    /**
251
     * Collects the foreign key column details for the given table.
252
     * @param TableSchema $table the table metadata
253
     */
254 311
    protected function findConstraints($table)
255
    {
256 311
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->name) . ')';
257 311
        $keys = $this->db->createCommand($sql)->queryAll();
258 311
        foreach ($keys as $key) {
259 7
            $id = (int) $key['id'];
260 7
            if (!isset($table->foreignKeys[$id])) {
261 7
                $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']];
262
            } else {
263
                // composite FK
264 7
                $table->foreignKeys[$id][$key['from']] = $key['to'];
265
            }
266
        }
267 311
    }
268
269
    /**
270
     * Returns all unique indexes for the given table.
271
     *
272
     * Each array element is of the following structure:
273
     *
274
     * ```php
275
     * [
276
     *     'IndexName1' => ['col1' [, ...]],
277
     *     'IndexName2' => ['col2' [, ...]],
278
     * ]
279
     * ```
280
     *
281
     * @param TableSchema $table the table metadata
282
     * @return array all unique indexes for the given table.
283
     */
284 1
    public function findUniqueIndexes($table)
285
    {
286 1
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName($table->name) . ')';
287 1
        $indexes = $this->db->createCommand($sql)->queryAll();
288 1
        $uniqueIndexes = [];
289
290 1
        foreach ($indexes as $index) {
291 1
            $indexName = $index['name'];
292 1
            $indexInfo = $this->db->createCommand('PRAGMA index_info(' . $this->quoteValue($index['name']) . ')')->queryAll();
293
294 1
            if ($index['unique']) {
295 1
                $uniqueIndexes[$indexName] = [];
296 1
                foreach ($indexInfo as $row) {
297 1
                    $uniqueIndexes[$indexName][] = $row['name'];
298
                }
299
            }
300
        }
301
302 1
        return $uniqueIndexes;
303
    }
304
305
    /**
306
     * Loads the column information into a [[ColumnSchema]] object.
307
     * @param array $info column information
308
     * @return ColumnSchema the column schema object
309
     */
310 311
    protected function loadColumnSchema($info)
311
    {
312 311
        $column = $this->createColumnSchema();
313 311
        $column->name = $info['name'];
314 311
        $column->allowNull = !$info['notnull'];
315 311
        $column->isPrimaryKey = $info['pk'] != 0;
316
317 311
        $column->dbType = strtolower($info['type']);
318 311
        $column->unsigned = strpos($column->dbType, 'unsigned') !== false;
319
320 311
        $column->type = self::TYPE_STRING;
321 311
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
322 311
            $type = strtolower($matches[1]);
323 311
            if (isset($this->typeMap[$type])) {
324 311
                $column->type = $this->typeMap[$type];
325
            }
326
327 311
            if (!empty($matches[2])) {
328 297
                $values = explode(',', $matches[2]);
329 297
                $column->size = $column->precision = (int) $values[0];
330 297
                if (isset($values[1])) {
331 97
                    $column->scale = (int) $values[1];
332
                }
333 297
                if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
334 20
                    $column->type = 'boolean';
335 297
                } elseif ($type === 'bit') {
336
                    if ($column->size > 32) {
337
                        $column->type = 'bigint';
338
                    } elseif ($column->size === 32) {
339
                        $column->type = 'integer';
340
                    }
341
                }
342
            }
343
        }
344 311
        $column->phpType = $this->getColumnPhpType($column);
345
346 311
        if (!$column->isPrimaryKey) {
347 309
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
348 308
                $column->defaultValue = null;
349 140
            } elseif ($column->type === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
350 20
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP');
351
            } else {
352 140
                $value = trim($info['dflt_value'], "'\"");
353 140
                $column->defaultValue = $column->phpTypecast($value);
354
            }
355
        }
356
357 311
        return $column;
358
    }
359
360
    /**
361
     * Sets the isolation level of the current transaction.
362
     * @param string $level The transaction isolation level to use for this transaction.
363
     * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
364
     * @throws NotSupportedException when unsupported isolation levels are used.
365
     * SQLite only supports SERIALIZABLE and READ UNCOMMITTED.
366
     * @see http://www.sqlite.org/pragma.html#pragma_read_uncommitted
367
     */
368 2
    public function setTransactionIsolationLevel($level)
369
    {
370
        switch ($level) {
371 2
            case Transaction::SERIALIZABLE:
372 1
                $this->db->createCommand('PRAGMA read_uncommitted = False;')->execute();
373 1
                break;
374 2
            case Transaction::READ_UNCOMMITTED:
375 2
                $this->db->createCommand('PRAGMA read_uncommitted = True;')->execute();
376 2
                break;
377
            default:
378
                throw new NotSupportedException(get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.');
379
        }
380 2
    }
381
382
    /**
383
     * Returns table columns info.
384
     * @param string $tableName table name
385
     * @return array
386
     */
387 31
    private function loadTableColumnsInfo($tableName)
388
    {
389 31
        $tableColumns = $this->db->createCommand('PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')')->queryAll();
390 31
        $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
391
392 31
        return ArrayHelper::index($tableColumns, 'cid');
393
    }
394
395
    /**
396
     * Loads multiple types of constraints and returns the specified ones.
397
     * @param string $tableName table name.
398
     * @param string $returnType return type:
399
     * - primaryKey
400
     * - indexes
401
     * - uniques
402
     * @return mixed constraints.
403
     */
404 62
    private function loadTableConstraints($tableName, $returnType)
405
    {
406 62
        $indexes = $this->db->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
407 62
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
408 62
        $tableColumns = null;
409 62
        if (!empty($indexes) && !isset($indexes[0]['origin'])) {
410
            /*
411
             * SQLite may not have an "origin" column in INDEX_LIST
412
             * See https://www.sqlite.org/src/info/2743846cdba572f6
413
             */
414
            $tableColumns = $this->loadTableColumnsInfo($tableName);
415
        }
416
        $result = [
417 62
            'primaryKey' => null,
418
            'indexes' => [],
419
            'uniques' => [],
420
        ];
421 62
        foreach ($indexes as $index) {
422 52
            $columns = $this->db->createCommand('PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')')->queryAll();
423 52
            $columns = $this->normalizePdoRowKeyCase($columns, true);
424 52
            ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
425 52
            if ($tableColumns !== null) {
426
                // SQLite may not have an "origin" column in INDEX_LIST
427
                $index['origin'] = 'c';
428
                if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) {
429
                    $index['origin'] = 'pk';
430
                } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) {
431
                    $index['origin'] = 'u';
432
                }
433
            }
434 52
            $result['indexes'][] = new IndexConstraint([
435 52
                'isPrimary' => $index['origin'] === 'pk',
436 52
                'isUnique' => (bool) $index['unique'],
437 52
                'name' => $index['name'],
438 52
                'columnNames' => ArrayHelper::getColumn($columns, 'name'),
439
            ]);
440 52
            if ($index['origin'] === 'u') {
441 44
                $result['uniques'][] = new Constraint([
442 44
                    'name' => $index['name'],
443 44
                    'columnNames' => ArrayHelper::getColumn($columns, 'name'),
444
                ]);
445 32
            } elseif ($index['origin'] === 'pk') {
446 31
                $result['primaryKey'] = new Constraint([
447 52
                    'columnNames' => ArrayHelper::getColumn($columns, 'name'),
448
                ]);
449
            }
450
        }
451
452 62
        if ($result['primaryKey'] === null) {
453
            /*
454
             * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID
455
             * See https://www.sqlite.org/lang_createtable.html#primkeyconst
456
             */
457 31
            if ($tableColumns === null) {
458 31
                $tableColumns = $this->loadTableColumnsInfo($tableName);
459
            }
460 31
            foreach ($tableColumns as $tableColumn) {
461 31
                if ($tableColumn['pk'] > 0) {
462 21
                    $result['primaryKey'] = new Constraint([
463 21
                        'columnNames' => [$tableColumn['name']],
464
                    ]);
465 31
                    break;
466
                }
467
            }
468
        }
469
470 62
        foreach ($result as $type => $data) {
471 62
            $this->setTableMetadata($tableName, $type, $data);
472
        }
473
474 62
        return $result[$returnType];
475
    }
476
477
    /**
478
     * Return whether the specified identifier is a SQLite system identifier.
479
     * @param string $identifier
480
     * @return bool
481
     * @see https://www.sqlite.org/src/artifact/74108007d286232f
482
     */
483
    private function isSystemIdentifier($identifier)
484
    {
485
        return strncmp($identifier, 'sqlite_', 7) === 0;
486
    }
487
}
488