Failed Conditions
Pull Request — develop (#3518)
by Michael
29:00 queued 25:29
created

SqliteSchemaManager::renameTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 10
c 0
b 0
f 0
ccs 0
cts 5
cp 0
cc 1
nc 1
nop 2
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Schema;
6
7
use Doctrine\DBAL\DBALException;
8
use Doctrine\DBAL\DriverManager;
9
use Doctrine\DBAL\FetchMode;
10
use Doctrine\DBAL\Types\StringType;
11
use Doctrine\DBAL\Types\TextType;
12
use Doctrine\DBAL\Types\Type;
13
use const CASE_LOWER;
14
use function array_change_key_case;
15
use function array_map;
16
use function array_reverse;
17
use function array_values;
18
use function explode;
19
use function file_exists;
20
use function preg_match;
21
use function preg_match_all;
22
use function preg_quote;
23
use function preg_replace;
24
use function rtrim;
25
use function sprintf;
26
use function str_replace;
27
use function strpos;
28
use function strtolower;
29
use function trim;
30
use function unlink;
31
use function usort;
32
33
/**
34
 * Sqlite SchemaManager.
35
 */
36
class SqliteSchemaManager extends AbstractSchemaManager
37
{
38
    /**
39
     * {@inheritdoc}
40
     */
41
    public function dropDatabase($database)
42
    {
43
        if (! file_exists($database)) {
44
            return;
45
        }
46
47
        unlink($database);
48
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    public function createDatabase($database)
54
    {
55
        $params  = $this->_conn->getParams();
56
        $driver  = $params['driver'];
57
        $options = [
58
            'driver' => $driver,
59
            'path' => $database,
60
        ];
61
        $conn    = DriverManager::getConnection($options);
62
        $conn->connect();
63
        $conn->close();
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function renameTable($name, $newName)
70
    {
71
        $tableDiff            = new TableDiff($name);
72
        $tableDiff->fromTable = $this->listTableDetails($name);
73
        $tableDiff->newName   = $newName;
74
        $this->alterTable($tableDiff);
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
81
    {
82
        $tableDiff                     = $this->getTableDiffForAlterForeignKey($table);
83
        $tableDiff->addedForeignKeys[] = $foreignKey;
84
85
        $this->alterTable($tableDiff);
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
92
    {
93
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
94
        $tableDiff->changedForeignKeys[] = $foreignKey;
95
96
        $this->alterTable($tableDiff);
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function dropForeignKey($foreignKey, $table)
103
    {
104
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
105
        $tableDiff->removedForeignKeys[] = $foreignKey;
106
107
        $this->alterTable($tableDiff);
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113
    public function listTableForeignKeys($table, $database = null)
114
    {
115
        if ($database === null) {
116
            $database = $this->_conn->getDatabase();
117
        }
118
        $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

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