Completed
Push — master ( ec4509...390789 )
by Alexander
43:01 queued 39:57
created

Schema::loadTableConstraints()   F

Complexity

Conditions 16
Paths 572

Size

Total Lines 72

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 16.864

Importance

Changes 0
Metric Value
dl 0
loc 72
ccs 34
cts 40
cp 0.85
rs 1.9944
c 0
b 0
f 0
cc 16
nc 572
nop 2
crap 16.864

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