Passed
Push — master ( 38aa1a...d42673 )
by Wilmer
07:33 queued 06:10
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\Constraint\CheckConstraint;
9
use Yiisoft\Db\Constraint\Constraint;
10
use Yiisoft\Db\Constraint\ConstraintFinderInterface;
11
use Yiisoft\Db\Constraint\ConstraintFinderTrait;
12
use Yiisoft\Db\Constraint\ForeignKeyConstraint;
13
use Yiisoft\Db\Constraint\IndexConstraint;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
use Yiisoft\Db\Expression\Expression;
16
use Yiisoft\Db\Schema\Schema as AbstractSchema;
17
use Yiisoft\Db\Schema\ColumnSchema;
18
use Yiisoft\Db\Schema\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\Transaction\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(string $name): ?TableSchema
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\Connection\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->getName()] = $column;
260 75
            if ($column->getIsPrimaryKey()) {
261 52
                $table->primaryKey[] = $column->getName();
262
            }
263
        }
264
265 75
        if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->getDbType(), '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->getDbType(), 'unsigned') !== false);
348 75
        $column->type(self::TYPE_STRING);
349
350 75
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $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->precision((int) $values[0]);
360 72
                $column->size((int) $values[0]);
361 72
                if (isset($values[1])) {
362 25
                    $column->scale((int) $values[1]);
363
                }
364 72
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
365 21
                    $column->type('boolean');
366 72
                } elseif ($type === 'bit') {
367
                    if ($column->getSize() > 32) {
368
                        $column->type('bigint');
369
                    } elseif ($column->getSize() === 32) {
370
                        $column->type('integer');
371
                    }
372
                }
373
            }
374
        }
375
376 75
        $column->phpType($this->getColumnPhpType($column));
377
378 75
        if (!$column->getIsPrimaryKey()) {
379 74
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
380 73
                $column->defaultValue(null);
381 60
            } elseif ($column->getType() === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
382 21
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP'));
383
            } else {
384 60
                $value = trim($info['dflt_value'], "'\"");
385 60
                $column->defaultValue($column->phpTypecast($value));
386
            }
387
        }
388
389 75
        return $column;
390
    }
391
392
    /**
393
     * Sets the isolation level of the current transaction.
394
     *
395
     * @param string $level The transaction isolation level to use for this transaction.
396
     * This can be either {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
397
     *
398
     * @throws NotSupportedException when unsupported isolation levels are used.
399
     * SQLite only supports SERIALIZABLE and READ UNCOMMITTED.
400
     *
401
     * {@see http://www.sqlite.org/pragma.html#pragma_read_uncommitted}
402
     */
403 3
    public function setTransactionIsolationLevel($level): void
404
    {
405
        switch ($level) {
406 3
            case Transaction::SERIALIZABLE:
407 1
                $this->db->createCommand('PRAGMA read_uncommitted = False;')->execute();
408 1
                break;
409 3
            case Transaction::READ_UNCOMMITTED:
410 3
                $this->db->createCommand('PRAGMA read_uncommitted = True;')->execute();
411 3
                break;
412
            default:
413
                throw new NotSupportedException(
414
                    get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'
415
                );
416
        }
417 3
    }
418
419
    /**
420
     * Returns table columns info.
421
     *
422
     * @param string $tableName table name
423
     *
424
     * @return array
425
     */
426 28
    private function loadTableColumnsInfo($tableName): array
427
    {
428 28
        $tableColumns = $this->db->createCommand(
429 28
            'PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')'
430 28
        )->queryAll();
431
432 28
        $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
433
434 28
        return ArrayHelper::index($tableColumns, 'cid');
435
    }
436
437
    /**
438
     * Loads multiple types of constraints and returns the specified ones.
439
     *
440
     * @param string $tableName table name.
441
     * @param string $returnType return type:
442
     * - primaryKey
443
     * - indexes
444
     * - uniques
445
     *
446
     * @return mixed constraints.
447
     */
448 52
    private function loadTableConstraints($tableName, $returnType)
449
    {
450 52
        $indexes = $this->db->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
451 52
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
452 52
        $tableColumns = null;
453
454 52
        if (!empty($indexes) && !isset($indexes[0]['origin'])) {
455
            /*
456
             * SQLite may not have an "origin" column in INDEX_LIST
457
             * See https://www.sqlite.org/src/info/2743846cdba572f6
458
             */
459
            $tableColumns = $this->loadTableColumnsInfo($tableName);
460
        }
461
462
        $result = [
463 52
            'primaryKey' => null,
464
            'indexes' => [],
465
            'uniques' => [],
466
        ];
467
468 52
        foreach ($indexes as $index) {
469 42
            $columns = $this->db->createCommand(
470 42
                'PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')'
471 42
            )->queryAll();
472
473 42
            $columns = $this->normalizePdoRowKeyCase($columns, true);
474
475 42
            ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
476
477 42
            if ($tableColumns !== null) {
478
                // SQLite may not have an "origin" column in INDEX_LIST
479
                $index['origin'] = 'c';
480
                if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) {
481
                    $index['origin'] = 'pk';
482
                } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) {
483
                    $index['origin'] = 'u';
484
                }
485
            }
486
487 42
            $ic = new IndexConstraint();
488
489 42
            $ic->setIsPrimary($index['origin'] === 'pk');
490 42
            $ic->setIsUnique((bool) $index['unique']);
491 42
            $ic->setName($index['name']);
492 42
            $ic->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
493
494 42
            $result['indexes'][] = $ic;
495
496 42
            if ($index['origin'] === 'u') {
497 41
                $ct = new Constraint();
498
499 41
                $ct->setName($index['name']);
500 41
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
501
502 41
                $result['uniques'][] = $ct;
503 25
            } elseif ($index['origin'] === 'pk') {
504 24
                $ct = new Constraint();
505
506 24
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
507
508 24
                $result['primaryKey'] = $ct;
509
            }
510
        }
511
512 52
        if ($result['primaryKey'] === null) {
513
            /*
514
             * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID
515
             * See https://www.sqlite.org/lang_createtable.html#primkeyconst
516
             */
517
518 28
            if ($tableColumns === null) {
519 28
                $tableColumns = $this->loadTableColumnsInfo($tableName);
520
            }
521
522 28
            foreach ($tableColumns as $tableColumn) {
523 28
                if ($tableColumn['pk'] > 0) {
524 18
                    $ct = new Constraint();
525 18
                    $ct->setColumnNames([$tableColumn['name']]);
526
527 18
                    $result['primaryKey'] = $ct;
528 18
                    break;
529
                }
530
            }
531
        }
532
533 52
        foreach ($result as $type => $data) {
534 52
            $this->setTableMetadata($tableName, $type, $data);
535
        }
536
537 52
        return $result[$returnType];
538
    }
539
540
    /**
541
     * Return whether the specified identifier is a SQLite system identifier.
542
     *
543
     * @param string $identifier
544
     *
545
     * @return bool
546
     *
547
     * {@see https://www.sqlite.org/src/artifact/74108007d286232f}
548
     */
549
    private function isSystemIdentifier($identifier): bool
550
    {
551
        return strncmp($identifier, 'sqlite_', 7) === 0;
552
    }
553
}
554