GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Schema   F
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 452
Duplicated Lines 0 %

Test Coverage

Coverage 92.19%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 204
dl 0
loc 452
ccs 177
cts 192
cp 0.9219
rs 2.8
c 1
b 0
f 0
wmc 70

18 Methods

Rating   Name   Duplication   Size   Complexity  
A findColumns() 0 21 6
C loadColumnSchema() 0 48 17
A findConstraints() 0 11 3
A loadTableSchema() 0 12 2
A createQueryBuilder() 0 3 1
A loadTablePrimaryKey() 0 3 1
A findUniqueIndexes() 0 19 4
A loadTableUniques() 0 3 1
A createColumnSchemaBuilder() 0 3 1
A findTableNames() 0 4 1
A loadTableIndexes() 0 3 1
A loadTableDefaultValues() 0 3 1
A isSystemIdentifier() 0 3 1
A setTransactionIsolationLevel() 0 11 3
A loadTableColumnsInfo() 0 6 1
B loadTableChecks() 0 34 6
F loadTableConstraints() 0 71 16
A loadTableForeignKeys() 0 18 4

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

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;
11
use yii\base\NotSupportedException;
12
use yii\db\CheckConstraint;
13
use yii\db\ColumnSchema;
14
use yii\db\Constraint;
15
use yii\db\ConstraintFinderInterface;
16
use yii\db\ConstraintFinderTrait;
17
use yii\db\Expression;
18
use yii\db\ForeignKeyConstraint;
19
use yii\db\IndexConstraint;
20
use yii\db\SqlToken;
21
use yii\db\TableSchema;
22
use yii\db\Transaction;
23
use yii\helpers\ArrayHelper;
24
25
/**
26
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
27
 *
28
 * @property-write string $transactionIsolationLevel The transaction isolation level to use for this
29
 * transaction. This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
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 425
    protected function loadTableSchema($name)
95
    {
96 425
        $table = new TableSchema();
97 425
        $table->name = $name;
98 425
        $table->fullName = $name;
99
100 425
        if ($this->findColumns($table)) {
101 327
            $this->findConstraints($table);
102 327
            return $table;
103
        }
104
105 142
        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 3
            ]);
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();
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type false; however, parameter $sql of yii\db\sqlite\SqlTokenizer::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

164
        $code = (new SqlTokenizer(/** @scrutinizer ignore-type */ $sql))->tokenize();
Loading history...
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 3
            ]);
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)
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

198
    protected function loadTableDefaultValues(/** @scrutinizer ignore-unused */ $tableName)

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

Loading history...
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 339
    public function createQueryBuilder()
209
    {
210 339
        return Yii::createObject(QueryBuilder::className(), [$this->db]);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

210
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ QueryBuilder::className(), [$this->db]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
211
    }
212
213
    /**
214
     * {@inheritdoc}
215
     * @return ColumnSchemaBuilder column schema builder instance
216
     */
217 18
    public function createColumnSchemaBuilder($type, $length = null)
218
    {
219 18
        return Yii::createObject(ColumnSchemaBuilder::className(), [$type, $length]);
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

219
        return Yii::createObject(/** @scrutinizer ignore-deprecated */ ColumnSchemaBuilder::className(), [$type, $length]);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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 425
    protected function findColumns($table)
228
    {
229 425
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->name) . ')';
230 425
        $columns = $this->db->createCommand($sql)->queryAll();
231 425
        if (empty($columns)) {
232 142
            return false;
233
        }
234
235 327
        foreach ($columns as $info) {
236 327
            $column = $this->loadColumnSchema($info);
237 327
            $table->columns[$column->name] = $column;
238 327
            if ($column->isPrimaryKey) {
239 293
                $table->primaryKey[] = $column->name;
240
            }
241
        }
242 327
        if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) {
243 261
            $table->sequenceName = '';
244 261
            $table->columns[$table->primaryKey[0]]->autoIncrement = true;
245
        }
246
247 327
        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 327
    protected function findConstraints($table)
255
    {
256 327
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->name) . ')';
257 327
        $keys = $this->db->createCommand($sql)->queryAll();
258 327
        foreach ($keys as $key) {
259 6
            $id = (int) $key['id'];
260 6
            if (!isset($table->foreignKeys[$id])) {
261 6
                $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']];
262
            } else {
263
                // composite FK
264 5
                $table->foreignKeys[$id][$key['from']] = $key['to'];
265
            }
266
        }
267
    }
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 327
    protected function loadColumnSchema($info)
311
    {
312 327
        $column = $this->createColumnSchema();
313 327
        $column->name = $info['name'];
314 327
        $column->allowNull = !$info['notnull'];
315 327
        $column->isPrimaryKey = $info['pk'] != 0;
316
317 327
        $column->dbType = strtolower($info['type']);
318 327
        $column->unsigned = strpos($column->dbType, 'unsigned') !== false;
319
320 327
        $column->type = self::TYPE_STRING;
321 327
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
322 327
            $type = strtolower($matches[1]);
323 327
            if (isset($this->typeMap[$type])) {
324 327
                $column->type = $this->typeMap[$type];
325
            }
326
327 327
            if (!empty($matches[2])) {
328 311
                $values = explode(',', $matches[2]);
329 311
                $column->size = $column->precision = (int) $values[0];
330 311
                if (isset($values[1])) {
331 101
                    $column->scale = (int) $values[1];
332
                }
333 311
                if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
334 26
                    $column->type = 'boolean';
335 306
                } 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 327
        $column->phpType = $this->getColumnPhpType($column);
345
346 327
        if (!$column->isPrimaryKey) {
347 325
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
348 324
                $column->defaultValue = null;
349 146
            } elseif ($column->type === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
350 21
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP');
351
            } else {
352 146
                $value = trim($info['dflt_value'], "'\"");
353 146
                $column->defaultValue = $column->phpTypecast($value);
354
            }
355
        }
356
357 327
        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 https://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
    }
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 62
        $result = [
417 62
            'primaryKey' => null,
418 62
            'indexes' => [],
419 62
            'uniques' => [],
420 62
        ];
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 52
            ]);
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 44
                ]);
445 32
            } elseif ($index['origin'] === 'pk') {
446 31
                $result['primaryKey'] = new Constraint([
447 31
                    'columnNames' => ArrayHelper::getColumn($columns, 'name'),
448 31
                ]);
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 21
                    ]);
465 21
                    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