Schema::loadTableConstraints()   F
last analyzed

Complexity

Conditions 16
Paths 364

Size

Total Lines 71
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 16.5

Importance

Changes 0
Metric Value
cc 16
eloc 42
nc 364
nop 2
dl 0
loc 71
ccs 42
cts 48
cp 0.875
crap 16.5
rs 2.6833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\db\sqlite;
10
11
use Yii;
12
use yii\base\NotSupportedException;
13
use yii\db\CheckConstraint;
14
use yii\db\ColumnSchema;
15
use yii\db\Constraint;
16
use yii\db\ConstraintFinderInterface;
17
use yii\db\ConstraintFinderTrait;
18
use yii\db\Expression;
19
use yii\db\ForeignKeyConstraint;
20
use yii\db\IndexConstraint;
21
use yii\db\SqlToken;
22
use yii\db\TableSchema;
23
use yii\db\Transaction;
24
use yii\helpers\ArrayHelper;
25
26
/**
27
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
28
 *
29
 * @property-write string $transactionIsolationLevel The transaction isolation level to use for this
30
 * transaction. This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
31
 *
32
 * @author Qiang Xue <[email protected]>
33
 * @since 2.0
34
 */
35
class Schema extends \yii\db\Schema implements ConstraintFinderInterface
36
{
37
    use ConstraintFinderTrait;
38
39
    /**
40
     * @var array mapping from physical column types (keys) to abstract column types (values)
41
     */
42
    public $typeMap = [
43
        'tinyint' => self::TYPE_TINYINT,
44
        'bit' => self::TYPE_SMALLINT,
45
        'boolean' => self::TYPE_BOOLEAN,
46
        'bool' => self::TYPE_BOOLEAN,
47
        'smallint' => self::TYPE_SMALLINT,
48
        'mediumint' => self::TYPE_INTEGER,
49
        'int' => self::TYPE_INTEGER,
50
        'integer' => self::TYPE_INTEGER,
51
        'bigint' => self::TYPE_BIGINT,
52
        'float' => self::TYPE_FLOAT,
53
        'double' => self::TYPE_DOUBLE,
54
        'real' => self::TYPE_FLOAT,
55
        'decimal' => self::TYPE_DECIMAL,
56
        'numeric' => self::TYPE_DECIMAL,
57
        'tinytext' => self::TYPE_TEXT,
58
        'mediumtext' => self::TYPE_TEXT,
59
        'longtext' => self::TYPE_TEXT,
60
        'text' => self::TYPE_TEXT,
61
        'varchar' => self::TYPE_STRING,
62
        'string' => self::TYPE_STRING,
63
        'char' => self::TYPE_CHAR,
64
        'blob' => self::TYPE_BINARY,
65
        'datetime' => self::TYPE_DATETIME,
66
        'year' => self::TYPE_DATE,
67
        'date' => self::TYPE_DATE,
68
        'time' => self::TYPE_TIME,
69
        'timestamp' => self::TYPE_TIMESTAMP,
70
        'enum' => self::TYPE_STRING,
71
    ];
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    protected $tableQuoteCharacter = '`';
77
    /**
78
     * {@inheritdoc}
79
     */
80
    protected $columnQuoteCharacter = '`';
81
82
83
    /**
84
     * {@inheritdoc}
85
     */
86 6
    protected function findTableNames($schema = '')
87
    {
88 6
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
89 6
        return $this->db->createCommand($sql)->queryColumn();
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95 425
    protected function loadTableSchema($name)
96
    {
97 425
        $table = new TableSchema();
98 425
        $table->name = $name;
99 425
        $table->fullName = $name;
100
101 425
        if ($this->findColumns($table)) {
102 327
            $this->findConstraints($table);
103 327
            return $table;
104
        }
105
106 142
        return null;
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112 40
    protected function loadTablePrimaryKey($tableName)
113
    {
114 40
        return $this->loadTableConstraints($tableName, 'primaryKey');
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 3
    protected function loadTableForeignKeys($tableName)
121
    {
122 3
        $foreignKeys = $this->db->createCommand('PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
123 3
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
124 3
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
125 3
        ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
126 3
        $result = [];
127 3
        foreach ($foreignKeys as $table => $foreignKey) {
128 3
            $result[] = new ForeignKeyConstraint([
129 3
                'columnNames' => ArrayHelper::getColumn($foreignKey, 'from'),
130 3
                'foreignTableName' => $table,
131 3
                'foreignColumnNames' => ArrayHelper::getColumn($foreignKey, 'to'),
132 3
                'onDelete' => isset($foreignKey[0]['on_delete']) ? $foreignKey[0]['on_delete'] : null,
133 3
                'onUpdate' => isset($foreignKey[0]['on_update']) ? $foreignKey[0]['on_update'] : null,
134 3
            ]);
135
        }
136
137 3
        return $result;
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143 10
    protected function loadTableIndexes($tableName)
144
    {
145 10
        return $this->loadTableConstraints($tableName, 'indexes');
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 12
    protected function loadTableUniques($tableName)
152
    {
153 12
        return $this->loadTableConstraints($tableName, 'uniques');
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 12
    protected function loadTableChecks($tableName)
160
    {
161 12
        $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
162 12
            ':tableName' => $tableName,
163 12
        ])->queryScalar();
164
        /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
165 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

165
        $code = (new SqlTokenizer(/** @scrutinizer ignore-type */ $sql))->tokenize();
Loading history...
166 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
167 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...
168
            return [];
169
        }
170
171 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
172 12
        $result = [];
173 12
        $offset = 0;
174 12
        while (true) {
175 12
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
176 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
177 12
                break;
178
            }
179
180 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
181 3
            $name = null;
182 3
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
183 3
            if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
184
                $name = $createTableToken[$firstMatchIndex - 1]->content;
185
            }
186 3
            $result[] = new CheckConstraint([
187 3
                'name' => $name,
188 3
                'expression' => $checkSql,
189 3
            ]);
190
        }
191
192 12
        return $result;
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     * @throws NotSupportedException if this method is called.
198
     */
199 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

199
    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...
200
    {
201 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
202
    }
203
204
    /**
205
     * Creates a query builder for the MySQL database.
206
     * This method may be overridden by child classes to create a DBMS-specific query builder.
207
     * @return QueryBuilder query builder instance
208
     */
209 338
    public function createQueryBuilder()
210
    {
211 338
        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

211
        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...
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     * @return ColumnSchemaBuilder column schema builder instance
217
     */
218 18
    public function createColumnSchemaBuilder($type, $length = null)
219
    {
220 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

220
        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...
221
    }
222
223
    /**
224
     * Collects the table column metadata.
225
     * @param TableSchema $table the table metadata
226
     * @return bool whether the table exists in the database
227
     */
228 425
    protected function findColumns($table)
229
    {
230 425
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->name) . ')';
231 425
        $columns = $this->db->createCommand($sql)->queryAll();
232 425
        if (empty($columns)) {
233 142
            return false;
234
        }
235
236 327
        foreach ($columns as $info) {
237 327
            $column = $this->loadColumnSchema($info);
238 327
            $table->columns[$column->name] = $column;
239 327
            if ($column->isPrimaryKey) {
240 293
                $table->primaryKey[] = $column->name;
241
            }
242
        }
243 327
        if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) {
244 261
            $table->sequenceName = '';
245 261
            $table->columns[$table->primaryKey[0]]->autoIncrement = true;
246
        }
247
248 327
        return true;
249
    }
250
251
    /**
252
     * Collects the foreign key column details for the given table.
253
     * @param TableSchema $table the table metadata
254
     */
255 327
    protected function findConstraints($table)
256
    {
257 327
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->name) . ')';
258 327
        $keys = $this->db->createCommand($sql)->queryAll();
259 327
        foreach ($keys as $key) {
260 6
            $id = (int) $key['id'];
261 6
            if (!isset($table->foreignKeys[$id])) {
262 6
                $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']];
263
            } else {
264
                // composite FK
265 5
                $table->foreignKeys[$id][$key['from']] = $key['to'];
266
            }
267
        }
268
    }
269
270
    /**
271
     * Returns all unique indexes for the given table.
272
     *
273
     * Each array element is of the following structure:
274
     *
275
     * ```php
276
     * [
277
     *     'IndexName1' => ['col1' [, ...]],
278
     *     'IndexName2' => ['col2' [, ...]],
279
     * ]
280
     * ```
281
     *
282
     * @param TableSchema $table the table metadata
283
     * @return array all unique indexes for the given table.
284
     */
285 1
    public function findUniqueIndexes($table)
286
    {
287 1
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName($table->name) . ')';
288 1
        $indexes = $this->db->createCommand($sql)->queryAll();
289 1
        $uniqueIndexes = [];
290
291 1
        foreach ($indexes as $index) {
292 1
            $indexName = $index['name'];
293 1
            $indexInfo = $this->db->createCommand('PRAGMA index_info(' . $this->quoteValue($index['name']) . ')')->queryAll();
294
295 1
            if ($index['unique']) {
296 1
                $uniqueIndexes[$indexName] = [];
297 1
                foreach ($indexInfo as $row) {
298 1
                    $uniqueIndexes[$indexName][] = $row['name'];
299
                }
300
            }
301
        }
302
303 1
        return $uniqueIndexes;
304
    }
305
306
    /**
307
     * Loads the column information into a [[ColumnSchema]] object.
308
     * @param array $info column information
309
     * @return ColumnSchema the column schema object
310
     */
311 327
    protected function loadColumnSchema($info)
312
    {
313 327
        $column = $this->createColumnSchema();
314 327
        $column->name = $info['name'];
315 327
        $column->allowNull = !$info['notnull'];
316 327
        $column->isPrimaryKey = $info['pk'] != 0;
317
318 327
        $column->dbType = strtolower($info['type']);
319 327
        $column->unsigned = strpos($column->dbType, 'unsigned') !== false;
320
321 327
        $column->type = self::TYPE_STRING;
322 327
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
323 327
            $type = strtolower($matches[1]);
324 327
            if (isset($this->typeMap[$type])) {
325 327
                $column->type = $this->typeMap[$type];
326
            }
327
328 327
            if (!empty($matches[2])) {
329 311
                $values = explode(',', $matches[2]);
330 311
                $column->size = $column->precision = (int) $values[0];
331 311
                if (isset($values[1])) {
332 101
                    $column->scale = (int) $values[1];
333
                }
334 311
                if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
335 26
                    $column->type = 'boolean';
336 306
                } elseif ($type === 'bit') {
337
                    if ($column->size > 32) {
338
                        $column->type = 'bigint';
339
                    } elseif ($column->size === 32) {
340
                        $column->type = 'integer';
341
                    }
342
                }
343
            }
344
        }
345 327
        $column->phpType = $this->getColumnPhpType($column);
346
347 327
        if (!$column->isPrimaryKey) {
348 325
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
349 324
                $column->defaultValue = null;
350 146
            } elseif ($column->type === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
351 21
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP');
352
            } else {
353 146
                $value = trim($info['dflt_value'], "'\"");
354 146
                $column->defaultValue = $column->phpTypecast($value);
355
            }
356
        }
357
358 327
        return $column;
359
    }
360
361
    /**
362
     * Sets the isolation level of the current transaction.
363
     * @param string $level The transaction isolation level to use for this transaction.
364
     * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
365
     * @throws NotSupportedException when unsupported isolation levels are used.
366
     * SQLite only supports SERIALIZABLE and READ UNCOMMITTED.
367
     * @see https://www.sqlite.org/pragma.html#pragma_read_uncommitted
368
     */
369 2
    public function setTransactionIsolationLevel($level)
370
    {
371
        switch ($level) {
372 2
            case Transaction::SERIALIZABLE:
373 1
                $this->db->createCommand('PRAGMA read_uncommitted = False;')->execute();
374 1
                break;
375 2
            case Transaction::READ_UNCOMMITTED:
376 2
                $this->db->createCommand('PRAGMA read_uncommitted = True;')->execute();
377 2
                break;
378
            default:
379
                throw new NotSupportedException(get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.');
380
        }
381
    }
382
383
    /**
384
     * Returns table columns info.
385
     * @param string $tableName table name
386
     * @return array
387
     */
388 31
    private function loadTableColumnsInfo($tableName)
389
    {
390 31
        $tableColumns = $this->db->createCommand('PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')')->queryAll();
391 31
        $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
392
393 31
        return ArrayHelper::index($tableColumns, 'cid');
394
    }
395
396
    /**
397
     * Loads multiple types of constraints and returns the specified ones.
398
     * @param string $tableName table name.
399
     * @param string $returnType return type:
400
     * - primaryKey
401
     * - indexes
402
     * - uniques
403
     * @return mixed constraints.
404
     */
405 62
    private function loadTableConstraints($tableName, $returnType)
406
    {
407 62
        $indexes = $this->db->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
408 62
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
409 62
        $tableColumns = null;
410 62
        if (!empty($indexes) && !isset($indexes[0]['origin'])) {
411
            /*
412
             * SQLite may not have an "origin" column in INDEX_LIST
413
             * See https://www.sqlite.org/src/info/2743846cdba572f6
414
             */
415
            $tableColumns = $this->loadTableColumnsInfo($tableName);
416
        }
417 62
        $result = [
418 62
            'primaryKey' => null,
419 62
            'indexes' => [],
420 62
            'uniques' => [],
421 62
        ];
422 62
        foreach ($indexes as $index) {
423 52
            $columns = $this->db->createCommand('PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')')->queryAll();
424 52
            $columns = $this->normalizePdoRowKeyCase($columns, true);
425 52
            ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
426 52
            if ($tableColumns !== null) {
427
                // SQLite may not have an "origin" column in INDEX_LIST
428
                $index['origin'] = 'c';
429
                if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) {
430
                    $index['origin'] = 'pk';
431
                } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) {
432
                    $index['origin'] = 'u';
433
                }
434
            }
435 52
            $result['indexes'][] = new IndexConstraint([
436 52
                'isPrimary' => $index['origin'] === 'pk',
437 52
                'isUnique' => (bool) $index['unique'],
438 52
                'name' => $index['name'],
439 52
                'columnNames' => ArrayHelper::getColumn($columns, 'name'),
440 52
            ]);
441 52
            if ($index['origin'] === 'u') {
442 44
                $result['uniques'][] = new Constraint([
443 44
                    'name' => $index['name'],
444 44
                    'columnNames' => ArrayHelper::getColumn($columns, 'name'),
445 44
                ]);
446 32
            } elseif ($index['origin'] === 'pk') {
447 31
                $result['primaryKey'] = new Constraint([
448 31
                    'columnNames' => ArrayHelper::getColumn($columns, 'name'),
449 31
                ]);
450
            }
451
        }
452
453 62
        if ($result['primaryKey'] === null) {
454
            /*
455
             * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID
456
             * See https://www.sqlite.org/lang_createtable.html#primkeyconst
457
             */
458 31
            if ($tableColumns === null) {
459 31
                $tableColumns = $this->loadTableColumnsInfo($tableName);
460
            }
461 31
            foreach ($tableColumns as $tableColumn) {
462 31
                if ($tableColumn['pk'] > 0) {
463 21
                    $result['primaryKey'] = new Constraint([
464 21
                        'columnNames' => [$tableColumn['name']],
465 21
                    ]);
466 21
                    break;
467
                }
468
            }
469
        }
470
471 62
        foreach ($result as $type => $data) {
472 62
            $this->setTableMetadata($tableName, $type, $data);
473
        }
474
475 62
        return $result[$returnType];
476
    }
477
478
    /**
479
     * Return whether the specified identifier is a SQLite system identifier.
480
     * @param string $identifier
481
     * @return bool
482
     * @see https://www.sqlite.org/src/artifact/74108007d286232f
483
     */
484
    private function isSystemIdentifier($identifier)
485
    {
486
        return strncmp($identifier, 'sqlite_', 7) === 0;
487
    }
488
}
489