Passed
Push — master ( d42673...c7102b )
by Wilmer
09:45 queued 08:06
created

Schema::loadColumnSchema()   C

Complexity

Conditions 17
Paths 92

Size

Total Lines 50
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 17.47

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 17
eloc 34
c 1
b 0
f 0
nc 92
nop 1
dl 0
loc 50
ccs 30
cts 34
cp 0.8824
crap 17.47
rs 5.2166

How to fix   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\Sqlite\Query\QueryBuilder;
19
use Yiisoft\Db\Sqlite\Token\SqlToken;
20
use Yiisoft\Db\Sqlite\Token\SqlTokenizer;
21
use Yiisoft\Db\Transaction\Transaction;
22
23
/**
24
 * Schema is the class for retrieving metadata from a SQLite (2/3) database.
25
 *
26
 * @property string $transactionIsolationLevel The transaction isolation level to use for this transaction.
27
 * This can be either {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
28
 */
29
class Schema extends AbstractSchema implements ConstraintFinderInterface
30
{
31
    use ConstraintFinderTrait;
32
33
    protected string $tableQuoteCharacter = '`';
34
    protected string $columnQuoteCharacter = '`';
35
36
    /**
37
     * @var array mapping from physical column types (keys) to abstract column types (values)
38
     */
39
    private array $typeMap = [
40
        'tinyint' => self::TYPE_TINYINT,
41
        'bit' => self::TYPE_SMALLINT,
42
        'boolean' => self::TYPE_BOOLEAN,
43
        'bool' => self::TYPE_BOOLEAN,
44
        'smallint' => self::TYPE_SMALLINT,
45
        'mediumint' => self::TYPE_INTEGER,
46
        'int' => self::TYPE_INTEGER,
47
        'integer' => self::TYPE_INTEGER,
48
        'bigint' => self::TYPE_BIGINT,
49
        'float' => self::TYPE_FLOAT,
50
        'double' => self::TYPE_DOUBLE,
51
        'real' => self::TYPE_FLOAT,
52
        'decimal' => self::TYPE_DECIMAL,
53
        'numeric' => self::TYPE_DECIMAL,
54
        'tinytext' => self::TYPE_TEXT,
55
        'mediumtext' => self::TYPE_TEXT,
56
        'longtext' => self::TYPE_TEXT,
57
        'text' => self::TYPE_TEXT,
58
        'varchar' => self::TYPE_STRING,
59
        'string' => self::TYPE_STRING,
60
        'char' => self::TYPE_CHAR,
61
        'blob' => self::TYPE_BINARY,
62
        'datetime' => self::TYPE_DATETIME,
63
        'year' => self::TYPE_DATE,
64
        'date' => self::TYPE_DATE,
65
        'time' => self::TYPE_TIME,
66
        'timestamp' => self::TYPE_TIMESTAMP,
67
        'enum' => self::TYPE_STRING,
68
    ];
69
70 5
    protected function findTableNames($schema = '')
71
    {
72 5
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
73
74 5
        return $this->getDb()->createCommand($sql)->queryColumn();
75
    }
76
77 77
    protected function loadTableSchema(string $name): ?TableSchema
78
    {
79 77
        $table = new TableSchema();
80
81 77
        $table->name($name);
82 77
        $table->fullName($name);
83
84 77
        if ($this->findColumns($table)) {
85 75
            $this->findConstraints($table);
86
87 75
            return $table;
88
        }
89
90 10
        return null;
91
    }
92
93 30
    protected function loadTablePrimaryKey($tableName)
94
    {
95 30
        return $this->loadTableConstraints($tableName, 'primaryKey');
96
    }
97
98 3
    protected function loadTableForeignKeys($tableName)
99
    {
100 3
        $foreignKeys = $this->getDb()->createCommand(
101 3
            'PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')'
102 3
        )->queryAll();
103
104 3
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
105
106 3
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
107
108 3
        ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
109
110 3
        $result = [];
111
112 3
        foreach ($foreignKeys as $table => $foreignKey) {
113 3
            $fk = new ForeignKeyConstraint();
114
115 3
            $fk->setColumnNames(ArrayHelper::getColumn($foreignKey, 'from'));
116 3
            $fk->setForeignTableName($table);
117 3
            $fk->setForeignColumnNames(ArrayHelper::getColumn($foreignKey, 'to'));
118 3
            $fk->setOnDelete($foreignKey[0]['on_delete'] ?? null);
119 3
            $fk->setOnUpdate($foreignKey[0]['on_update'] ?? null);
120
121 3
            $result[] = $fk;
122
        }
123
124 3
        return $result;
125
    }
126
127 10
    protected function loadTableIndexes($tableName)
128
    {
129 10
        return $this->loadTableConstraints($tableName, 'indexes');
130
    }
131
132 12
    protected function loadTableUniques($tableName)
133
    {
134 12
        return $this->loadTableConstraints($tableName, 'uniques');
135
    }
136
137 12
    protected function loadTableChecks($tableName)
138
    {
139 12
        $sql = $this->getDb()->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
140 12
            ':tableName' => $tableName,
141 12
        ])->queryScalar();
142
143
        /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
144 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

144
        $code = (new SqlTokenizer(/** @scrutinizer ignore-type */ $sql))->tokenize();
Loading history...
145 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
146
147 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...
148
            return [];
149
        }
150
151 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
152 12
        $result = [];
153 12
        $offset = 0;
154
155 12
        while (true) {
156 12
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
157
158 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
159 12
                break;
160
            }
161
162 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
163 3
            $name = null;
164 3
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
165
166 3
            if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
167
                $name = $createTableToken[$firstMatchIndex - 1]->content;
168
            }
169
170 3
            $ck = new CheckConstraint();
171 3
            $ck->setName($name);
172 3
            $ck->setExpression($checkSql);
173
174 3
            $result[] = $ck;
175
        }
176
177 12
        return $result;
178
    }
179
180 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

180
    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...
181
    {
182 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
183
    }
184
185
    /**
186
     * Creates a query builder for the MySQL database.
187
     *
188
     * This method may be overridden by child classes to create a DBMS-specific query builder.
189
     *
190
     * @return QueryBuilder query builder instance
191
     */
192 50
    public function createQueryBuilder()
193
    {
194 50
        return new QueryBuilder($this->getDb());
0 ignored issues
show
Bug introduced by
It seems like $this->getDb() 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

194
        return new QueryBuilder(/** @scrutinizer ignore-type */ $this->getDb());
Loading history...
195
    }
196
197
    /**
198
     * @return ColumnSchemaBuilder column schema builder instance
199
     */
200 3
    public function createColumnSchemaBuilder($type, $length = null)
201
    {
202 3
        return new ColumnSchemaBuilder($type, $length);
203
    }
204
205
    /**
206
     * Collects the table column metadata.
207
     *
208
     * @param TableSchema $table the table metadata
209
     *
210
     * @return bool whether the table exists in the database
211
     */
212 77
    protected function findColumns($table): bool
213
    {
214 77
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->getName()) . ')';
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteSimpleTableName() 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

214
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
215 77
        $columns = $this->getDb()->createCommand($sql)->queryAll();
216
217 77
        if (empty($columns)) {
218 10
            return false;
219
        }
220
221 75
        foreach ($columns as $info) {
222 75
            $column = $this->loadColumnSchema($info);
223 75
            $table->columns($column->getName(), $column);
224 75
            if ($column->getIsPrimaryKey()) {
225 52
                $table->primaryKey($column->getName());
226
            }
227
        }
228
229 75
        $pk = $table->getPrimaryKey();
230 75
        if (count($pk) === 1 && !strncasecmp($table->getColumn($pk[0])->getDbType(), 'int', 3)) {
231 52
            $table->sequenceName('');
232 52
            $table->getColumn($pk[0])->autoIncrement(true);
233
        }
234
235 75
        return true;
236
    }
237
238
    /**
239
     * Collects the foreign key column details for the given table.
240
     *
241
     * @param TableSchema $table the table metadata
242
     */
243 75
    protected function findConstraints($table)
244
    {
245 75
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->getName()) . ')';
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteSimpleTableName() 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

245
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
246 75
        $keys = $this->getDb()->createCommand($sql)->queryAll();
247
248 75
        foreach ($keys as $key) {
249 5
            $id = (int) $key['id'];
250 5
            $fk = $table->getForeignKeys();
251 5
            if (!isset($fk[$id])) {
252 5
                $table->foreignKey($id, ([$key['table'], $key['from'] => $key['to']]));
253
            } else {
254
                /** composite FK */
255 5
                $table->compositeFK($id, $key['from'], $key['to']);
256
            }
257
        }
258 75
    }
259
260
    /**
261
     * Returns all unique indexes for the given table.
262
     *
263
     * Each array element is of the following structure:
264
     *
265
     * ```php
266
     * [
267
     *     'IndexName1' => ['col1' [, ...]],
268
     *     'IndexName2' => ['col2' [, ...]],
269
     * ]
270
     * ```
271
     *
272
     * @param TableSchema $table the table metadata
273
     *
274
     * @return array all unique indexes for the given table.
275
     */
276 1
    public function findUniqueIndexes($table)
277
    {
278 1
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName($table->getName()) . ')';
0 ignored issues
show
Bug introduced by
It seems like $table->getName() can also be of type null; however, parameter $name of Yiisoft\Db\Schema\Schema::quoteSimpleTableName() 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

278
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName(/** @scrutinizer ignore-type */ $table->getName()) . ')';
Loading history...
279 1
        $indexes = $this->getDb()->createCommand($sql)->queryAll();
280 1
        $uniqueIndexes = [];
281
282 1
        foreach ($indexes as $index) {
283 1
            $indexName = $index['name'];
284 1
            $indexInfo = $this->getDb()->createCommand(
285 1
                'PRAGMA index_info(' . $this->quoteValue($index['name']) . ')'
286 1
            )->queryAll();
287
288 1
            if ($index['unique']) {
289 1
                $uniqueIndexes[$indexName] = [];
290 1
                foreach ($indexInfo as $row) {
291 1
                    $uniqueIndexes[$indexName][] = $row['name'];
292
                }
293
            }
294
        }
295
296 1
        return $uniqueIndexes;
297
    }
298
299
    /**
300
     * Loads the column information into a {@see ColumnSchema} object.
301
     *
302
     * @param array $info column information
303
     *
304
     * @return ColumnSchema the column schema object
305
     */
306 75
    protected function loadColumnSchema($info): ColumnSchema
307
    {
308 75
        $column = $this->createColumnSchema();
309 75
        $column->name($info['name']);
310 75
        $column->allowNull(!$info['notnull']);
311 75
        $column->isPrimaryKey($info['pk'] != 0);
312 75
        $column->dbType(strtolower($info['type']));
313 75
        $column->unsigned(strpos($column->getDbType(), 'unsigned') !== false);
314 75
        $column->type(self::TYPE_STRING);
315
316 75
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->getDbType(), $matches)) {
317 75
            $type = strtolower($matches[1]);
318
319 75
            if (isset($this->typeMap[$type])) {
320 75
                $column->type($this->typeMap[$type]);
321
            }
322
323 75
            if (!empty($matches[2])) {
324 72
                $values = explode(',', $matches[2]);
325 72
                $column->precision((int) $values[0]);
326 72
                $column->size((int) $values[0]);
327 72
                if (isset($values[1])) {
328 25
                    $column->scale((int) $values[1]);
329
                }
330 72
                if ($column->getSize() === 1 && ($type === 'tinyint' || $type === 'bit')) {
331 21
                    $column->type('boolean');
332 72
                } elseif ($type === 'bit') {
333
                    if ($column->getSize() > 32) {
334
                        $column->type('bigint');
335
                    } elseif ($column->getSize() === 32) {
336
                        $column->type('integer');
337
                    }
338
                }
339
            }
340
        }
341
342 75
        $column->phpType($this->getColumnPhpType($column));
343
344 75
        if (!$column->getIsPrimaryKey()) {
345 74
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
346 73
                $column->defaultValue(null);
347 60
            } elseif ($column->getType() === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
348 21
                $column->defaultValue(new Expression('CURRENT_TIMESTAMP'));
349
            } else {
350 60
                $value = trim($info['dflt_value'], "'\"");
351 60
                $column->defaultValue($column->phpTypecast($value));
352
            }
353
        }
354
355 75
        return $column;
356
    }
357
358
    /**
359
     * Sets the isolation level of the current transaction.
360
     *
361
     * @param string $level The transaction isolation level to use for this transaction. This can be either
362
     * {@see Transaction::READ_UNCOMMITTED} or {@see Transaction::SERIALIZABLE}.
363
     *
364
     * @throws NotSupportedException when unsupported isolation levels are used. SQLite only supports SERIALIZABLE and
365
     * READ UNCOMMITTED.
366
     *
367
     * {@see http://www.sqlite.org/pragma.html#pragma_read_uncommitted}
368
     */
369 3
    public function setTransactionIsolationLevel($level): void
370
    {
371
        switch ($level) {
372 3
            case Transaction::SERIALIZABLE:
373 1
                $this->getDb()->createCommand('PRAGMA read_uncommitted = False;')->execute();
374 1
                break;
375 3
            case Transaction::READ_UNCOMMITTED:
376 3
                $this->getDb()->createCommand('PRAGMA read_uncommitted = True;')->execute();
377 3
                break;
378
            default:
379
                throw new NotSupportedException(
380
                    get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'
381
                );
382
        }
383 3
    }
384
385
    /**
386
     * Returns table columns info.
387
     *
388
     * @param string $tableName table name
389
     *
390
     * @return array
391
     */
392 28
    private function loadTableColumnsInfo($tableName): array
393
    {
394 28
        $tableColumns = $this->getDb()->createCommand(
395 28
            'PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')'
396 28
        )->queryAll();
397
398 28
        $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
399
400 28
        return ArrayHelper::index($tableColumns, 'cid');
401
    }
402
403
    /**
404
     * Loads multiple types of constraints and returns the specified ones.
405
     *
406
     * @param string $tableName table name.
407
     * @param string $returnType return type:
408
     * - primaryKey
409
     * - indexes
410
     * - uniques
411
     *
412
     * @return mixed constraints.
413
     */
414 52
    private function loadTableConstraints($tableName, $returnType)
415
    {
416 52
        $indexes = $this->getDb()->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
417 52
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
418 52
        $tableColumns = null;
419
420 52
        if (!empty($indexes) && !isset($indexes[0]['origin'])) {
421
            /*
422
             * SQLite may not have an "origin" column in INDEX_LIST
423
             * See https://www.sqlite.org/src/info/2743846cdba572f6
424
             */
425
            $tableColumns = $this->loadTableColumnsInfo($tableName);
426
        }
427
428
        $result = [
429 52
            'primaryKey' => null,
430
            'indexes' => [],
431
            'uniques' => [],
432
        ];
433
434 52
        foreach ($indexes as $index) {
435 42
            $columns = $this->getDb()->createCommand(
436 42
                'PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')'
437 42
            )->queryAll();
438
439 42
            $columns = $this->normalizePdoRowKeyCase($columns, true);
440
441 42
            ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
442
443 42
            if ($tableColumns !== null) {
444
                // SQLite may not have an "origin" column in INDEX_LIST
445
                $index['origin'] = 'c';
446
                if (!empty($columns) && $tableColumns[$columns[0]['cid']]['pk'] > 0) {
447
                    $index['origin'] = 'pk';
448
                } elseif ($index['unique'] && $this->isSystemIdentifier($index['name'])) {
449
                    $index['origin'] = 'u';
450
                }
451
            }
452
453 42
            $ic = new IndexConstraint();
454
455 42
            $ic->setIsPrimary($index['origin'] === 'pk');
456 42
            $ic->setIsUnique((bool) $index['unique']);
457 42
            $ic->setName($index['name']);
458 42
            $ic->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
459
460 42
            $result['indexes'][] = $ic;
461
462 42
            if ($index['origin'] === 'u') {
463 41
                $ct = new Constraint();
464
465 41
                $ct->setName($index['name']);
466 41
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
467
468 41
                $result['uniques'][] = $ct;
469 25
            } elseif ($index['origin'] === 'pk') {
470 24
                $ct = new Constraint();
471
472 24
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
473
474 24
                $result['primaryKey'] = $ct;
475
            }
476
        }
477
478 52
        if ($result['primaryKey'] === null) {
479
            /*
480
             * Additional check for PK in case of INTEGER PRIMARY KEY with ROWID
481
             * See https://www.sqlite.org/lang_createtable.html#primkeyconst
482
             */
483
484 28
            if ($tableColumns === null) {
485 28
                $tableColumns = $this->loadTableColumnsInfo($tableName);
486
            }
487
488 28
            foreach ($tableColumns as $tableColumn) {
489 28
                if ($tableColumn['pk'] > 0) {
490 18
                    $ct = new Constraint();
491 18
                    $ct->setColumnNames([$tableColumn['name']]);
492
493 18
                    $result['primaryKey'] = $ct;
494 18
                    break;
495
                }
496
            }
497
        }
498
499 52
        foreach ($result as $type => $data) {
500 52
            $this->setTableMetadata($tableName, $type, $data);
501
        }
502
503 52
        return $result[$returnType];
504
    }
505
506
    /**
507
     * Return whether the specified identifier is a SQLite system identifier.
508
     *
509
     * @param string $identifier
510
     *
511
     * @return bool
512
     *
513
     * {@see https://www.sqlite.org/src/artifact/74108007d286232f}
514
     */
515
    private function isSystemIdentifier($identifier): bool
516
    {
517
        return strncmp($identifier, 'sqlite_', 7) === 0;
518
    }
519
}
520