Completed
Push — master ( e0dde8...a7d2aa )
by Carsten
09:47
created

Schema   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 95.76%

Importance

Changes 0
Metric Value
wmc 62
lcom 1
cbo 16
dl 0
loc 402
ccs 158
cts 165
cp 0.9576
rs 3.8461
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A loadTablePrimaryKey() 0 4 1
A loadTableForeignKeys() 0 18 4
A loadTableIndexes() 0 4 1
A loadTableUniques() 0 4 1
A findTableNames() 0 5 1
A loadTableSchema() 0 13 2
B loadTableChecks() 0 34 6
A loadTableDefaultValues() 0 4 1
A quoteSimpleTableName() 0 4 2
A quoteSimpleColumnName() 0 4 3
A createQueryBuilder() 0 4 1
A createColumnSchemaBuilder() 0 4 1
B findColumns() 0 22 6
A findConstraints() 0 14 3
A findUniqueIndexes() 0 20 4
C loadColumnSchema() 0 49 17
A setTransactionIsolationLevel() 0 13 3
B loadTableConstraints() 0 35 5

How to fix   Complexity   

Complex Class

Complex classes like Schema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Schema, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\db\sqlite;
9
10
use yii\base\NotSupportedException;
11
use yii\db\CheckConstraint;
12
use yii\db\ColumnSchema;
13
use yii\db\Constraint;
14
use yii\db\ConstraintFinderTrait;
15
use yii\db\Expression;
16
use yii\db\ForeignKeyConstraint;
17
use yii\db\IndexConstraint;
18
use yii\db\SqlToken;
19
use yii\db\TableSchema;
20
use yii\db\Transaction;
21
use yii\helpers\ArrayHelper;
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 [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
28
 *
29
 * @author Qiang Xue <[email protected]>
30
 * @since 2.0
31
 */
32
class Schema extends \yii\db\Schema
33
{
34
    use ConstraintFinderTrait;
35
36
    /**
37
     * @var array mapping from physical column types (keys) to abstract column types (values)
38
     */
39
    public $typeMap = [
40
        'tinyint' => self::TYPE_SMALLINT,
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
    /**
71
     * @inheritDoc
72
     */
73 5
    protected function findTableNames($schema = '')
74
    {
75 5
        $sql = "SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence' ORDER BY tbl_name";
76 5
        return $this->db->createCommand($sql)->queryColumn();
77
    }
78
79
    /**
80
     * @inheritDoc
81
     */
82 207
    protected function loadTableSchema($name)
83
    {
84 207
        $table = new TableSchema();
85 207
        $table->name = $name;
86 207
        $table->fullName = $name;
87
88 207
        if ($this->findColumns($table)) {
89 196
            $this->findConstraints($table);
90 196
            return $table;
91
        }
92
93 35
        return null;
94
    }
95
96
    /**
97
     * @inheritDoc
98
     */
99 12
    protected function loadTablePrimaryKey($tableName)
100
    {
101 12
        return $this->loadTableConstraints($tableName, 'primaryKey');
102
    }
103
104
    /**
105
     * @inheritDoc
106
     */
107 3
    protected function loadTableForeignKeys($tableName)
108
    {
109 3
        $foreignKeys = $this->db->createCommand('PRAGMA FOREIGN_KEY_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
110 3
        $foreignKeys = $this->normalizePdoRowKeyCase($foreignKeys, true);
111 3
        $foreignKeys = ArrayHelper::index($foreignKeys, null, 'table');
112 3
        ArrayHelper::multisort($foreignKeys, 'seq', SORT_ASC, SORT_NUMERIC);
113 3
        $result = [];
114 3
        foreach ($foreignKeys as $table => $foreignKey) {
115 3
            $result[] = new ForeignKeyConstraint([
116 3
                'columnNames' => ArrayHelper::getColumn($foreignKey, 'from'),
117 3
                'foreignTableName' => $table,
118 3
                'foreignColumnNames' => ArrayHelper::getColumn($foreignKey, 'to'),
119 3
                'onDelete' => isset($foreignKey[0]['on_delete']) ? $foreignKey[0]['on_delete'] : null,
120 3
                'onUpdate' => isset($foreignKey[0]['on_update']) ? $foreignKey[0]['on_update'] : null,
121
            ]);
122
        }
123 3
        return $result;
124
    }
125
126
    /**
127
     * @inheritDoc
128
     */
129 10
    protected function loadTableIndexes($tableName)
130
    {
131 10
        return $this->loadTableConstraints($tableName, 'indexes');
132
    }
133
134
    /**
135
     * @inheritDoc
136
     */
137 12
    protected function loadTableUniques($tableName)
138
    {
139 12
        return $this->loadTableConstraints($tableName, 'uniques');
140
    }
141
142
    /**
143
     * @inheritDoc
144
     */
145 12
    protected function loadTableChecks($tableName)
146
    {
147 12
        $sql = $this->db->createCommand('SELECT `sql` FROM `sqlite_master` WHERE name = :tableName', [
148 12
            ':tableName' => $tableName,
149 12
        ])->queryScalar();
150
        /** @var $code SqlToken[]|SqlToken[][]|SqlToken[][][] */
151 12
        $code = (new SqlTokenizer($sql))->tokenize();
0 ignored issues
show
Bug introduced by
It seems like $sql defined by $this->db->createCommand...leName))->queryScalar() on line 147 can also be of type false or null; however, yii\db\SqlTokenizer::__construct() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
152 12
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
153 12
        if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
0 ignored issues
show
Bug introduced by
The variable $firstMatchIndex does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $lastMatchIndex does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
154
            return [];
155
        }
156
157 12
        $createTableToken = $code[0][$lastMatchIndex - 1];
158 12
        $result = [];
159 12
        $offset = 0;
160 12
        while (true) {
161 12
            $pattern = (new SqlTokenizer('any CHECK()'))->tokenize();
162 12
            if (!$createTableToken->matches($pattern, $offset, $firstMatchIndex, $offset)) {
163 12
                break;
164
            }
165
166 3
            $checkSql = $createTableToken[$offset - 1]->getSql();
167 3
            $name = null;
168 3
            $pattern = (new SqlTokenizer('CONSTRAINT any'))->tokenize();
169 3
            if (isset($createTableToken[$firstMatchIndex - 2]) && $createTableToken->matches($pattern, $firstMatchIndex - 2)) {
170
                $name = $createTableToken[$firstMatchIndex - 1]->content;
171
            }
172 3
            $result[] = new CheckConstraint([
173 3
                'name' => $name,
174 3
                'expression' => $checkSql,
175
            ]);
176
        }
177 12
        return $result;
178
    }
179
180
    /**
181
     * @inheritDoc
182
     * @throws NotSupportedException if this method is called.
183
     */
184 12
    protected function loadTableDefaultValues($tableName)
0 ignored issues
show
Unused Code introduced by
The parameter $tableName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
185
    {
186 12
        throw new NotSupportedException('SQLite does not support default value constraints.');
187
    }
188
189
    /**
190
     * Quotes a table name for use in a query.
191
     * A simple table name has no schema prefix.
192
     * @param string $name table name
193
     * @return string the properly quoted table name
194
     */
195 299
    public function quoteSimpleTableName($name)
196
    {
197 299
        return strpos($name, '`') !== false ? $name : "`$name`";
198
    }
199
200
    /**
201
     * Quotes a column name for use in a query.
202
     * A simple column name has no prefix.
203
     * @param string $name column name
204
     * @return string the properly quoted column name
205
     */
206 296
    public function quoteSimpleColumnName($name)
207
    {
208 296
        return strpos($name, '`') !== false || $name === '*' ? $name : "`$name`";
209
    }
210
211
    /**
212
     * Creates a query builder for the MySQL database.
213
     * This method may be overridden by child classes to create a DBMS-specific query builder.
214
     * @return QueryBuilder query builder instance
215
     */
216 212
    public function createQueryBuilder()
217
    {
218 212
        return new QueryBuilder($this->db);
219
    }
220
221
    /**
222
     * @inheritdoc
223
     * @return ColumnSchemaBuilder column schema builder instance
224
     */
225 4
    public function createColumnSchemaBuilder($type, $length = null)
226
    {
227 4
        return new ColumnSchemaBuilder($type, $length);
228
    }
229
230
    /**
231
     * Collects the table column metadata.
232
     * @param TableSchema $table the table metadata
233
     * @return bool whether the table exists in the database
234
     */
235 207
    protected function findColumns($table)
236
    {
237 207
        $sql = 'PRAGMA table_info(' . $this->quoteSimpleTableName($table->name) . ')';
238 207
        $columns = $this->db->createCommand($sql)->queryAll();
239 207
        if (empty($columns)) {
240 35
            return false;
241
        }
242
243 196
        foreach ($columns as $info) {
244 196
            $column = $this->loadColumnSchema($info);
245 196
            $table->columns[$column->name] = $column;
246 196
            if ($column->isPrimaryKey) {
247 176
                $table->primaryKey[] = $column->name;
248
            }
249
        }
250 196
        if (count($table->primaryKey) === 1 && !strncasecmp($table->columns[$table->primaryKey[0]]->dbType, 'int', 3)) {
251 155
            $table->sequenceName = '';
252 155
            $table->columns[$table->primaryKey[0]]->autoIncrement = true;
253
        }
254
255 196
        return true;
256
    }
257
258
    /**
259
     * Collects the foreign key column details for the given table.
260
     * @param TableSchema $table the table metadata
261
     */
262 196
    protected function findConstraints($table)
263
    {
264 196
        $sql = 'PRAGMA foreign_key_list(' . $this->quoteSimpleTableName($table->name) . ')';
265 196
        $keys = $this->db->createCommand($sql)->queryAll();
266 196
        foreach ($keys as $key) {
267 8
            $id = (int) $key['id'];
268 8
            if (!isset($table->foreignKeys[$id])) {
269 8
                $table->foreignKeys[$id] = [$key['table'], $key['from'] => $key['to']];
270
            } else {
271
                // composite FK
272 5
                $table->foreignKeys[$id][$key['from']] = $key['to'];
273
            }
274
        }
275 196
    }
276
277
    /**
278
     * Returns all unique indexes for the given table.
279
     * Each array element is of the following structure:
280
     *
281
     * ```php
282
     * [
283
     *     'IndexName1' => ['col1' [, ...]],
284
     *     'IndexName2' => ['col2' [, ...]],
285
     * ]
286
     * ```
287
     *
288
     * @param TableSchema $table the table metadata
289
     * @return array all unique indexes for the given table.
290
     */
291 1
    public function findUniqueIndexes($table)
292
    {
293 1
        $sql = 'PRAGMA index_list(' . $this->quoteSimpleTableName($table->name) . ')';
294 1
        $indexes = $this->db->createCommand($sql)->queryAll();
295 1
        $uniqueIndexes = [];
296
297 1
        foreach ($indexes as $index) {
298 1
            $indexName = $index['name'];
299 1
            $indexInfo = $this->db->createCommand('PRAGMA index_info(' . $this->quoteValue($index['name']) . ')')->queryAll();
300
301 1
            if ($index['unique']) {
302 1
                $uniqueIndexes[$indexName] = [];
303 1
                foreach ($indexInfo as $row) {
304 1
                    $uniqueIndexes[$indexName][] = $row['name'];
305
                }
306
            }
307
        }
308
309 1
        return $uniqueIndexes;
310
    }
311
312
    /**
313
     * Loads the column information into a [[ColumnSchema]] object.
314
     * @param array $info column information
315
     * @return ColumnSchema the column schema object
316
     */
317 196
    protected function loadColumnSchema($info)
318
    {
319 196
        $column = $this->createColumnSchema();
320 196
        $column->name = $info['name'];
321 196
        $column->allowNull = !$info['notnull'];
322 196
        $column->isPrimaryKey = $info['pk'] != 0;
323
324 196
        $column->dbType = strtolower($info['type']);
325 196
        $column->unsigned = strpos($column->dbType, 'unsigned') !== false;
326
327 196
        $column->type = self::TYPE_STRING;
328 196
        if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
329 196
            $type = strtolower($matches[1]);
330 196
            if (isset($this->typeMap[$type])) {
331 196
                $column->type = $this->typeMap[$type];
332
            }
333
334 196
            if (!empty($matches[2])) {
335 188
                $values = explode(',', $matches[2]);
336 188
                $column->size = $column->precision = (int) $values[0];
337 188
                if (isset($values[1])) {
338 67
                    $column->scale = (int) $values[1];
339
                }
340 188
                if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
341 10
                    $column->type = 'boolean';
342 188
                } elseif ($type === 'bit') {
343
                    if ($column->size > 32) {
344
                        $column->type = 'bigint';
345
                    } elseif ($column->size === 32) {
346
                        $column->type = 'integer';
347
                    }
348
                }
349
            }
350
        }
351 196
        $column->phpType = $this->getColumnPhpType($column);
352
353 196
        if (!$column->isPrimaryKey) {
354 194
            if ($info['dflt_value'] === 'null' || $info['dflt_value'] === '' || $info['dflt_value'] === null) {
355 193
                $column->defaultValue = null;
356 76
            } elseif ($column->type === 'timestamp' && $info['dflt_value'] === 'CURRENT_TIMESTAMP') {
357 10
                $column->defaultValue = new Expression('CURRENT_TIMESTAMP');
358
            } else {
359 76
                $value = trim($info['dflt_value'], "'\"");
360 76
                $column->defaultValue = $column->phpTypecast($value);
361
            }
362
        }
363
364 196
        return $column;
365
    }
366
367
    /**
368
     * Sets the isolation level of the current transaction.
369
     * @param string $level The transaction isolation level to use for this transaction.
370
     * This can be either [[Transaction::READ_UNCOMMITTED]] or [[Transaction::SERIALIZABLE]].
371
     * @throws NotSupportedException when unsupported isolation levels are used.
372
     * SQLite only supports SERIALIZABLE and READ UNCOMMITTED.
373
     * @see http://www.sqlite.org/pragma.html#pragma_read_uncommitted
374
     */
375 2
    public function setTransactionIsolationLevel($level)
376
    {
377
        switch ($level) {
378 2
            case Transaction::SERIALIZABLE:
379 1
                $this->db->createCommand('PRAGMA read_uncommitted = False;')->execute();
380 1
                break;
381 2
            case Transaction::READ_UNCOMMITTED:
382 2
                $this->db->createCommand('PRAGMA read_uncommitted = True;')->execute();
383 2
                break;
384
            default:
385
                throw new NotSupportedException(get_class($this) . ' only supports transaction isolation levels READ UNCOMMITTED and SERIALIZABLE.');
386
        }
387 2
    }
388
389
    /**
390
     * Loads multiple types of constraints and returns the specified ones.
391
     * @param string $tableName table name.
392
     * @param string $returnType return type:
393
     * - primaryKey
394
     * - indexes
395
     * - uniques
396
     * @return mixed constraints.
397
     */
398 34
    private function loadTableConstraints($tableName, $returnType)
399
    {
400 34
        $indexes = $this->db->createCommand('PRAGMA INDEX_LIST (' . $this->quoteValue($tableName) . ')')->queryAll();
401 34
        $indexes = $this->normalizePdoRowKeyCase($indexes, true);
402
        $result = [
403 34
            'primaryKey' => null,
404
            'indexes' => [],
405
            'uniques' => [],
406
        ];
407 34
        foreach ($indexes as $index) {
408 25
            $columns = $this->db->createCommand('PRAGMA INDEX_INFO (' . $this->quoteValue($index['name']) . ')')->queryAll();
409 25
            $columns = $this->normalizePdoRowKeyCase($columns, true);
410 25
            ArrayHelper::multisort($columns, 'seqno', SORT_ASC, SORT_NUMERIC);
411 25
            $result['indexes'][] = new IndexConstraint([
412 25
                'isPrimary' => $index['origin'] === 'pk',
413 25
                'isUnique' => (bool) $index['unique'],
414 25
                'name' => $index['name'],
415 25
                'columnNames' => ArrayHelper::getColumn($columns, 'name'),
416
            ]);
417 25
            if ($index['origin'] === 'u') {
418 24
                $result['uniques'][] = new Constraint([
419 24
                    'name' => $index['name'],
420 24
                    'columnNames' => ArrayHelper::getColumn($columns, 'name'),
421
                ]);
422 25
            } elseif ($index['origin'] === 'pk') {
423 24
                $result['primaryKey'] = new Constraint([
424 24
                    'columnNames' => ArrayHelper::getColumn($columns, 'name'),
425
                ]);
426
            }
427
        }
428 34
        foreach ($result as $type => $data) {
429 34
            $this->setTableMetadata($tableName, $type, $data);
430
        }
431 34
        return $result[$returnType];
432
    }
433
}
434