SqliteSchemaManager   F
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 485
Duplicated Lines 0 %

Test Coverage

Coverage 91.06%

Importance

Changes 0
Metric Value
eloc 231
dl 0
loc 485
rs 2.8
c 0
b 0
f 0
ccs 214
cts 235
cp 0.9106
wmc 70

20 Methods

Rating   Name   Duplication   Size   Complexity  
A dropForeignKey() 0 13 2
A dropDatabase() 0 7 2
C _getPortableTableColumnList() 0 56 13
A dropAndCreateForeignKey() 0 9 1
A parseTableCommentFromSQL() 0 17 3
B listTableForeignKeys() 0 41 9
A getCreateTableSQL() 0 24 2
B _getPortableTableIndexesList() 0 57 7
B _getPortableTableColumnDefinition() 0 57 9
A renameTable() 0 6 1
A parseColumnCollationFromSQL() 0 10 2
A createDatabase() 0 11 1
B _getPortableTableForeignKeysList() 0 48 8
A parseColumnCommentFromSQL() 0 13 2
A _getPortableViewDefinition() 0 3 1
A listTableDetails() 0 13 2
A _getPortableTableDefinition() 0 3 1
A createForeignKey() 0 9 1
A ensureTable() 0 7 2
A getTableDiffForAlterForeignKey() 0 6 1

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
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Schema;
6
7
use Doctrine\DBAL\DriverManager;
8
use Doctrine\DBAL\FetchMode;
9
use Doctrine\DBAL\Types\StringType;
10
use Doctrine\DBAL\Types\TextType;
11
use Doctrine\DBAL\Types\Type;
12
use function array_change_key_case;
13
use function array_reverse;
14
use function array_values;
15
use function assert;
16
use function count;
17
use function file_exists;
18
use function is_string;
19
use function preg_match;
20
use function preg_match_all;
21
use function preg_quote;
22
use function preg_replace;
23
use function rtrim;
24
use function sprintf;
25
use function str_replace;
26
use function strpos;
27
use function strtolower;
28
use function trim;
29
use function unlink;
30
use function usort;
31
use const CASE_LOWER;
32
33
/**
34
 * Sqlite SchemaManager.
35
 */
36
class SqliteSchemaManager extends AbstractSchemaManager
37
{
38 2
    public function dropDatabase(string $database) : void
39
    {
40 2
        if (! file_exists($database)) {
41 1
            return;
42
        }
43
44 2
        unlink($database);
45 2
    }
46
47 2
    public function createDatabase(string $database) : void
48
    {
49 2
        $params  = $this->_conn->getParams();
50 2
        $driver  = $params['driver'];
51
        $options = [
52 2
            'driver' => $driver,
53 2
            'path' => $database,
54
        ];
55 2
        $conn    = DriverManager::getConnection($options);
56 2
        $conn->connect();
57 2
        $conn->close();
58 2
    }
59
60 1
    public function renameTable(string $name, string $newName) : void
61
    {
62 1
        $tableDiff            = new TableDiff($name);
63 1
        $tableDiff->fromTable = $this->listTableDetails($name);
64 1
        $tableDiff->newName   = $newName;
65 1
        $this->alterTable($tableDiff);
66 1
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 2
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) : void
72
    {
73 2
        $table = $this->ensureTable($table);
74
75 2
        $tableDiff = $this->getTableDiffForAlterForeignKey($table);
76
77 2
        $tableDiff->addedForeignKeys[] = $foreignKey;
78
79 2
        $this->alterTable($tableDiff);
80 2
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) : void
86
    {
87
        $table = $this->ensureTable($table);
88
89
        $tableDiff = $this->getTableDiffForAlterForeignKey($table);
90
91
        $tableDiff->changedForeignKeys[] = $foreignKey;
92
93
        $this->alterTable($tableDiff);
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99
    public function dropForeignKey($foreignKey, $table) : void
100
    {
101
        $table = $this->ensureTable($table);
102
103
        $tableDiff = $this->getTableDiffForAlterForeignKey($table);
104
105
        if (is_string($foreignKey)) {
106
            $tableDiff->removedForeignKeys[] = $table->getForeignKey($foreignKey);
107
        } else {
108
            $tableDiff->removedForeignKeys[] = $foreignKey;
109
        }
110
111
        $this->alterTable($tableDiff);
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117 104
    public function listTableForeignKeys(string $table, ?string $database = null) : array
118
    {
119 104
        if ($database === null) {
120 104
            $database = $this->_conn->getDatabase();
121
        }
122
123 104
        $sql              = $this->_platform->getListTableForeignKeysSQL($table, $database);
124 104
        $tableForeignKeys = $this->_conn->fetchAll($sql);
125
126 104
        if (! empty($tableForeignKeys)) {
127 11
            $createSql = $this->getCreateTableSQL($table);
128
129 11
            if (preg_match_all(
130
                '#
131
                    (?:CONSTRAINT\s+([^\s]+)\s+)?
132
                    (?:FOREIGN\s+KEY[^\)]+\)\s*)?
133
                    REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
134
                    (?:
135
                        [^,]*?
136
                        (NOT\s+DEFERRABLE|DEFERRABLE)
137
                        (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
138
                    )?#isx',
139 11
                $createSql,
140 11
                $match
141 11
            ) > 0) {
142 11
                $names      = array_reverse($match[1]);
143 11
                $deferrable = array_reverse($match[2]);
144 11
                $deferred   = array_reverse($match[3]);
145
            } else {
146
                $names = $deferrable = $deferred = [];
147
            }
148
149 11
            foreach ($tableForeignKeys as $key => $value) {
150 11
                $id                                        = $value['id'];
151 11
                $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && $names[$id] !== '' ? $names[$id] : $id;
152 11
                $tableForeignKeys[$key]['deferrable']      = isset($deferrable[$id]) && strtolower($deferrable[$id]) === 'deferrable';
153 11
                $tableForeignKeys[$key]['deferred']        = isset($deferred[$id]) && strtolower($deferred[$id]) === 'deferred';
154
            }
155
        }
156
157 104
        return $this->_getPortableTableForeignKeysList($tableForeignKeys);
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163 55
    protected function _getPortableTableDefinition(array $table) : string
164
    {
165 55
        return $table['name'];
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     *
171
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
172
     */
173 107
    protected function _getPortableTableIndexesList(array $tableIndexRows, string $tableName) : array
174
    {
175 107
        $indexBuffer = [];
176
177
        // fetch primary
178 107
        $stmt       = $this->_conn->executeQuery(sprintf(
179 2
            'PRAGMA TABLE_INFO (%s)',
180 107
            $this->_conn->quote($tableName)
181
        ));
182 107
        $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);
183
184
        usort($indexArray, static function ($a, $b) {
185 47
            if ($a['pk'] === $b['pk']) {
186 41
                return $a['cid'] - $b['cid'];
187
            }
188
189 17
            return $a['pk'] - $b['pk'];
190 107
        });
191 107
        foreach ($indexArray as $indexColumnRow) {
192 107
            if ($indexColumnRow['pk'] === '0') {
193 54
                continue;
194
            }
195
196 68
            $indexBuffer[] = [
197 68
                'key_name' => 'primary',
198
                'primary' => true,
199
                'non_unique' => false,
200 68
                'column_name' => $indexColumnRow['name'],
201
            ];
202
        }
203
204
        // fetch regular indexes
205 107
        foreach ($tableIndexRows as $tableIndex) {
206
            // Ignore indexes with reserved names, e.g. autoindexes
207 13
            if (strpos($tableIndex['name'], 'sqlite_') === 0) {
208 5
                continue;
209
            }
210
211 11
            $keyName           = $tableIndex['name'];
212 11
            $idx               = [];
213 11
            $idx['key_name']   = $keyName;
214 11
            $idx['primary']    = false;
215 11
            $idx['non_unique'] = ! $tableIndex['unique'];
216
217 11
                $stmt       = $this->_conn->executeQuery(sprintf(
218
                    'PRAGMA INDEX_INFO (%s)',
219 11
                    $this->_conn->quote($keyName)
220
                ));
221 11
                $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);
222
223 11
            foreach ($indexArray as $indexColumnRow) {
224 11
                $idx['column_name'] = $indexColumnRow['name'];
225 11
                $indexBuffer[]      = $idx;
226
            }
227
        }
228
229 107
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235 109
    protected function _getPortableTableColumnList(string $table, string $database, array $tableColumns) : array
236
    {
237 109
        $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
238
239
        // find column with autoincrement
240 109
        $autoincrementColumn = null;
241 109
        $autoincrementCount  = 0;
242
243 109
        foreach ($tableColumns as $tableColumn) {
244 109
            if ($tableColumn['pk'] === '0') {
245 57
                continue;
246
            }
247
248 67
            $autoincrementCount++;
249 67
            if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') {
250 3
                continue;
251
            }
252
253 67
            $autoincrementColumn = $tableColumn['name'];
254
        }
255
256 109
        if ($autoincrementCount === 1 && $autoincrementColumn !== null) {
0 ignored issues
show
introduced by
The condition $autoincrementColumn !== null is always false.
Loading history...
257 66
            foreach ($list as $column) {
258 66
                if ($autoincrementColumn !== $column->getName()) {
259 15
                    continue;
260
                }
261
262 66
                $column->setAutoincrement(true);
263
            }
264
        }
265
266
        // inspect column collation and comments
267 109
        $createSql = $this->getCreateTableSQL($table);
268
269 109
        foreach ($list as $columnName => $column) {
270 109
            $type = $column->getType();
271
272 109
            if ($type instanceof StringType || $type instanceof TextType) {
273 43
                $column->setPlatformOption(
274 43
                    'collation',
275 43
                    $this->parseColumnCollationFromSQL($columnName, $createSql) ?? 'BINARY'
276
                );
277
            }
278
279 109
            $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
280
281 109
            $type = $this->extractDoctrineTypeFromComment($comment);
282
283 109
            if ($type !== null) {
284 10
                $column->setType(Type::getType($type));
285
            }
286
287 109
            $column->setComment($comment);
288
        }
289
290 109
        return $list;
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296 109
    protected function _getPortableTableColumnDefinition(array $tableColumn) : Column
297
    {
298 109
        preg_match('/^([^()]*)\\s*(\\(((\\d+)(,\\s*(\\d+))?)\\))?/', $tableColumn['type'], $matches);
299
300 109
        $dbType = trim(strtolower($matches[1]));
301
302 109
        $length = $precision = $unsigned = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $unsigned is dead and can be removed.
Loading history...
303 109
        $fixed  = $unsigned = false;
304 109
        $scale  = 0;
305
306 109
        if (count($matches) >= 6) {
307 4
            $precision = (int) $matches[4];
308 4
            $scale     = (int) $matches[6];
309 109
        } elseif (count($matches) >= 4) {
310 42
            $length = (int) $matches[4];
311
        }
312
313 109
        if (strpos($dbType, ' unsigned') !== false) {
314 1
            $dbType   = str_replace(' unsigned', '', $dbType);
315 1
            $unsigned = true;
316
        }
317
318 109
        $type    = $this->_platform->getDoctrineTypeMapping($dbType);
319 109
        $default = $tableColumn['dflt_value'];
320 109
        if ($default === 'NULL') {
321 22
            $default = null;
322
        }
323
324 109
        if ($default !== null) {
325
            // SQLite returns the default value as a literal expression, so we need to parse it
326 26
            if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) {
327 25
                $default = str_replace("''", "'", $matches[1]);
328
            }
329
        }
330
331 109
        $notnull = (bool) $tableColumn['notnull'];
332
333 109
        if (! isset($tableColumn['name'])) {
334
            $tableColumn['name'] = '';
335
        }
336
337 109
        if ($dbType === 'char') {
338 3
            $fixed = true;
339
        }
340
341
        $options = [
342 109
            'length'   => $length,
343 109
            'unsigned' => $unsigned,
344 109
            'fixed'    => $fixed,
345 109
            'notnull'  => $notnull,
346 109
            'default'  => $default,
347 109
            'precision' => $precision,
348 109
            'scale'     => $scale,
349
            'autoincrement' => false,
350
        ];
351
352 109
        return new Column($tableColumn['name'], Type::getType($type), $options);
353
    }
354
355
    /**
356
     * {@inheritdoc}
357
     */
358 1
    protected function _getPortableViewDefinition(array $view) : View
359
    {
360 1
        return new View($view['name'], $view['sql']);
361
    }
362
363
    /**
364
     * {@inheritdoc}
365
     */
366 104
    protected function _getPortableTableForeignKeysList(array $tableForeignKeys) : array
367
    {
368 104
        $list = [];
369 104
        foreach ($tableForeignKeys as $value) {
370 11
            $value = array_change_key_case($value, CASE_LOWER);
371 11
            $name  = $value['constraint_name'];
372 11
            if (! isset($list[$name])) {
373 11
                if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') {
374
                    $value['on_delete'] = null;
375
                }
376
377 11
                if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') {
378
                    $value['on_update'] = null;
379
                }
380
381 11
                $list[$name] = [
382 11
                    'name' => $name,
383
                    'local' => [],
384
                    'foreign' => [],
385 11
                    'foreignTable' => $value['table'],
386 11
                    'onDelete' => $value['on_delete'],
387 11
                    'onUpdate' => $value['on_update'],
388 11
                    'deferrable' => $value['deferrable'],
389 11
                    'deferred'=> $value['deferred'],
390
                ];
391
            }
392
393 11
            $list[$name]['local'][]   = $value['from'];
394 11
            $list[$name]['foreign'][] = $value['to'];
395
        }
396
397 104
        $result = [];
398 104
        foreach ($list as $constraint) {
399 11
            $result[] = new ForeignKeyConstraint(
400 11
                array_values($constraint['local']),
401 11
                $constraint['foreignTable'],
402 11
                array_values($constraint['foreign']),
403 11
                $constraint['name'],
404
                [
405 11
                    'onDelete' => $constraint['onDelete'],
406 11
                    'onUpdate' => $constraint['onUpdate'],
407 11
                    'deferrable' => $constraint['deferrable'],
408 11
                    'deferred'=> $constraint['deferred'],
409
                ]
410
            );
411
        }
412
413 104
        return $result;
414
    }
415
416 2
    private function getTableDiffForAlterForeignKey(Table $table) : TableDiff
417
    {
418 2
        $tableDiff            = new TableDiff($table->getName());
419 2
        $tableDiff->fromTable = $table;
420
421 2
        return $tableDiff;
422
    }
423
424
    /**
425
     * @param string|Table $table
426
     */
427 2
    private function ensureTable($table) : Table
428
    {
429 2
        if (is_string($table)) {
430 2
            $table = $this->listTableDetails($table);
431
        }
432
433 2
        return $table;
434
    }
435
436 395
    private function parseColumnCollationFromSQL(string $column, string $sql) : ?string
437
    {
438 395
        $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
439 395
            . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
440
441 395
        if (preg_match($pattern, $sql, $match) !== 1) {
442 153
            return null;
443
        }
444
445 245
        return $match[1];
446
    }
447
448 102
    private function parseTableCommentFromSQL(string $table, string $sql) : ?string
449
    {
450
        $pattern = '/\s* # Allow whitespace characters at start of line
451
CREATE\sTABLE # Match "CREATE TABLE"
452 102
(?:\W"' . preg_quote($this->_platform->quoteSingleIdentifier($table), '/') . '"\W|\W' . preg_quote($table, '/')
453 102
            . '\W) # Match table name (quoted and unquoted)
454
( # Start capture
455
   (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s)
456
)/ix';
457
458 102
        if (preg_match($pattern, $sql, $match) !== 1) {
459 101
            return null;
460
        }
461
462 1
        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
463
464 1
        return $comment === '' ? null : $comment;
465
    }
466
467 593
    private function parseColumnCommentFromSQL(string $column, string $sql) : string
468
    {
469 593
        $pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column)) . '\W|\W' . preg_quote($column)
470 593
            . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
471
472 593
        if (preg_match($pattern, $sql, $match) !== 1) {
473 348
            return '';
474
        }
475
476 275
        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
477 275
        assert(is_string($comment));
478
479 275
        return $comment;
480
    }
481
482 111
    private function getCreateTableSQL(string $table) : string
483
    {
484 111
        $sql = $this->_conn->fetchColumn(
485
            <<<'SQL'
486 111
SELECT sql
487
  FROM (
488
      SELECT *
489
        FROM sqlite_master
490
   UNION ALL
491
      SELECT *
492
        FROM sqlite_temp_master
493
  )
494
WHERE type = 'table'
495
AND name = ?
496
SQL
497
            ,
498 111
            [$table]
499
        );
500
501 111
        if ($sql !== false) {
502 111
            return $sql;
503
        }
504
505
        return '';
506
    }
507
508 102
    public function listTableDetails(string $tableName) : Table
509
    {
510 102
        $table = parent::listTableDetails($tableName);
511
512 102
        $tableCreateSql = $this->getCreateTableSQL($tableName);
513
514 102
        $comment = $this->parseTableCommentFromSQL($tableName, $tableCreateSql);
515
516 102
        if ($comment !== null) {
517 1
            $table->addOption('comment', $comment);
518
        }
519
520 102
        return $table;
521
    }
522
}
523