Passed
Pull Request — master (#7)
by Wilmer
12:36
created

Schema::loadColumnSchema()   C

Complexity

Conditions 17
Paths 92

Size

Total Lines 49
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 33
c 0
b 0
f 0
nc 92
nop 1
dl 0
loc 49
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\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
    protected function findTableNames($schema = '')
82
    {
83
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
84
85
        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
    protected function loadTableSchema($name)
92
    {
93
        $table = new TableSchema();
94
95
        $table->name = $name;
96
        $table->fullName = $name;
97
98
        if ($this->findColumns($table)) {
99
            $this->findConstraints($table);
100
101
            return $table;
102
        }
103
104
        return null;
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110
    protected function loadTablePrimaryKey($tableName)
111
    {
112
        return $this->loadTableConstraints($tableName, 'primaryKey');
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    protected function loadTableForeignKeys($tableName)
119
    {
120
        $foreignKeys = $this->db->createCommand(
121
            'PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')'
122
        )->queryAll();
123
124
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
125
126
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
127
128
        ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
129
130
        $result = [];
131
132
        foreach ($foreignKeys as $table => $foreignKey) {
133
            $fk = new ForeignKeyConstraint();
134
135
            $fk->setColumnNames(ArrayHelper::getColumn($foreignKey, 'from'));
136
            $fk->setForeignTableName($table);
137
            $fk->setForeignColumnNames(ArrayHelper::getColumn($foreignKey, 'to'));
138
            $fk->setOnDelete($foreignKey[0]['on_delete'] ?? null);
139
            $fk->setOnUpdate($foreignKey[0]['on_update'] ?? null);
140
141
            $result[] = $fk;
142
        }
143
144
        return $result;
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150
    protected function loadTableIndexes($tableName)
151
    {
152
        return $this->loadTableConstraints($tableName, 'indexes');
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158
    protected function loadTableUniques($tableName)
159
    {
160
        return $this->loadTableConstraints($tableName, 'uniques');
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166
    protected function loadTableChecks($tableName)
167
    {
168
        $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
169
            ':tableName' => $tableName,
170
        ])->queryScalar();
171
172
        /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
173
        $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
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
175
176
        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
        $createTableToken = $code[0][$lastMatchIndex - 1];
181
        $result = [];
182
        $offset = 0;
183
184
        while (true) {
185
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
186
187
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
188
                break;
189
            }
190
191
            $checkSql = $createTableToken[$offset - 1]->getSql();
192
            $name = null;
193
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
194
195
            if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
196
                $name = $createTableToken[$firstMatchIndex - 1]->content;
197
            }
198
199
            $ck = new CheckConstraint();
200
            $ck->setName($name);
201
            $ck->setExpression($checkSql);
202
203
            $result[] = $ck;
204
        }
205
206
        return $result;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     *
212
     * @throws NotSupportedException if this method is called.
213
     */
214
    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
        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
    public function createQueryBuilder()
227
    {
228
        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
    public function createColumnSchemaBuilder($type, $length = null)
237
    {
238
        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
    protected function findColumns($table): bool
249
    {
250
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->name) . ')';
251
        $columns = $this->db->createCommand($sql)->queryAll();
252
253
        if (empty($columns)) {
254
            return false;
255
        }
256
257
        foreach ($columns as $info) {
258
            $column = $this->loadColumnSchema($info);
259
            $table->columns[$column->name] = $column;
260
            if ($column->isPrimaryKey) {
261
                $table->primaryKey[] = $column->name;
262
            }
263
        }
264
265
        if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) {
266
            $table->sequenceName = '';
267
            $table->columns[$table->primaryKey[0]]->autoIncrement = true;
268
        }
269
270
        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
    protected function findConstraints($table)
279
    {
280
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->name) . ')';
281
        $keys = $this->db->createCommand($sql)->queryAll();
282
283
        foreach ($keys as $key) {
284
            $id = (int) $key['id'];
285
            if (!isset($table->foreignKeys[$id])) {
286
                $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']];
287
            } else {
288
                // composite FK
289
                $table->foreignKeys[$id][$key['from']] = $key['to'];
290
            }
291
        }
292
    }
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
    public function findUniqueIndexes($table)
311
    {
312
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName($table->name) . ')';
313
        $indexes = $this->db->createCommand($sql)->queryAll();
314
        $uniqueIndexes = [];
315
316
        foreach ($indexes as $index) {
317
            $indexName = $index['name'];
318
            $indexInfo = $this->db->createCommand(
319
                'PRAGMA index_info(' . $this->quoteValue($index['name']) . ')'
320
            )->queryAll();
321
322
            if ($index['unique']) {
323
                $uniqueIndexes[$indexName] = [];
324
                foreach ($indexInfo as $row) {
325
                    $uniqueIndexes[$indexName][] = $row['name'];
326
                }
327
            }
328
        }
329
330
        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
    protected function loadColumnSchema($info): ColumnSchema
341
    {
342
        $column = $this->createColumnSchema();
343
        $column->name = $info['name'];
344
        $column->allowNull = !$info['notnull'];
345
        $column->isPrimaryKey = $info['pk'] != 0;
346
        $column->dbType = strtolower($info['type']);
347
        $column->unsigned = strpos($column->dbType, 'unsigned') !== false;
348
        $column->type = self::TYPE_STRING;
349
350
        if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $column->dbType, $matches)) {
351
            $type = strtolower($matches[1]);
352
353
            if (isset($this->typeMap[$type])) {
354
                $column->type = $this->typeMap[$type];
355
            }
356
357
            if (!empty($matches[2])) {
358
                $values = explode(',', $matches[2]);
359
                $column->size = $column->precision = (int) $values[0];
360
                if (isset($values[1])) {
361
                    $column->scale = (int) $values[1];
362
                }
363
                if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
364
                    $column->type = 'boolean';
365
                } 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
        $column->phpType = $this->getColumnPhpType($column);
376
377
        if (!$column->isPrimaryKey) {
378
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
379
                $column->defaultValue = null;
380
            } elseif ($column->type === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
381
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP');
382
            } else {
383
                $value = trim($info['dflt_value'], "'\"");
384
                $column->defaultValue = $column->phpTypecast($value);
385
            }
386
        }
387
388
        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
    public function setTransactionIsolationLevel($level): void
403
    {
404
        switch ($level) {
405
            case Transaction::SERIALIZABLE:
406
                $this->db->createCommand('PRAGMA read_uncommitted = False;')->execute();
407
                break;
408
            case Transaction::READ_UNCOMMITTED:
409
                $this->db->createCommand('PRAGMA read_uncommitted = True;')->execute();
410
                break;
411
            default:
412
                throw new NotSupportedException(
413
                    get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.'
414
                );
415
        }
416
    }
417
418
    /**
419
     * Returns table columns info.
420
     *
421
     * @param string $tableName table name
422
     *
423
     * @return array
424
     */
425
    private function loadTableColumnsInfo($tableName): array
426
    {
427
        $tableColumns = $this->db->createCommand(
428
            'PRAGMA TABLE_INFO (' . $this->quoteValue($tableName) . ')'
429
        )->queryAll();
430
431
        $tableColumns = $this->normalizePdoRowKeyCase($tableColumns, true);
432
433
        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
    private function loadTableConstraints($tableName, $returnType)
448
    {
449
        $indexes = $this->db->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
450
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
451
        $tableColumns = null;
452
453
        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
            'primaryKey' => null,
463
            'indexes' => [],
464
            'uniques' => [],
465
        ];
466
467
        foreach ($indexes as $index) {
468
            $columns = $this->db->createCommand(
469
                'PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')'
470
            )->queryAll();
471
472
            $columns = $this->normalizePdoRowKeyCase($columns, true);
473
474
            ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
475
476
            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
            $ic = new IndexConstraint();
487
488
            $ic->setIsPrimary($index['origin'] === 'pk');
489
            $ic->setIsUnique((bool) $index['unique']);
490
            $ic->setName($index['name']);
491
            $ic->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
492
493
            $result['indexes'][] = $ic;
494
495
            if ($index['origin'] === 'u') {
496
                $ct = new Constraint();
497
498
                $ct->setName($index['name']);
499
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
500
501
                $result['uniques'][] = $ct;
502
            } elseif ($index['origin'] === 'pk') {
503
                $ct = new Constraint();
504
505
                $ct->setColumnNames(ArrayHelper::getColumn($columns, 'name'));
506
507
                $result['primaryKey'] = $ct;
508
            }
509
        }
510
511
        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
            if ($tableColumns === null) {
518
                $tableColumns = $this->loadTableColumnsInfo($tableName);
519
            }
520
521
            foreach ($tableColumns as $tableColumn) {
522
                if ($tableColumn['pk'] > 0) {
523
                    $ct = new Constraint();
524
                    $ct->setColumnNames([$tableColumn['name']]);
525
526
                    $result['primaryKey'] = $ct;
527
                    break;
528
                }
529
            }
530
        }
531
532
        foreach ($result as $type => $data) {
533
            $this->setTableMetadata($tableName, $type, $data);
534
        }
535
536
        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