Passed
Pull Request — master (#2920)
by Luís
09:30
created

SqliteSchemaManager   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 440
Duplicated Lines 0 %

Test Coverage

Coverage 84.91%

Importance

Changes 0
Metric Value
wmc 77
dl 0
loc 440
ccs 180
cts 212
cp 0.8491
rs 2.1739
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A dropForeignKey() 0 6 1
A dropDatabase() 0 4 2
C _getPortableTableColumnList() 0 52 16
A dropAndCreateForeignKey() 0 6 1
C listTableForeignKeys() 0 39 12
C _getPortableTableIndexesList() 0 47 8
A createForeignKey() 0 6 1
F _getPortableTableColumnDefinition() 0 68 15
A renameTable() 0 6 1
A _getPortableTableIndexDefinition() 0 5 1
A parseColumnCollationFromSQL() 0 11 2
A createDatabase() 0 11 1
C _getPortableTableForeignKeysList() 0 44 8
A parseColumnCommentFromSQL() 0 13 3
A _getPortableViewDefinition() 0 3 1
A getTableDiffForAlterForeignKey() 0 15 3
A _getPortableTableDefinition() 0 3 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
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Schema;
21
22
use Doctrine\DBAL\DBALException;
23
use Doctrine\DBAL\Types\StringType;
24
use Doctrine\DBAL\Types\TextType;
25
use Doctrine\DBAL\Types\Type;
26
27
/**
28
 * Sqlite SchemaManager.
29
 *
30
 * @author Konsta Vesterinen <[email protected]>
31
 * @author Lukas Smith <[email protected]> (PEAR MDB2 library)
32
 * @author Jonathan H. Wage <[email protected]>
33
 * @author Martin Hasoň <[email protected]>
34
 * @since  2.0
35
 */
36
class SqliteSchemaManager extends AbstractSchemaManager
37
{
38
    /**
39
     * {@inheritdoc}
40
     */
41 2
    public function dropDatabase($database)
42
    {
43 2
        if (file_exists($database)) {
44 2
            unlink($database);
45
        }
46 2
    }
47
48
    /**
49
     * {@inheritdoc}
50
     */
51 2
    public function createDatabase($database)
52
    {
53 2
        $params = $this->_conn->getParams();
54 2
        $driver = $params['driver'];
55
        $options = [
56 2
            'driver' => $driver,
57 2
            'path' => $database
58
        ];
59 2
        $conn = \Doctrine\DBAL\DriverManager::getConnection($options);
60 2
        $conn->connect();
61 2
        $conn->close();
62 2
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 1
    public function renameTable($name, $newName)
68
    {
69 1
        $tableDiff = new TableDiff($name);
70 1
        $tableDiff->fromTable = $this->listTableDetails($name);
71 1
        $tableDiff->newName = $newName;
72 1
        $this->alterTable($tableDiff);
73 1
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
79
    {
80
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
81
        $tableDiff->addedForeignKeys[] = $foreignKey;
82
83
        $this->alterTable($tableDiff);
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
90
    {
91
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $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($foreignKey, $table);
103
        $tableDiff->removedForeignKeys[] = $foreignKey;
104
105
        $this->alterTable($tableDiff);
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 1
    public function listTableForeignKeys($table, $database = null)
112
    {
113 1
        if (null === $database) {
114 1
            $database = $this->_conn->getDatabase();
115
        }
116 1
        $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

116
        /** @scrutinizer ignore-call */ 
117
        $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...
117 1
        $tableForeignKeys = $this->_conn->fetchAll($sql);
118
119 1
        if ( ! empty($tableForeignKeys)) {
120 1
            $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'");
121 1
            $createSql = isset($createSql[0]['sql']) ? $createSql[0]['sql'] : '';
122
123 1
            if (preg_match_all('#
124
                    (?:CONSTRAINT\s+([^\s]+)\s+)?
125
                    (?:FOREIGN\s+KEY[^\)]+\)\s*)?
126
                    REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
127
                    (?:
128
                        [^,]*?
129
                        (NOT\s+DEFERRABLE|DEFERRABLE)
130
                        (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
131
                    )?#isx',
132 1
                    $createSql, $match)) {
133
134 1
                $names = array_reverse($match[1]);
135 1
                $deferrable = array_reverse($match[2]);
136 1
                $deferred = array_reverse($match[3]);
137
            } else {
138
                $names = $deferrable = $deferred = [];
139
            }
140
141 1
            foreach ($tableForeignKeys as $key => $value) {
142 1
                $id = $value['id'];
143 1
                $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && '' != $names[$id] ? $names[$id] : $id;
144 1
                $tableForeignKeys[$key]['deferrable'] = isset($deferrable[$id]) && 'deferrable' == strtolower($deferrable[$id]) ? true : false;
145 1
                $tableForeignKeys[$key]['deferred'] = isset($deferred[$id]) && 'deferred' == strtolower($deferred[$id]) ? true : false;
146
            }
147
        }
148
149 1
        return $this->_getPortableTableForeignKeysList($tableForeignKeys);
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155 13
    protected function _getPortableTableDefinition($table)
156
    {
157 13
        return $table['name'];
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     *
163
     * @license New BSD License
164
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
165
     */
166 31
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
167
    {
168 31
        $indexBuffer = [];
169
170
        // fetch primary
171 31
        $stmt = $this->_conn->executeQuery("PRAGMA TABLE_INFO ('$tableName')");
172 31
        $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC);
173
174 31
        usort($indexArray, function($a, $b) {
175 22
            if ($a['pk'] == $b['pk']) {
176 18
                return $a['cid'] - $b['cid'];
177
            }
178
179 12
            return $a['pk'] - $b['pk'];
180 31
        });
181 31
        foreach ($indexArray as $indexColumnRow) {
182 31
            if ($indexColumnRow['pk'] != "0") {
183 21
                $indexBuffer[] = [
184 21
                    'key_name' => 'primary',
185
                    'primary' => true,
186
                    'non_unique' => false,
187 31
                    'column_name' => $indexColumnRow['name']
188
                ];
189
            }
190
        }
191
192
        // fetch regular indexes
193 31
        foreach ($tableIndexes as $tableIndex) {
194
            // Ignore indexes with reserved names, e.g. autoindexes
195 8
            if (strpos($tableIndex['name'], 'sqlite_') !== 0) {
196 6
                $keyName = $tableIndex['name'];
197 6
                $idx = [];
198 6
                $idx['key_name'] = $keyName;
199 6
                $idx['primary'] = false;
200 6
                $idx['non_unique'] = $tableIndex['unique']?false:true;
201
202 6
                $stmt = $this->_conn->executeQuery("PRAGMA INDEX_INFO ('{$keyName}')");
203 6
                $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC);
204
205 6
                foreach ($indexArray as $indexColumnRow) {
206 6
                    $idx['column_name'] = $indexColumnRow['name'];
207 8
                    $indexBuffer[] = $idx;
208
                }
209
            }
210
        }
211
212 31
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    protected function _getPortableTableIndexDefinition($tableIndex)
219
    {
220
        return [
221
            'name' => $tableIndex['name'],
222
            'unique' => (bool) $tableIndex['unique']
223
        ];
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229 38
    protected function _getPortableTableColumnList($table, $database, $tableColumns)
230
    {
231 38
        $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
232
233
        // find column with autoincrement
234 38
        $autoincrementColumn = null;
235 38
        $autoincrementCount = 0;
236
237 38
        foreach ($tableColumns as $tableColumn) {
238 38
            if ('0' != $tableColumn['pk']) {
239 24
                $autoincrementCount++;
240 24
                if (null === $autoincrementColumn && 'integer' == strtolower($tableColumn['type'])) {
241 38
                    $autoincrementColumn = $tableColumn['name'];
242
                }
243
            }
244
        }
245
246 38
        if (1 == $autoincrementCount && null !== $autoincrementColumn) {
247 23
            foreach ($list as $column) {
248 23
                if ($autoincrementColumn == $column->getName()) {
249 23
                    $column->setAutoincrement(true);
250
                }
251
            }
252
        }
253
254
        // inspect column collation and comments
255 38
        $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'");
256 38
        $createSql = isset($createSql[0]['sql']) ? $createSql[0]['sql'] : '';
257
258 38
        foreach ($list as $columnName => $column) {
259 38
            $type = $column->getType();
260
261 38
            if ($type instanceof StringType || $type instanceof TextType) {
262 14
                $column->setPlatformOption('collation', $this->parseColumnCollationFromSQL($columnName, $createSql) ?: 'BINARY');
263
            }
264
265 38
            $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
266
267 38
            if (false !== $comment) {
268 14
                $type = $this->extractDoctrineTypeFromComment($comment, null);
269
270 14
                if (null !== $type) {
271 2
                    $column->setType(Type::getType($type));
272
273 2
                    $comment = $this->removeDoctrineTypeFromComment($comment, $type);
274
                }
275
276 38
                $column->setComment($comment);
277
            }
278
        }
279
280 38
        return $list;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286 38
    protected function _getPortableTableColumnDefinition($tableColumn)
287
    {
288 38
        $parts = explode('(', $tableColumn['type']);
289 38
        $tableColumn['type'] = trim($parts[0]);
290 38
        if (isset($parts[1])) {
291 12
            $length = trim($parts[1], ')');
292 12
            $tableColumn['length'] = $length;
293
        }
294
295 38
        $dbType = strtolower($tableColumn['type']);
296 38
        $length = isset($tableColumn['length']) ? $tableColumn['length'] : null;
297 38
        $unsigned = false;
298
299 38
        if (strpos($dbType, ' unsigned') !== false) {
300 1
            $dbType = str_replace(' unsigned', '', $dbType);
301 1
            $unsigned = true;
302
        }
303
304 38
        $fixed = false;
305 38
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
306 38
        $default = $tableColumn['dflt_value'];
307 38
        if ($default == 'NULL') {
308 4
            $default = null;
309
        }
310 38
        if ($default !== null) {
311
            // SQLite returns strings wrapped in single quotes, so we need to strip them
312 6
            $default = preg_replace("/^'(.*)'$/", '\1', $default);
313
        }
314 38
        $notnull = (bool) $tableColumn['notnull'];
315
316 38
        if ( ! isset($tableColumn['name'])) {
317
            $tableColumn['name'] = '';
318
        }
319
320 38
        $precision = null;
321 38
        $scale = null;
322
323
        switch ($dbType) {
324 38
            case 'char':
325 4
                $fixed = true;
326 4
                break;
327 37
            case 'float':
328 37
            case 'double':
329 37
            case 'real':
330 37
            case 'decimal':
331 37
            case 'numeric':
332 4
                if (isset($tableColumn['length'])) {
333 4
                    if (strpos($tableColumn['length'], ',') === false) {
334
                        $tableColumn['length'] .= ",0";
335
                    }
336 4
                    list($precision, $scale) = array_map('trim', explode(',', $tableColumn['length']));
337
                }
338 4
                $length = null;
339 4
                break;
340
        }
341
342
        $options = [
343 38
            'length'   => $length,
344 38
            'unsigned' => (bool) $unsigned,
345 38
            'fixed'    => $fixed,
346 38
            'notnull'  => $notnull,
347 38
            'default'  => $default,
348 38
            'precision' => $precision,
349 38
            'scale'     => $scale,
350
            'autoincrement' => false,
351
        ];
352
353 38
        return new Column($tableColumn['name'], \Doctrine\DBAL\Types\Type::getType($type), $options);
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359 1
    protected function _getPortableViewDefinition($view)
360
    {
361 1
        return new View($view['name'], $view['sql']);
362
    }
363
364
    /**
365
     * {@inheritdoc}
366
     */
367 1
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
368
    {
369 1
        $list = [];
370 1
        foreach ($tableForeignKeys as $value) {
371 1
            $value = array_change_key_case($value, CASE_LOWER);
372 1
            $name = $value['constraint_name'];
373 1
            if ( ! isset($list[$name])) {
374 1
                if ( ! isset($value['on_delete']) || $value['on_delete'] == "RESTRICT") {
375
                    $value['on_delete'] = null;
376
                }
377 1
                if ( ! isset($value['on_update']) || $value['on_update'] == "RESTRICT") {
378
                    $value['on_update'] = null;
379
                }
380
381 1
                $list[$name] = [
382 1
                    'name' => $name,
383
                    'local' => [],
384
                    'foreign' => [],
385 1
                    'foreignTable' => $value['table'],
386 1
                    'onDelete' => $value['on_delete'],
387 1
                    'onUpdate' => $value['on_update'],
388 1
                    'deferrable' => $value['deferrable'],
389 1
                    'deferred'=> $value['deferred'],
390
                ];
391
            }
392 1
            $list[$name]['local'][] = $value['from'];
393 1
            $list[$name]['foreign'][] = $value['to'];
394
        }
395
396 1
        $result = [];
397 1
        foreach ($list as $constraint) {
398 1
            $result[] = new ForeignKeyConstraint(
399 1
                array_values($constraint['local']), $constraint['foreignTable'],
400 1
                array_values($constraint['foreign']), $constraint['name'],
401
                [
402 1
                    'onDelete' => $constraint['onDelete'],
403 1
                    'onUpdate' => $constraint['onUpdate'],
404 1
                    'deferrable' => $constraint['deferrable'],
405 1
                    'deferred'=> $constraint['deferred'],
406
                ]
407
            );
408
        }
409
410 1
        return $result;
411
    }
412
413
    /**
414
     * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey
415
     * @param \Doctrine\DBAL\Schema\Table|string         $table
416
     *
417
     * @return \Doctrine\DBAL\Schema\TableDiff
418
     *
419
     * @throws \Doctrine\DBAL\DBALException
420
     */
421
    private function getTableDiffForAlterForeignKey(ForeignKeyConstraint $foreignKey, $table)
0 ignored issues
show
Unused Code introduced by
The parameter $foreignKey is not used and could be removed. ( Ignorable by Annotation )

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

421
    private function getTableDiffForAlterForeignKey(/** @scrutinizer ignore-unused */ ForeignKeyConstraint $foreignKey, $table)

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

Loading history...
422
    {
423
        if ( ! $table instanceof Table) {
424
            $tableDetails = $this->tryMethod('listTableDetails', $table);
425
            if (false === $table) {
426
                throw new DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table));
427
            }
428
429
            $table = $tableDetails;
430
        }
431
432
        $tableDiff = new TableDiff($table->getName());
433
        $tableDiff->fromTable = $table;
0 ignored issues
show
Documentation Bug introduced by
It seems like $table can also be of type false. However, the property $fromTable is declared as type Doctrine\DBAL\Schema\Table. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
434
435
        return $tableDiff;
436
    }
437
438
    /**
439
     * @param string $column
440
     * @param string $sql
441
     *
442
     * @return string|false
443
     */
444 19
    private function parseColumnCollationFromSQL($column, $sql)
445
    {
446 19
        if (preg_match(
447 19
            '{(?:'.preg_quote($column).'|'.preg_quote($this->_platform->quoteSingleIdentifier($column)).')
448
                [^,(]+(?:\([^()]+\)[^,]*)?
449
                (?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*
450 19
                COLLATE\s+["\']?([^\s,"\')]+)}isx', $sql, $match)) {
451 7
            return $match[1];
452
        }
453
454 15
        return false;
455
    }
456
457
    /**
458
     * @param string $column
459
     * @param string $sql
460
     *
461
     * @return string|false
462
     */
463 38
    private function parseColumnCommentFromSQL($column, $sql)
464
    {
465 38
        if (preg_match(
466 38
            '{[\s(,](?:'.preg_quote($this->_platform->quoteSingleIdentifier($column)).'|'.preg_quote($column).')
467
            (?:\(.*?\)|[^,(])*?,?((?:\s*--[^\n]*\n?)+)
468 38
            }isx', $sql, $match
469
        )) {
470 14
            $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
471
472 14
            return '' === $comment ? false : $comment;
473
        }
474
475 35
        return false;
476
    }
477
}
478