Completed
Push — master ( cc3868...bfc8bb )
by Marco
21s queued 15s
created

SqliteSchemaManager   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 481
Duplicated Lines 0 %

Test Coverage

Coverage 85.15%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 73
eloc 226
dl 0
loc 481
ccs 195
cts 229
cp 0.8515
rs 2.56
c 3
b 0
f 0

20 Methods

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

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