Passed
Push — master ( 1265f7...db8617 )
by Alexander
01:41
created

Schema::loadTableConstraints()   F

Complexity

Conditions 16
Paths 364

Size

Total Lines 90
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 16.5676

Importance

Changes 0
Metric Value
cc 16
eloc 48
c 0
b 0
f 0
nc 364
nop 2
dl 0
loc 90
ccs 40
cts 46
cp 0.8696
crap 16.5676
rs 2.6833

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
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite\Schema;
6
7
use Yiisoft\Arrays\ArrayHelper;
8
use Yiisoft\Db\Constraints\CheckConstraint;
9
use Yiisoft\Db\Constraints\Constraint;
10
use Yiisoft\Db\Constraints\ConstraintFinderInterface;
11
use Yiisoft\Db\Constraints\ConstraintFinderTrait;
12
use Yiisoft\Db\Constraints\ForeignKeyConstraint;
13
use Yiisoft\Db\Constraints\IndexConstraint;
14
use Yiisoft\Db\Exceptions\NotSupportedException;
15
use Yiisoft\Db\Expressions\Expression;
16
use Yiisoft\Db\Schemas\Schema as AbstractSchema;
17
use Yiisoft\Db\Schemas\ColumnSchema;
18
use Yiisoft\Db\Schemas\TableSchema;
19
use Yiisoft\Db\Sqlite\Query\QueryBuilder;
20
use Yiisoft\Db\Sqlite\Token\SqlToken;
21
use Yiisoft\Db\Sqlite\Token\SqlTokenizer;
22
use Yiisoft\Db\Transactions\Transaction;
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 {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
29
 */
30
class Schema extends AbstractSchema implements ConstraintFinderInterface
31
{
32
    use ConstraintFinderTrait;
33
34
    /**
35
     * @var array mapping from physical column types (keys) to abstract column types (values)
36
     */
37
    public array $typeMap = [
38
        'tinyint' => self::TYPE_TINYINT,
39
        'bit' => self::TYPE_SMALLINT,
40
        'boolean' => self::TYPE_BOOLEAN,
41
        'bool' => self::TYPE_BOOLEAN,
42
        'smallint' => self::TYPE_SMALLINT,
43
        'mediumint' => self::TYPE_INTEGER,
44
        'int' => self::TYPE_INTEGER,
45
        'integer' => self::TYPE_INTEGER,
46
        'bigint' => self::TYPE_BIGINT,
47
        'float' => self::TYPE_FLOAT,
48
        'double' => self::TYPE_DOUBLE,
49
        'real' => self::TYPE_FLOAT,
50
        'decimal' => self::TYPE_DECIMAL,
51
        'numeric' => self::TYPE_DECIMAL,
52
        'tinytext' => self::TYPE_TEXT,
53
        'mediumtext' => self::TYPE_TEXT,
54
        'longtext' => self::TYPE_TEXT,
55
        'text' => self::TYPE_TEXT,
56
        'varchar' => self::TYPE_STRING,
57
        'string' => self::TYPE_STRING,
58
        'char' => self::TYPE_CHAR,
59
        'blob' => self::TYPE_BINARY,
60
        'datetime' => self::TYPE_DATETIME,
61
        'year' => self::TYPE_DATE,
62
        'date' => self::TYPE_DATE,
63
        'time' => self::TYPE_TIME,
64
        'timestamp' => self::TYPE_TIMESTAMP,
65
        'enum' => self::TYPE_STRING,
66
    ];
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    protected string $tableQuoteCharacter = '`';
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    protected string $columnQuoteCharacter = '`';
77
78
    /**
79
     * {@inheritdoc}
80
     */
81 5
    protected function findTableNames($schema = '')
82
    {
83 5
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
84
85 5
        return $this->db->createCommand($sql)->queryColumn();
0 ignored issues
show
Bug introduced by
The method createCommand() does not exist on null. ( Ignorable by Annotation )

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

85
        return $this->db->/** @scrutinizer ignore-call */ createCommand($sql)->queryColumn();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91 77
    protected function loadTableSchema($name)
92
    {
93 77
        $table = new TableSchema();
94
95 77
        $table->name = $name;
96 77
        $table->fullName = $name;
97
98 77
        if ($this->findColumns($table)) {
99 75
            $this->findConstraints($table);
100
101 75
            return $table;
102
        }
103
104 10
        return null;
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 30
    protected function loadTablePrimaryKey($tableName)
111
    {
112 30
        return $this->loadTableConstraints($tableName, 'primaryKey');
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118 3
    protected function loadTableForeignKeys($tableName)
119
    {
120 3
        $foreignKeys = $this->db->createCommand(
121 3
            'PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')'
122 3
        )->queryAll();
123
124 3
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
125
126 3
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
127
128 3
        ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
129
130 3
        $result = [];
131
132 3
        foreach ($foreignKeys as $table => $foreignKey) {
133 3
            $fk = new ForeignKeyConstraint();
134
135 3
            $fk->setColumnNames(ArrayHelper::getColumn($foreignKey, 'from'));
136 3
            $fk->setForeignTableName($table);
137 3
            $fk->setForeignColumnNames(ArrayHelper::getColumn($foreignKey, 'to'));
138 3
            $fk->setOnDelete($foreignKey[0]['on_delete'] ?? null);
139 3
            $fk->setOnUpdate($foreignKey[0]['on_update'] ?? null);
140
141 3
            $result[] = $fk;
142
        }
143
144 3
        return $result;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 10
    protected function loadTableIndexes($tableName)
151
    {
152 10
        return $this->loadTableConstraints($tableName, 'indexes');
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 12
    protected function loadTableUniques($tableName)
159
    {
160 12
        return $this->loadTableConstraints($tableName, 'uniques');
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166 12
    protected function loadTableChecks($tableName)
167
    {
168 12
        $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
169 12
            ':tableName' => $tableName,
170 12
        ])->queryScalar();
171
172
        /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
173 12
        $code = (new SqlTokenizer($sql))->tokenize();
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type false and null; however, parameter $sql of Yiisoft\Db\Sqlite\Token\...okenizer::__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

173
        $code = (new SqlTokenizer(/** @scrutinizer ignore-type */ $sql))->tokenize();
Loading history...
174 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
175
176 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...
177
            return [];
178
        }
179
180 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
181 12
        $result = [];
182 12
        $offset = 0;
183
184 12
        while (true) {
185 12
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
186
187 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
188 12
                break;
189
            }
190
191 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
192 3
            $name = null;
193 3
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
194
195 3
            if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
196
                $name = $createTableToken[$firstMatchIndex - 1]->content;
197
            }
198
199 3
            $ck = new CheckConstraint();
200 3
            $ck->setName($name);
201 3
            $ck->setExpression($checkSql);
202
203 3
            $result[] = $ck;
204
        }
205
206 12
        return $result;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     *
212
     * @throws NotSupportedException if this method is called.
213
     */
214 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

214
    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...
215
    {
216 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
217
    }
218
219
    /**
220
     * Creates a query builder for the MySQL database.
221
     *
222
     * This method may be overridden by child classes to create a DBMS-specific query builder.
223
     *
224
     * @return QueryBuilder query builder instance
225
     */
226 50
    public function createQueryBuilder()
227
    {
228 50
        return new QueryBuilder($this->db);
0 ignored issues
show
Bug introduced by
It seems like $this->db can also be of type null; however, parameter $db of Yiisoft\Db\Sqlite\Query\...yBuilder::__construct() does only seem to accept Yiisoft\Db\Drivers\Connection, 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

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