Completed
Push — master ( 6d2e0a...92d224 )
by Alexander
12:32
created

Schema::isSystemIdentifier()   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
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
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\NotSupportedException;
11
use yii\db\CheckConstraint;
12
use yii\db\ColumnSchema;
13
use yii\db\Constraint;
14
use yii\db\ConstraintFinderTrait;
15
use yii\db\Expression;
16
use yii\db\ForeignKeyConstraint;
17
use yii\db\IndexConstraint;
18
use yii\db\SqlToken;
19
use yii\db\TableSchema;
20
use yii\db\Transaction;
21
use yii\helpers\ArrayHelper;
22
23
/**
24
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
25
 *
26
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction.
27
 * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
28
 *
29
 * @author Qiang Xue <[email protected]>
30
 * @since 2.0
31
 */
32
class Schema extends \yii\db\Schema
33
{
34
    use ConstraintFinderTrait;
35
36
    /**
37
     * @var array mapping from physical column types (keys) to abstract column types (values)
38
     */
39
    public $typeMap = [
40
        'tinyint' => self::TYPE_SMALLINT,
41
        'bit' => self::TYPE_SMALLINT,
42
        'boolean' => self::TYPE_BOOLEAN,
43
        'bool' => self::TYPE_BOOLEAN,
44
        'smallint' => self::TYPE_SMALLINT,
45
        'mediumint' => self::TYPE_INTEGER,
46
        'int' => self::TYPE_INTEGER,
47
        'integer' => self::TYPE_INTEGER,
48
        'bigint' => self::TYPE_BIGINT,
49
        'float' => self::TYPE_FLOAT,
50
        'double' => self::TYPE_DOUBLE,
51
        'real' => self::TYPE_FLOAT,
52
        'decimal' => self::TYPE_DECIMAL,
53
        'numeric' => self::TYPE_DECIMAL,
54
        'tinytext' => self::TYPE_TEXT,
55
        'mediumtext' => self::TYPE_TEXT,
56
        'longtext' => self::TYPE_TEXT,
57
        'text' => self::TYPE_TEXT,
58
        'varchar' => self::TYPE_STRING,
59
        'string' => self::TYPE_STRING,
60
        'char' => self::TYPE_CHAR,
61
        'blob' => self::TYPE_BINARY,
62
        'datetime' => self::TYPE_DATETIME,
63
        'year' => self::TYPE_DATE,
64
        'date' => self::TYPE_DATE,
65
        'time' => self::TYPE_TIME,
66
        'timestamp' => self::TYPE_TIMESTAMP,
67
        'enum' => self::TYPE_STRING,
68
    ];
69
70
    /**
71
     * @inheritDoc
72
     */
73 5
    protected function findTableNames($schema = '')
74
    {
75 5
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
76 5
        return $this->db->createCommand($sql)->queryColumn();
77
    }
78
79
    /**
80
     * @inheritDoc
81
     */
82 209
    protected function loadTableSchema($name)
83
    {
84 209
        $table = new TableSchema();
85 209
        $table->name = $name;
86 209
        $table->fullName = $name;
87
88 209
        if ($this->findColumns($table)) {
89 198
            $this->findConstraints($table);
90 198
            return $table;
91
        }
92
93 36
        return null;
94
    }
95
96
    /**
97
     * @inheritDoc
98
     */
99 12
    protected function loadTablePrimaryKey($tableName)
100
    {
101 12
        return $this->loadTableConstraints($tableName, 'primaryKey');
102
    }
103
104
    /**
105
     * @inheritDoc
106
     */
107 3
    protected function loadTableForeignKeys($tableName)
108
    {
109 3
        $foreignKeys = $this->db->createCommand('PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
110 3
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
111 3
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
112 3
        ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
113 3
        $result = [];
114 3
        foreach ($foreignKeys as $table => $foreignKey) {
115 3
            $result[] = new ForeignKeyConstraint([
116 3
                'columnNames' => ArrayHelper::getColumn($foreignKey, 'from'),
117 3
                'foreignTableName' => $table,
118 3
                'foreignColumnNames' => ArrayHelper::getColumn($foreignKey, 'to'),
119 3
                'onDelete' => isset($foreignKey[0]['on_delete']) ? $foreignKey[0]['on_delete'] : null,
120 3
                'onUpdate' => isset($foreignKey[0]['on_update']) ? $foreignKey[0]['on_update'] : null,
121
            ]);
122
        }
123 3
        return $result;
124
    }
125
126
    /**
127
     * @inheritDoc
128
     */
129 10
    protected function loadTableIndexes($tableName)
130
    {
131 10
        return $this->loadTableConstraints($tableName, 'indexes');
132
    }
133
134
    /**
135
     * @inheritDoc
136
     */
137 12
    protected function loadTableUniques($tableName)
138
    {
139 12
        return $this->loadTableConstraints($tableName, 'uniques');
140
    }
141
142
    /**
143
     * @inheritDoc
144
     */
145 12
    protected function loadTableChecks($tableName)
146
    {
147 12
        $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
148 12
            ':tableName' => $tableName,
149 12
        ])->queryScalar();
150
        /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
151 12
        $code = (new SqlTokenizer($sql))->tokenize();
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->db->createCommand...leName))->queryScalar() on line 147 can also be of type false or null; however, yii\db\SqlTokenizer::__construct() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
152 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
153 12
        if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
0 ignored issues
show
Bug introduced by
The variable $firstMatchIndex 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 $lastMatchIndex 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...
154
            return [];
155
        }
156
157 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
158 12
        $result = [];
159 12
        $offset = 0;
160 12
        while (true) {
161 12
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
162 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
163 12
                break;
164
            }
165
166 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
167 3
            $name = null;
168 3
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
169 3
            if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
170
                $name = $createTableToken[$firstMatchIndex - 1]->content;
171
            }
172 3
            $result[] = new CheckConstraint([
173 3
                'name' => $name,
174 3
                'expression' => $checkSql,
175
            ]);
176
        }
177 12
        return $result;
178
    }
179
180
    /**
181
     * @inheritDoc
182
     * @throws NotSupportedException if this method is called.
183
     */
184 12
    protected function loadTableDefaultValues($tableName)
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
185
    {
186 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
187
    }
188
189
    /**
190
     * Quotes a table name for use in a query.
191
     * A simple table name has no schema prefix.
192
     * @param string $name table name
193
     * @return string the properly quoted table name
194
     */
195 303
    public function quoteSimpleTableName($name)
196
    {
197 303
        return strpos($name, '`') !== false ? $name : "`$name`";
198
    }
199
200
    /**
201
     * Quotes a column name for use in a query.
202
     * A simple column name has no prefix.
203
     * @param string $name column name
204
     * @return string the properly quoted column name
205
     */
206 300
    public function quoteSimpleColumnName($name)
207
    {
208 300
        return strpos($name, '`') !== false || $name === '*' ? $name : "`$name`";
209
    }
210
211
    /**
212
     * Creates a query builder for the MySQL database.
213
     * This method may be overridden by child classes to create a DBMS-specific query builder.
214
     * @return QueryBuilder query builder instance
215
     */
216 216
    public function createQueryBuilder()
217
    {
218 216
        return new QueryBuilder($this->db);
219
    }
220
221
    /**
222
     * @inheritdoc
223
     * @return ColumnSchemaBuilder column schema builder instance
224
     */
225 4
    public function createColumnSchemaBuilder($type, $length = null)
226
    {
227 4
        return new ColumnSchemaBuilder($type, $length);
228
    }
229
230
    /**
231
     * Collects the table column metadata.
232
     * @param TableSchema $table the table metadata
233
     * @return bool whether the table exists in the database
234
     */
235 209
    protected function findColumns($table)
236
    {
237 209
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->name) . ')';
238 209
        $columns = $this->db->createCommand($sql)->queryAll();
239 209
        if (empty($columns)) {
240 36
            return false;
241
        }
242
243 198
        foreach ($columns as $info) {
244 198
            $column = $this->loadColumnSchema($info);
245 198
            $table->columns[$column->name] = $column;
246 198
            if ($column->isPrimaryKey) {
247 198
                $table->primaryKey[] = $column->name;
248
            }
249
        }
250 198
        if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) {
251 156
            $table->sequenceName = '';
252 156
            $table->columns[$table->primaryKey[0]]->autoIncrement = true;
253
        }
254
255 198
        return true;
256
    }
257
258
    /**
259
     * Collects the foreign key column details for the given table.
260
     * @param TableSchema $table the table metadata
261
     */
262 198
    protected function findConstraints($table)
263
    {
264 198
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->name) . ')';
265 198
        $keys = $this->db->createCommand($sql)->queryAll();
266 198
        foreach ($keys as $key) {
267 8
            $id = (int) $key['id'];
268 8
            if (!isset($table->foreignKeys[$id])) {
269 8
                $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']];
270
            } else {
271
                // composite FK
272 8
                $table->foreignKeys[$id][$key['from']] = $key['to'];
273
            }
274
        }
275 198
    }
276
277
    /**
278
     * Returns all unique indexes for the given table.
279
     * Each array element is of the following structure:
280
     *
281
     * ```php
282
     * [
283
     *     'IndexName1' => ['col1' [, ...]],
284
     *     'IndexName2' => ['col2' [, ...]],
285
     * ]
286
     * ```
287
     *
288
     * @param TableSchema $table the table metadata
289
     * @return array all unique indexes for the given table.
290
     */
291 1
    public function findUniqueIndexes($table)
292
    {
293 1
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName($table->name) . ')';
294 1
        $indexes = $this->db->createCommand($sql)->queryAll();
295 1
        $uniqueIndexes = [];
296
297 1
        foreach ($indexes as $index) {
298 1
            $indexName = $index['name'];
299 1
            $indexInfo = $this->db->createCommand('PRAGMA index_info(' . $this->quoteValue($index['name']) . ')')->queryAll();
300
301 1
            if ($index['unique']) {
302 1
                $uniqueIndexes[$indexName] = [];
303 1
                foreach ($indexInfo as $row) {
304 1
                    $uniqueIndexes[$indexName][] = $row['name'];
305
                }
306
            }
307
        }
308
309 1
        return $uniqueIndexes;
310
    }
311
312
    /**
313
     * Loads the column information into a [[ColumnSchema]] object.
314
     * @param array $info column information
315
     * @return ColumnSchema the column schema object
316
     */
317 198
    protected function loadColumnSchema($info)
318
    {
319 198
        $column = $this->createColumnSchema();
320 198
        $column->name = $info['name'];
321 198
        $column->allowNull = !$info['notnull'];
322 198
        $column->isPrimaryKey = $info['pk'] != 0;
323
324 198
        $column->dbType = strtolower($info['type']);
325 198
        $column->unsigned = strpos($column->dbType, 'unsigned') !== false;
326
327 198
        $column->type = self::TYPE_STRING;
328 198
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
329 198
            $type = strtolower($matches[1]);
330 198
            if (isset($this->typeMap[$type])) {
331 198
                $column->type = $this->typeMap[$type];
332
            }
333
334 198
            if (!empty($matches[2])) {
335 190
                $values = explode(',', $matches[2]);
336 190
                $column->size = $column->precision = (int) $values[0];
337 190
                if (isset($values[1])) {
338 67
                    $column->scale = (int) $values[1];
339
                }
340 190
                if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
341 10
                    $column->type = 'boolean';
342 190
                } elseif ($type === 'bit') {
343
                    if ($column->size > 32) {
344
                        $column->type = 'bigint';
345
                    } elseif ($column->size === 32) {
346
                        $column->type = 'integer';
347
                    }
348
                }
349
            }
350
        }
351 198
        $column->phpType = $this->getColumnPhpType($column);
352
353 198
        if (!$column->isPrimaryKey) {
354 196
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
355 195
                $column->defaultValue = null;
356 77
            } elseif ($column->type === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
357 10
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP');
358
            } else {
359 77
                $value = trim($info['dflt_value'], "'\"");
360 77
                $column->defaultValue = $column->phpTypecast($value);
361
            }
362
        }
363
364 198
        return $column;
365
    }
366
367
    /**
368
     * Sets the isolation level of the current transaction.
369
     * @param string $level The transaction isolation level to use for this transaction.
370
     * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
371
     * @throws NotSupportedException when unsupported isolation levels are used.
372
     * SQLite only supports SERIALIZABLE and READ UNCOMMITTED.
373
     * @see http://www.sqlite.org/pragma.html#pragma_read_uncommitted
374
     */
375 2
    public function setTransactionIsolationLevel($level)
376
    {
377
        switch ($level) {
378 2
            case Transaction::SERIALIZABLE:
379 1
                $this->db->createCommand('PRAGMA read_uncommitted = False;')->execute();
380 1
                break;
381 2
            case Transaction::READ_UNCOMMITTED:
382 2
                $this->db->createCommand('PRAGMA read_uncommitted = True;')->execute();
383 2
                break;
384
            default:
385
                throw new NotSupportedException(get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.');
386
        }
387 2
    }
388
389
    /**
390
     * Loads multiple types of constraints and returns the specified ones.
391
     * @param string $tableName table name.
392
     * @param string $returnType return type:
393
     * - primaryKey
394
     * - indexes
395
     * - uniques
396
     * @return mixed constraints.
397
     */
398 34
    private function loadTableConstraints($tableName, $returnType)
399
    {
400 34
        $indexes = $this->db->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
401 34
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
402 34
        $tableColumns = null;
403 34
        if (!empty($indexes) && !isset($indexes[0]['origin'])) {
404
            /*
405
             * SQLite may not have an "origin" column in INDEX_LIST
406
             * See https://www.sqlite.org/src/info/2743846cdba572f6
407
             */
408
            $tableColumns = $this->db->createCommand('PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')')->queryAll();
409
            $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
410
            $tableColumns = ArrayHelper::index($tableColumns, 'cid');
411
        }
412
        $result = [
413 34
            'primaryKey' => null,
414
            'indexes' => [],
415
            'uniques' => [],
416
        ];
417 34
        foreach ($indexes as $index) {
418 25
            $columns = $this->db->createCommand('PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')')->queryAll();
419 25
            $columns = $this->normalizePdoRowKeyCase($columns, true);
420 25
            ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
421 25
            if ($tableColumns !== null) {
422
                // SQLite may not have an "origin" column in INDEX_LIST
423
                $index['origin'] = 'c';
424
                if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) {
425
                    $index['origin'] = 'pk';
426
                } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) {
427
                    $index['origin'] = 'u';
428
                }
429
            }
430 25
            $result['indexes'][] = new IndexConstraint([
431 25
                'isPrimary' => $index['origin'] === 'pk',
432 25
                'isUnique' => (bool) $index['unique'],
433 25
                'name' => $index['name'],
434 25
                'columnNames' => ArrayHelper::getColumn($columns, 'name'),
435
            ]);
436 25
            if ($index['origin'] === 'u') {
437 24
                $result['uniques'][] = new Constraint([
438 24
                    'name' => $index['name'],
439 24
                    'columnNames' => ArrayHelper::getColumn($columns, 'name'),
440
                ]);
441 25
            } elseif ($index['origin'] === 'pk') {
442 24
                $result['primaryKey'] = new Constraint([
443 25
                    'columnNames' => ArrayHelper::getColumn($columns, 'name'),
444
                ]);
445
            }
446
        }
447 34
        foreach ($result as $type => $data) {
448 34
            $this->setTableMetadata($tableName, $type, $data);
449
        }
450 34
        return $result[$returnType];
451
    }
452
453
    /**
454
     * Return whether the specified identifier is a SQLite system identifier.
455
     * @param string $identifier
456
     * @return bool
457
     * @see https://www.sqlite.org/src/artifact/74108007d286232f
458
     */
459
    private function isSystemIdentifier($identifier)
460
    {
461
        return strpos($identifier, 'sqlite_') === 0;
462
    }
463
}
464