Failed Conditions
Pull Request — master (#3543)
by Andreas
15:32
created

SqliteSchemaManager   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 473
Duplicated Lines 0 %

Test Coverage

Coverage 85.41%

Importance

Changes 0
Metric Value
wmc 75
eloc 228
dl 0
loc 473
ccs 199
cts 233
cp 0.8541
rs 2.4
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A _getPortableTableDefinition() 0 3 1
A _getPortableTableIndexDefinition() 0 5 1
A dropForeignKey() 0 6 1
A dropDatabase() 0 7 2
A dropAndCreateForeignKey() 0 6 1
A createForeignKey() 0 6 1
A renameTable() 0 6 1
A createDatabase() 0 11 1
B listTableForeignKeys() 0 40 10
C _getPortableTableColumnList() 0 59 15
A getCreateTableSQL() 0 18 2
B _getPortableTableIndexesList() 0 59 7
F _getPortableTableColumnDefinition() 0 72 15
A parseColumnCollationFromSQL() 0 10 2
B _getPortableTableForeignKeysList() 0 46 8
A parseColumnCommentFromSQL() 0 12 3
A _getPortableViewDefinition() 0 3 1
A getTableDiffForAlterForeignKey() 0 16 3

How to fix   Complexity   

Complex Class

Complex classes like SqliteSchemaManager 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.

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 SqliteSchemaManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Doctrine\DBAL\Schema;
4
5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\DriverManager;
7
use Doctrine\DBAL\FetchMode;
8
use Doctrine\DBAL\Types\StringType;
9
use Doctrine\DBAL\Types\TextType;
10
use Doctrine\DBAL\Types\Type;
11
use const CASE_LOWER;
12
use function array_change_key_case;
13
use function array_map;
14
use function array_reverse;
15
use function array_values;
16
use function assert;
17
use function explode;
18
use function file_exists;
19
use function is_array;
20
use function preg_match;
21
use function preg_match_all;
22
use function preg_quote;
23
use function preg_replace;
24
use function rtrim;
25
use function sprintf;
26
use function str_replace;
27
use function strpos;
28
use function strtolower;
29
use function trim;
30
use function unlink;
31
use function usort;
32
33
/**
34
 * Sqlite SchemaManager.
35
 */
36
class SqliteSchemaManager extends AbstractSchemaManager
37
{
38
    /**
39
     * {@inheritdoc}
40
     */
41 88
    public function dropDatabase($database)
42
    {
43 88
        if (! file_exists($database)) {
44 87
            return;
45
        }
46
47 88
        unlink($database);
48 88
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53 88
    public function createDatabase($database)
54
    {
55 88
        $params  = $this->_conn->getParams();
56 88
        $driver  = $params['driver'];
57
        $options = [
58 88
            'driver' => $driver,
59 88
            'path' => $database,
60
        ];
61 88
        $conn    = DriverManager::getConnection($options);
62 88
        $conn->connect();
63 88
        $conn->close();
64 88
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69 86
    public function renameTable($name, $newName)
70
    {
71 86
        $tableDiff            = new TableDiff($name);
72 86
        $tableDiff->fromTable = $this->listTableDetails($name);
73 86
        $tableDiff->newName   = $newName;
74 86
        $this->alterTable($tableDiff);
75 86
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
81
    {
82
        $tableDiff                     = $this->getTableDiffForAlterForeignKey($table);
83
        $tableDiff->addedForeignKeys[] = $foreignKey;
84
85
        $this->alterTable($tableDiff);
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
92
    {
93
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
94
        $tableDiff->changedForeignKeys[] = $foreignKey;
95
96
        $this->alterTable($tableDiff);
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function dropForeignKey($foreignKey, $table)
103
    {
104
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
105
        $tableDiff->removedForeignKeys[] = $foreignKey;
106
107
        $this->alterTable($tableDiff);
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 85
    public function listTableForeignKeys($table, $database = null)
114
    {
115 85
        if ($database === null) {
116 85
            $database = $this->_conn->getDatabase();
117
        }
118 85
        $sql              = $this->_platform->getListTableForeignKeysSQL($table, $database);
0 ignored issues
show
Unused Code introduced by
The call to Doctrine\DBAL\Platforms\...stTableForeignKeysSQL() has too many arguments starting with $database. ( Ignorable by Annotation )

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

118
        /** @scrutinizer ignore-call */ 
119
        $sql              = $this->_platform->getListTableForeignKeysSQL($table, $database);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
119 85
        $tableForeignKeys = $this->_conn->fetchAll($sql);
120
121 85
        if (! empty($tableForeignKeys)) {
122 85
            $createSql = $this->getCreateTableSQL($table);
123
124 85
            if ($createSql !== null && preg_match_all(
125
                '#
126
                    (?:CONSTRAINT\s+([^\s]+)\s+)?
127
                    (?:FOREIGN\s+KEY[^\)]+\)\s*)?
128
                    REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
129
                    (?:
130
                        [^,]*?
131
                        (NOT\s+DEFERRABLE|DEFERRABLE)
132
                        (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
133
                    )?#isx',
134 85
                $createSql,
135 85
                $match
136
            )) {
137 85
                $names      = array_reverse($match[1]);
138 85
                $deferrable = array_reverse($match[2]);
139 85
                $deferred   = array_reverse($match[3]);
140
            } else {
141
                $names = $deferrable = $deferred = [];
142
            }
143
144 85
            foreach ($tableForeignKeys as $key => $value) {
145 85
                $id                                        = $value['id'];
146 85
                $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && $names[$id] !== '' ? $names[$id] : $id;
147 85
                $tableForeignKeys[$key]['deferrable']      = isset($deferrable[$id]) && strtolower($deferrable[$id]) === 'deferrable';
148 85
                $tableForeignKeys[$key]['deferred']        = isset($deferred[$id]) && strtolower($deferred[$id]) === 'deferred';
149
            }
150
        }
151
152 85
        return $this->_getPortableTableForeignKeysList($tableForeignKeys);
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 163
    protected function _getPortableTableDefinition($table)
159
    {
160 163
        return $table['name'];
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     *
166
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
167
     */
168 156
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
169
    {
170 156
        $indexBuffer = [];
171
172
        // fetch primary
173 156
        $stmt       = $this->_conn->executeQuery(sprintf(
174 4
            'PRAGMA TABLE_INFO (%s)',
175 156
            $this->_conn->quote($tableName)
176
        ));
177 156
        $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);
178 156
        assert(is_array($indexArray));
179
180
        usort($indexArray, static function ($a, $b) {
181 102
            if ($a['pk'] === $b['pk']) {
182 102
                return $a['cid'] - $b['cid'];
183
            }
184
185 86
            return $a['pk'] - $b['pk'];
186 156
        });
187 156
        foreach ($indexArray as $indexColumnRow) {
188 156
            if ($indexColumnRow['pk'] === '0') {
189 104
                continue;
190
            }
191
192 138
            $indexBuffer[] = [
193 138
                'key_name' => 'primary',
194
                'primary' => true,
195
                'non_unique' => false,
196 138
                'column_name' => $indexColumnRow['name'],
197
            ];
198
        }
199
200
        // fetch regular indexes
201 156
        foreach ($tableIndexes as $tableIndex) {
202
            // Ignore indexes with reserved names, e.g. autoindexes
203 82
            if (strpos($tableIndex['name'], 'sqlite_') === 0) {
204 82
                continue;
205
            }
206
207 73
            $keyName           = $tableIndex['name'];
208 73
            $idx               = [];
209 73
            $idx['key_name']   = $keyName;
210 73
            $idx['primary']    = false;
211 73
            $idx['non_unique'] = ! $tableIndex['unique'];
212
213 73
            $stmt       = $this->_conn->executeQuery(sprintf(
214
                'PRAGMA INDEX_INFO (%s)',
215 73
                $this->_conn->quote($keyName)
216
            ));
217 73
            $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);
218 73
            assert(is_array($indexArray));
219
220 73
            foreach ($indexArray as $indexColumnRow) {
221 73
                $idx['column_name'] = $indexColumnRow['name'];
222 73
                $indexBuffer[]      = $idx;
223
            }
224
        }
225
226 156
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232
    protected function _getPortableTableIndexDefinition($tableIndex)
233
    {
234
        return [
235
            'name' => $tableIndex['name'],
236
            'unique' => (bool) $tableIndex['unique'],
237
        ];
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 156
    protected function _getPortableTableColumnList($table, $database, $tableColumns)
244
    {
245 156
        $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
246
247
        // find column with autoincrement
248 156
        $autoincrementColumn = null;
249 156
        $autoincrementCount  = 0;
250
251 156
        foreach ($tableColumns as $tableColumn) {
252 156
            if ($tableColumn['pk'] === '0') {
253 104
                continue;
254
            }
255
256 138
            $autoincrementCount++;
257 138
            if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') {
258 73
                continue;
259
            }
260
261 138
            $autoincrementColumn = $tableColumn['name'];
262
        }
263
264 156
        if ($autoincrementCount === 1 && $autoincrementColumn !== null) {
0 ignored issues
show
introduced by
The condition $autoincrementColumn !== null is always false.
Loading history...
265 138
            foreach ($list as $column) {
266 138
                if ($autoincrementColumn !== $column->getName()) {
267 86
                    continue;
268
                }
269
270 138
                $column->setAutoincrement(true);
271
            }
272
        }
273
274
        // inspect column collation and comments
275 156
        $createSql = $this->getCreateTableSQL($table) ?? '';
276
277 156
        foreach ($list as $columnName => $column) {
278 156
            $type = $column->getType();
279
280 156
            if ($type instanceof StringType || $type instanceof TextType) {
281 102
                $column->setPlatformOption('collation', $this->parseColumnCollationFromSQL($columnName, $createSql) ?: 'BINARY');
282
            }
283
284 156
            $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
285
286 156
            if ($comment === null) {
287 156
                continue;
288
            }
289
290 102
            $type = $this->extractDoctrineTypeFromComment($comment, '');
291
292 102
            if ($type !== '') {
293 59
                $column->setType(Type::getType($type));
294
295 59
                $comment = $this->removeDoctrineTypeFromComment($comment, $type);
296
            }
297
298 102
            $column->setComment($comment);
299
        }
300
301 156
        return $list;
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307 156
    protected function _getPortableTableColumnDefinition($tableColumn)
308
    {
309 156
        $parts               = explode('(', $tableColumn['type']);
310 156
        $tableColumn['type'] = trim($parts[0]);
311 156
        if (isset($parts[1])) {
312 102
            $length                = trim($parts[1], ')');
313 102
            $tableColumn['length'] = $length;
314
        }
315
316 156
        $dbType   = strtolower($tableColumn['type']);
317 156
        $length   = $tableColumn['length'] ?? null;
318 156
        $unsigned = false;
319
320 156
        if (strpos($dbType, ' unsigned') !== false) {
321 36
            $dbType   = str_replace(' unsigned', '', $dbType);
322 36
            $unsigned = true;
323
        }
324
325 156
        $fixed   = false;
326 156
        $type    = $this->_platform->getDoctrineTypeMapping($dbType);
327 156
        $default = $tableColumn['dflt_value'];
328 156
        if ($default === 'NULL') {
329 102
            $default = null;
330
        }
331
332 156
        if ($default !== null) {
333
            // SQLite returns the default value as a literal expression, so we need to parse it
334 104
            if (preg_match('/^\'(.*)\'$/s', $default, $matches)) {
335 103
                $default = str_replace("''", "'", $matches[1]);
336
            }
337
        }
338
339 156
        $notnull = (bool) $tableColumn['notnull'];
340
341 156
        if (! isset($tableColumn['name'])) {
342
            $tableColumn['name'] = '';
343
        }
344
345 156
        $precision = null;
346 156
        $scale     = null;
347
348 156
        switch ($dbType) {
349 4
            case 'char':
350 71
                $fixed = true;
351 71
                break;
352 4
            case 'float':
353 4
            case 'double':
354 4
            case 'real':
355 4
            case 'decimal':
356 4
            case 'numeric':
357 72
                if (isset($tableColumn['length'])) {
358 72
                    if (strpos($tableColumn['length'], ',') === false) {
359
                        $tableColumn['length'] .= ',0';
360
                    }
361 72
                    [$precision, $scale] = array_map('trim', explode(',', $tableColumn['length']));
362
                }
363 72
                $length = null;
364 72
                break;
365
        }
366
367
        $options = [
368 156
            'length'   => $length,
369 156
            'unsigned' => (bool) $unsigned,
370 156
            'fixed'    => $fixed,
371 156
            'notnull'  => $notnull,
372 156
            'default'  => $default,
373 156
            'precision' => $precision,
374 156
            'scale'     => $scale,
375
            'autoincrement' => false,
376
        ];
377
378 156
        return new Column($tableColumn['name'], Type::getType($type), $options);
379
    }
380
381
    /**
382
     * {@inheritdoc}
383
     */
384 63
    protected function _getPortableViewDefinition($view)
385
    {
386 63
        return new View($view['name'], $view['sql']);
387
    }
388
389
    /**
390
     * {@inheritdoc}
391
     */
392 85
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
393
    {
394 85
        $list = [];
395 85
        foreach ($tableForeignKeys as $value) {
396 85
            $value = array_change_key_case($value, CASE_LOWER);
397 85
            $name  = $value['constraint_name'];
398 85
            if (! isset($list[$name])) {
399 85
                if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') {
400
                    $value['on_delete'] = null;
401
                }
402 85
                if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') {
403
                    $value['on_update'] = null;
404
                }
405
406 85
                $list[$name] = [
407 85
                    'name' => $name,
408
                    'local' => [],
409
                    'foreign' => [],
410 85
                    'foreignTable' => $value['table'],
411 85
                    'onDelete' => $value['on_delete'],
412 85
                    'onUpdate' => $value['on_update'],
413 85
                    'deferrable' => $value['deferrable'],
414 85
                    'deferred'=> $value['deferred'],
415
                ];
416
            }
417 85
            $list[$name]['local'][]   = $value['from'];
418 85
            $list[$name]['foreign'][] = $value['to'];
419
        }
420
421 85
        $result = [];
422 85
        foreach ($list as $constraint) {
423 85
            $result[] = new ForeignKeyConstraint(
424 85
                array_values($constraint['local']),
425 85
                $constraint['foreignTable'],
426 85
                array_values($constraint['foreign']),
427 85
                $constraint['name'],
428
                [
429 85
                    'onDelete' => $constraint['onDelete'],
430 85
                    'onUpdate' => $constraint['onUpdate'],
431 85
                    'deferrable' => $constraint['deferrable'],
432 85
                    'deferred'=> $constraint['deferred'],
433
                ]
434
            );
435
        }
436
437 85
        return $result;
438
    }
439
440
    /**
441
     * @param Table|string $table
442
     *
443
     * @return TableDiff
444
     *
445
     * @throws DBALException
446
     */
447
    private function getTableDiffForAlterForeignKey($table)
448
    {
449
        if (! $table instanceof Table) {
450
            $tableDetails = $this->tryMethod('listTableDetails', $table);
451
452
            if ($tableDetails === false) {
453
                throw new DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table));
454
            }
455
456
            $table = $tableDetails;
457
        }
458
459
        $tableDiff            = new TableDiff($table->getName());
460
        $tableDiff->fromTable = $table;
461
462
        return $tableDiff;
463
    }
464
465 974
    private function parseColumnCollationFromSQL(string $column, string $sql) : ?string
466
    {
467 974
        $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
468 974
            . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
469
470 974
        if (preg_match($pattern, $sql, $match) !== 1) {
471 904
            return null;
472
        }
473
474 946
        return $match[1];
475
    }
476
477 680
    private function parseColumnCommentFromSQL(string $column, string $sql) : ?string
478
    {
479 680
        $pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column)) . '\W|\W' . preg_quote($column)
480 680
            . '\W)(?:\(.*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
481
482 680
        if (preg_match($pattern, $sql, $match) !== 1) {
483 658
            return null;
484
        }
485
486 652
        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
487
488 652
        return $comment === '' ? null : $comment;
489
    }
490
491 156
    private function getCreateTableSQL(string $table) : ?string
492
    {
493 156
        return $this->_conn->fetchColumn(
494
            <<<'SQL'
495 156
SELECT sql
496
  FROM (
497
      SELECT *
498
        FROM sqlite_master
499
   UNION ALL
500
      SELECT *
501
        FROM sqlite_temp_master
502
  )
503
WHERE type = 'table'
504
AND name = ?
505
SQL
506
            ,
507 156
            [$table]
508 156
        ) ?: null;
509
    }
510
}
511