Failed Conditions
Push — master ( 379085...b45ed5 )
by Marco
54s queued 28s
created

SqliteSchemaManager::renameTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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 explode;
17
use function file_exists;
18
use function preg_match;
19
use function preg_match_all;
20
use function preg_quote;
21
use function preg_replace;
22
use function rtrim;
23
use function sprintf;
24
use function str_replace;
25
use function strpos;
26
use function strtolower;
27
use function trim;
28
use function unlink;
29
use function usort;
30
31
/**
32
 * Sqlite SchemaManager.
33
 */
34
class SqliteSchemaManager extends AbstractSchemaManager
35
{
36
    /**
37
     * {@inheritdoc}
38
     */
39 2
    public function dropDatabase($database)
40
    {
41 2
        if (! file_exists($database)) {
42 1
            return;
43
        }
44
45 2
        unlink($database);
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    = 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 ($database === null) {
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(
121 1
                sprintf(
122 1
                    "SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '%s'",
123 1
                    $table
124
                )
125
            );
126 1
            $createSql = $createSql[0]['sql'] ?? '';
127
128 1
            if (preg_match_all(
129 1
                '#
130
                    (?:CONSTRAINT\s+([^\s]+)\s+)?
131
                    (?:FOREIGN\s+KEY[^\)]+\)\s*)?
132
                    REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
133
                    (?:
134
                        [^,]*?
135
                        (NOT\s+DEFERRABLE|DEFERRABLE)
136
                        (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
137
                    )?#isx',
138 1
                $createSql,
139 1
                $match
140
            )) {
141 1
                $names      = array_reverse($match[1]);
142 1
                $deferrable = array_reverse($match[2]);
143 1
                $deferred   = array_reverse($match[3]);
144
            } else {
145
                $names = $deferrable = $deferred = [];
146
            }
147
148 1
            foreach ($tableForeignKeys as $key => $value) {
149 1
                $id                                        = $value['id'];
150 1
                $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && $names[$id] !== '' ? $names[$id] : $id;
151 1
                $tableForeignKeys[$key]['deferrable']      = isset($deferrable[$id]) && strtolower($deferrable[$id]) === 'deferrable';
152 1
                $tableForeignKeys[$key]['deferred']        = isset($deferred[$id]) && strtolower($deferred[$id]) === 'deferred';
153
            }
154
        }
155
156 1
        return $this->_getPortableTableForeignKeysList($tableForeignKeys);
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 101
    protected function _getPortableTableDefinition($table)
163
    {
164 101
        return $table['name'];
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     *
170
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
171
     */
172 69
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
173
    {
174 69
        $indexBuffer = [];
175
176
        // fetch primary
177 69
        $stmt       = $this->_conn->executeQuery(
178 69
            sprintf("PRAGMA TABLE_INFO ('%s')", $tableName)
179
        );
180 69
        $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);
181
182
        usort($indexArray, static function ($a, $b) {
183 22
            if ($a['pk'] === $b['pk']) {
184 18
                return $a['cid'] - $b['cid'];
185
            }
186
187 12
            return $a['pk'] - $b['pk'];
188 69
        });
189 69
        foreach ($indexArray as $indexColumnRow) {
190 69
            if ($indexColumnRow['pk'] === '0') {
191 22
                continue;
192
            }
193
194 57
            $indexBuffer[] = [
195 57
                'key_name' => 'primary',
196
                'primary' => true,
197
                'non_unique' => false,
198 57
                'column_name' => $indexColumnRow['name'],
199
            ];
200
        }
201
202
        // fetch regular indexes
203 69
        foreach ($tableIndexes as $tableIndex) {
204
            // Ignore indexes with reserved names, e.g. autoindexes
205 8
            if (strpos($tableIndex['name'], 'sqlite_') === 0) {
206 5
                continue;
207
            }
208
209 6
            $keyName           = $tableIndex['name'];
210 6
            $idx               = [];
211 6
            $idx['key_name']   = $keyName;
212 6
            $idx['primary']    = false;
213 6
            $idx['non_unique'] = $tableIndex['unique']?false:true;
214
215 6
            $stmt       = $this->_conn->executeQuery(
216 6
                sprintf("PRAGMA INDEX_INFO ('%s')", $keyName)
217
            );
218 6
            $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);
219
220 6
            foreach ($indexArray as $indexColumnRow) {
221 6
                $idx['column_name'] = $indexColumnRow['name'];
222 6
                $indexBuffer[]      = $idx;
223
            }
224
        }
225
226 69
        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 76
    protected function _getPortableTableColumnList($table, $database, $tableColumns)
244
    {
245 76
        $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
246
247
        // find column with autoincrement
248 76
        $autoincrementColumn = null;
249 76
        $autoincrementCount  = 0;
250
251 76
        foreach ($tableColumns as $tableColumn) {
252 76
            if ($tableColumn['pk'] === '0') {
253 28
                continue;
254
            }
255
256 60
            $autoincrementCount++;
257 60
            if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') {
258 3
                continue;
259
            }
260
261 60
            $autoincrementColumn = $tableColumn['name'];
262
        }
263
264 76
        if ($autoincrementCount === 1 && $autoincrementColumn !== null) {
0 ignored issues
show
introduced by
The condition $autoincrementColumn !== null is always false.
Loading history...
265 59
            foreach ($list as $column) {
266 59
                if ($autoincrementColumn !== $column->getName()) {
267 12
                    continue;
268
                }
269
270 59
                $column->setAutoincrement(true);
271
            }
272
        }
273
274
        // inspect column collation and comments
275 76
        $createSql = $this->_conn->fetchAll(
276 76
            sprintf(
277 76
                "SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '%s'",
278 76
                $table
279
            )
280
        );
281 76
        $createSql = $createSql[0]['sql'] ?? '';
282
283 76
        foreach ($list as $columnName => $column) {
284 76
            $type = $column->getType();
285
286 76
            if ($type instanceof StringType || $type instanceof TextType) {
287 16
                $column->setPlatformOption('collation', $this->parseColumnCollationFromSQL($columnName, $createSql) ?: 'BINARY');
288
            }
289
290 76
            $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
291
292 76
            if ($comment === null) {
293 71
                continue;
294
            }
295
296 16
            $type = $this->extractDoctrineTypeFromComment($comment, null);
297
298 16
            if ($type !== null) {
299 4
                $column->setType(Type::getType($type));
300
301 4
                $comment = $this->removeDoctrineTypeFromComment($comment, $type);
302
            }
303
304 16
            $column->setComment($comment);
305
        }
306
307 76
        return $list;
308
    }
309
310
    /**
311
     * {@inheritdoc}
312
     */
313 76
    protected function _getPortableTableColumnDefinition($tableColumn)
314
    {
315 76
        $parts               = explode('(', $tableColumn['type']);
316 76
        $tableColumn['type'] = trim($parts[0]);
317 76
        if (isset($parts[1])) {
318 12
            $length                = trim($parts[1], ')');
319 12
            $tableColumn['length'] = $length;
320
        }
321
322 76
        $dbType   = strtolower($tableColumn['type']);
323 76
        $length   = $tableColumn['length'] ?? null;
324 76
        $unsigned = false;
325
326 76
        if (strpos($dbType, ' unsigned') !== false) {
327 1
            $dbType   = str_replace(' unsigned', '', $dbType);
328 1
            $unsigned = true;
329
        }
330
331 76
        $fixed   = false;
332 76
        $type    = $this->_platform->getDoctrineTypeMapping($dbType);
333 76
        $default = $tableColumn['dflt_value'];
334 76
        if ($default === 'NULL') {
335 4
            $default = null;
336
        }
337 76
        if ($default !== null) {
338
            // SQLite returns strings wrapped in single quotes, so we need to strip them
339 6
            $default = preg_replace("/^'(.*)'$/", '\1', $default);
340
        }
341 76
        $notnull = (bool) $tableColumn['notnull'];
342
343 76
        if (! isset($tableColumn['name'])) {
344
            $tableColumn['name'] = '';
345
        }
346
347 76
        $precision = null;
348 76
        $scale     = null;
349
350 2
        switch ($dbType) {
351 76
            case 'char':
352 3
                $fixed = true;
353 3
                break;
354 75
            case 'float':
355 75
            case 'double':
356 75
            case 'real':
357 75
            case 'decimal':
358 75
            case 'numeric':
359 4
                if (isset($tableColumn['length'])) {
360 4
                    if (strpos($tableColumn['length'], ',') === false) {
361
                        $tableColumn['length'] .= ',0';
362
                    }
363 4
                    [$precision, $scale] = array_map('trim', explode(',', $tableColumn['length']));
364
                }
365 4
                $length = null;
366 4
                break;
367
        }
368
369
        $options = [
370 76
            'length'   => $length,
371 76
            'unsigned' => (bool) $unsigned,
372 76
            'fixed'    => $fixed,
373 76
            'notnull'  => $notnull,
374 76
            'default'  => $default,
375 76
            'precision' => $precision,
376 76
            'scale'     => $scale,
377
            'autoincrement' => false,
378
        ];
379
380 76
        return new Column($tableColumn['name'], Type::getType($type), $options);
381
    }
382
383
    /**
384
     * {@inheritdoc}
385
     */
386 1
    protected function _getPortableViewDefinition($view)
387
    {
388 1
        return new View($view['name'], $view['sql']);
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     */
394 1
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
395
    {
396 1
        $list = [];
397 1
        foreach ($tableForeignKeys as $value) {
398 1
            $value = array_change_key_case($value, CASE_LOWER);
399 1
            $name  = $value['constraint_name'];
400 1
            if (! isset($list[$name])) {
401 1
                if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') {
402
                    $value['on_delete'] = null;
403
                }
404 1
                if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') {
405
                    $value['on_update'] = null;
406
                }
407
408 1
                $list[$name] = [
409 1
                    'name' => $name,
410
                    'local' => [],
411
                    'foreign' => [],
412 1
                    'foreignTable' => $value['table'],
413 1
                    'onDelete' => $value['on_delete'],
414 1
                    'onUpdate' => $value['on_update'],
415 1
                    'deferrable' => $value['deferrable'],
416 1
                    'deferred'=> $value['deferred'],
417
                ];
418
            }
419 1
            $list[$name]['local'][]   = $value['from'];
420 1
            $list[$name]['foreign'][] = $value['to'];
421
        }
422
423 1
        $result = [];
424 1
        foreach ($list as $constraint) {
425 1
            $result[] = new ForeignKeyConstraint(
426 1
                array_values($constraint['local']),
427 1
                $constraint['foreignTable'],
428 1
                array_values($constraint['foreign']),
429 1
                $constraint['name'],
430
                [
431 1
                    'onDelete' => $constraint['onDelete'],
432 1
                    'onUpdate' => $constraint['onUpdate'],
433 1
                    'deferrable' => $constraint['deferrable'],
434 1
                    'deferred'=> $constraint['deferred'],
435
                ]
436
            );
437
        }
438
439 1
        return $result;
440
    }
441
442
    /**
443
     * @param Table|string $table
444
     *
445
     * @return TableDiff
446
     *
447
     * @throws DBALException
448
     */
449
    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

449
    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...
450
    {
451
        if (! $table instanceof Table) {
452
            $tableDetails = $this->tryMethod('listTableDetails', $table);
453
            if ($table === false) {
0 ignored issues
show
introduced by
The condition $table === false is always false.
Loading history...
454
                throw new DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table));
455
            }
456
457
            $table = $tableDetails;
458
        }
459
460
        $tableDiff            = new TableDiff($table->getName());
461
        $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...
462
463
        return $tableDiff;
464
    }
465
466 320
    private function parseColumnCollationFromSQL(string $column, string $sql) : ?string
467
    {
468 320
        $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
469 320
            . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
470
471 320
        if (preg_match($pattern, $sql, $match) !== 1) {
472 111
            return null;
473
        }
474
475 212
        return $match[1];
476
    }
477
478 494
    private function parseColumnCommentFromSQL(string $column, string $sql) : ?string
479
    {
480 494
        $pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column)) . '\W|\W' . preg_quote($column)
481 494
            . '\W)(?:\(.*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
482
483 494
        if (preg_match($pattern, $sql, $match) !== 1) {
484 280
            return null;
485
        }
486
487 225
        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
488
489 225
        return $comment === '' ? null : $comment;
490
    }
491
}
492