Completed
Push — 2.10.x ( 61a6b9...f20ba1 )
by Grégoire
13:37 queued 11s
created

SqliteSchemaManager   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 517
Duplicated Lines 0 %

Test Coverage

Coverage 91.43%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 80
eloc 242
dl 0
loc 517
ccs 224
cts 245
cp 0.9143
rs 2
c 2
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A dropForeignKey() 0 6 1
A dropDatabase() 0 7 2
A dropAndCreateForeignKey() 0 6 1
A renameTable() 0 6 1
A createDatabase() 0 11 1
A _getPortableTableDefinition() 0 3 1
B _getPortableTableIndexesList() 0 57 7
A createForeignKey() 0 6 1
A parseTableCommentFromSQL() 0 17 3
B listTableForeignKeys() 0 41 10
C _getPortableTableColumnList() 0 59 15
A getCreateTableSQL() 0 18 2
F _getPortableTableColumnDefinition() 0 74 15
A _getPortableTableIndexDefinition() 0 5 1
A parseColumnCollationFromSQL() 0 10 2
B _getPortableTableForeignKeysList() 0 48 8
A parseColumnCommentFromSQL() 0 12 3
A _getPortableViewDefinition() 0 3 1
A listTableDetails() 0 13 2
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 function array_change_key_case;
12
use function array_map;
13
use function array_reverse;
14
use function array_values;
15
use function explode;
16
use function file_exists;
17
use function preg_match;
18
use function preg_match_all;
19
use function preg_quote;
20
use function preg_replace;
21
use function rtrim;
22
use function sprintf;
23
use function str_replace;
24
use function strpos;
25
use function strtolower;
26
use function trim;
27
use function unlink;
28
use function usort;
29
use const CASE_LOWER;
30
31
/**
32
 * Sqlite SchemaManager.
33
 */
34
class SqliteSchemaManager extends AbstractSchemaManager
35
{
36
    /**
37
     * {@inheritdoc}
38
     */
39 91
    public function dropDatabase($database)
40
    {
41 91
        if (! file_exists($database)) {
42 90
            return;
43
        }
44
45 91
        unlink($database);
46 91
    }
47
48
    /**
49
     * {@inheritdoc}
50
     */
51 91
    public function createDatabase($database)
52
    {
53 91
        $params  = $this->_conn->getParams();
54 91
        $driver  = $params['driver'];
55
        $options = [
56 91
            'driver' => $driver,
57 91
            'path' => $database,
58
        ];
59 91
        $conn    = DriverManager::getConnection($options);
60 91
        $conn->connect();
61 91
        $conn->close();
62 91
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 89
    public function renameTable($name, $newName)
68
    {
69 89
        $tableDiff            = new TableDiff($name);
70 89
        $tableDiff->fromTable = $this->listTableDetails($name);
71 89
        $tableDiff->newName   = $newName;
72 89
        $this->alterTable($tableDiff);
73 89
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78 66
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
79
    {
80 66
        $tableDiff                     = $this->getTableDiffForAlterForeignKey($table);
81 66
        $tableDiff->addedForeignKeys[] = $foreignKey;
82
83 66
        $this->alterTable($tableDiff);
84 66
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
90
    {
91
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
92
        $tableDiff->changedForeignKeys[] = $foreignKey;
93
94
        $this->alterTable($tableDiff);
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100
    public function dropForeignKey($foreignKey, $table)
101
    {
102
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
103
        $tableDiff->removedForeignKeys[] = $foreignKey;
104
105
        $this->alterTable($tableDiff);
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 149
    public function listTableForeignKeys($table, $database = null)
112
    {
113 149
        if ($database === null) {
114 149
            $database = $this->_conn->getDatabase();
115
        }
116
117 149
        $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

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