SqliteSchemaManager.php 16.1 KB
Newer Older
romanb's avatar
romanb committed
1 2
<?php

3
namespace Doctrine\DBAL\Schema;
romanb's avatar
romanb committed
4

5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\DriverManager;
7 8
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
9
use Doctrine\DBAL\Types\Type;
10

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
use function array_change_key_case;
use function array_map;
use function array_reverse;
use function array_values;
use function explode;
use function file_exists;
use function preg_match;
use function preg_match_all;
use function preg_quote;
use function preg_replace;
use function rtrim;
use function sprintf;
use function str_replace;
use function strpos;
use function strtolower;
use function trim;
use function unlink;
use function usort;
29

Grégoire Paris's avatar
Grégoire Paris committed
30
use const CASE_LOWER;
31

romanb's avatar
romanb committed
32
/**
Benjamin Morel's avatar
Benjamin Morel committed
33
 * Sqlite SchemaManager.
romanb's avatar
romanb committed
34
 */
35
class SqliteSchemaManager extends AbstractSchemaManager
36
{
romanb's avatar
romanb committed
37 38 39 40
    /**
     * {@inheritdoc}
     */
    public function dropDatabase($database)
41
    {
42 43
        if (! file_exists($database)) {
            return;
44
        }
45 46

        unlink($database);
47 48
    }

romanb's avatar
romanb committed
49 50 51 52
    /**
     * {@inheritdoc}
     */
    public function createDatabase($database)
53
    {
54 55
        $params  = $this->_conn->getParams();
        $driver  = $params['driver'];
56
        $options = [
jwage's avatar
jwage committed
57
            'driver' => $driver,
58
            'path' => $database,
59
        ];
60
        $conn    = DriverManager::getConnection($options);
jwage's avatar
jwage committed
61 62
        $conn->connect();
        $conn->close();
63 64
    }

65 66 67 68 69
    /**
     * {@inheritdoc}
     */
    public function renameTable($name, $newName)
    {
70
        $tableDiff            = new TableDiff($name);
71
        $tableDiff->fromTable = $this->listTableDetails($name);
72
        $tableDiff->newName   = $newName;
73 74 75 76 77 78 79 80
        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
Sergei Morozov's avatar
Sergei Morozov committed
81
        $tableDiff                     = $this->getTableDiffForAlterForeignKey($table);
82 83 84 85 86 87 88 89 90 91
        $tableDiff->addedForeignKeys[] = $foreignKey;

        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
Sergei Morozov's avatar
Sergei Morozov committed
92
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
93 94 95 96 97 98 99 100 101 102
        $tableDiff->changedForeignKeys[] = $foreignKey;

        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function dropForeignKey($foreignKey, $table)
    {
Sergei Morozov's avatar
Sergei Morozov committed
103
        $tableDiff                       = $this->getTableDiffForAlterForeignKey($table);
104 105 106 107 108 109 110 111 112 113
        $tableDiff->removedForeignKeys[] = $foreignKey;

        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function listTableForeignKeys($table, $database = null)
    {
114
        if ($database === null) {
115 116
            $database = $this->_conn->getDatabase();
        }
Grégoire Paris's avatar
Grégoire Paris committed
117

118
        $sql              = $this->_platform->getListTableForeignKeysSQL($table, $database);
119
        $tableForeignKeys = $this->_conn->fetchAllAssociative($sql);
120

121
        if (! empty($tableForeignKeys)) {
122
            $createSql = $this->getCreateTableSQL($table);
123

124 125 126
            if (
                $createSql !== null && preg_match_all(
                    '#
127 128 129 130 131 132 133 134
                    (?:CONSTRAINT\s+([^\s]+)\s+)?
                    (?:FOREIGN\s+KEY[^\)]+\)\s*)?
                    REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
                    (?:
                        [^,]*?
                        (NOT\s+DEFERRABLE|DEFERRABLE)
                        (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
                    )?#isx',
135 136 137 138
                    $createSql,
                    $match
                )
            ) {
139
                $names      = array_reverse($match[1]);
140
                $deferrable = array_reverse($match[2]);
141
                $deferred   = array_reverse($match[3]);
142
            } else {
143
                $names = $deferrable = $deferred = [];
144 145 146
            }

            foreach ($tableForeignKeys as $key => $value) {
147 148
                $id                                        = $value['id'];
                $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && $names[$id] !== '' ? $names[$id] : $id;
149 150
                $tableForeignKeys[$key]['deferrable']      = isset($deferrable[$id]) && strtolower($deferrable[$id]) === 'deferrable';
                $tableForeignKeys[$key]['deferred']        = isset($deferred[$id]) && strtolower($deferred[$id]) === 'deferred';
151 152 153 154 155 156
            }
        }

        return $this->_getPortableTableForeignKeysList($tableForeignKeys);
    }

Benjamin Morel's avatar
Benjamin Morel committed
157 158 159
    /**
     * {@inheritdoc}
     */
160 161 162 163 164
    protected function _getPortableTableDefinition($table)
    {
        return $table['name'];
    }

165
    /**
Benjamin Morel's avatar
Benjamin Morel committed
166 167
     * {@inheritdoc}
     *
168 169
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
170
    protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
171
    {
172
        $indexBuffer = [];
173 174

        // fetch primary
175
        $indexArray = $this->_conn->fetchAllAssociative(sprintf(
176 177 178
            'PRAGMA TABLE_INFO (%s)',
            $this->_conn->quote($tableName)
        ));
179

180 181
        usort($indexArray, static function ($a, $b) {
            if ($a['pk'] === $b['pk']) {
182 183
                return $a['cid'] - $b['cid'];
            }
184

185 186
            return $a['pk'] - $b['pk'];
        });
Steve Müller's avatar
Steve Müller committed
187
        foreach ($indexArray as $indexColumnRow) {
188 189
            if ($indexColumnRow['pk'] === '0') {
                continue;
190
            }
191 192 193 194 195 196 197

            $indexBuffer[] = [
                'key_name' => 'primary',
                'primary' => true,
                'non_unique' => false,
                'column_name' => $indexColumnRow['name'],
            ];
198 199 200
        }

        // fetch regular indexes
Steve Müller's avatar
Steve Müller committed
201
        foreach ($tableIndexes as $tableIndex) {
202
            // Ignore indexes with reserved names, e.g. autoindexes
203 204 205 206 207 208 209 210
            if (strpos($tableIndex['name'], 'sqlite_') === 0) {
                continue;
            }

            $keyName           = $tableIndex['name'];
            $idx               = [];
            $idx['key_name']   = $keyName;
            $idx['primary']    = false;
211
            $idx['non_unique'] = ! $tableIndex['unique'];
212

213 214 215 216
            $indexArray = $this->_conn->fetchAllAssociative(sprintf(
                'PRAGMA INDEX_INFO (%s)',
                $this->_conn->quote($keyName)
            ));
217 218 219 220

            foreach ($indexArray as $indexColumnRow) {
                $idx['column_name'] = $indexColumnRow['name'];
                $indexBuffer[]      = $idx;
221 222 223 224 225 226
            }
        }

        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
    }

Benjamin Morel's avatar
Benjamin Morel committed
227
    /**
228
     * @deprecated
229
     *
230 231
     * @param array<string, mixed> $tableIndex
     *
232
     * @return array<string, bool|string>
Benjamin Morel's avatar
Benjamin Morel committed
233
     */
234 235
    protected function _getPortableTableIndexDefinition($tableIndex)
    {
236
        return [
237
            'name' => $tableIndex['name'],
238
            'unique' => (bool) $tableIndex['unique'],
239
        ];
240 241
    }

Benjamin Morel's avatar
Benjamin Morel committed
242 243 244
    /**
     * {@inheritdoc}
     */
245 246 247
    protected function _getPortableTableColumnList($table, $database, $tableColumns)
    {
        $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
248 249

        // find column with autoincrement
250
        $autoincrementColumn = null;
251
        $autoincrementCount  = 0;
252

253
        foreach ($tableColumns as $tableColumn) {
254 255 256 257 258 259 260
            if ($tableColumn['pk'] === '0') {
                continue;
            }

            $autoincrementCount++;
            if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') {
                continue;
261
            }
262 263

            $autoincrementColumn = $tableColumn['name'];
264 265
        }

266
        if ($autoincrementCount === 1 && $autoincrementColumn !== null) {
267
            foreach ($list as $column) {
268 269
                if ($autoincrementColumn !== $column->getName()) {
                    continue;
270
                }
271 272

                $column->setAutoincrement(true);
273 274 275
            }
        }

276
        // inspect column collation and comments
277
        $createSql = $this->getCreateTableSQL($table) ?? '';
278 279 280 281 282 283 284

        foreach ($list as $columnName => $column) {
            $type = $column->getType();

            if ($type instanceof StringType || $type instanceof TextType) {
                $column->setPlatformOption('collation', $this->parseColumnCollationFromSQL($columnName, $createSql) ?: 'BINARY');
            }
285 286

            $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
Steve Müller's avatar
Steve Müller committed
287

288 289 290
            if ($comment === null) {
                continue;
            }
Steve Müller's avatar
Steve Müller committed
291

Sergei Morozov's avatar
Sergei Morozov committed
292
            $type = $this->extractDoctrineTypeFromComment($comment, '');
Steve Müller's avatar
Steve Müller committed
293

Sergei Morozov's avatar
Sergei Morozov committed
294
            if ($type !== '') {
295
                $column->setType(Type::getType($type));
296

297
                $comment = $this->removeDoctrineTypeFromComment($comment, $type);
298
            }
299 300

            $column->setComment($comment);
301 302
        }

303 304 305
        return $list;
    }

Benjamin Morel's avatar
Benjamin Morel committed
306 307 308
    /**
     * {@inheritdoc}
     */
309 310
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
311
        $parts               = explode('(', $tableColumn['type']);
312
        $tableColumn['type'] = trim($parts[0]);
313
        if (isset($parts[1])) {
314
            $length                = trim($parts[1], ')');
315 316 317
            $tableColumn['length'] = $length;
        }

Gabriel Caruso's avatar
Gabriel Caruso committed
318 319
        $dbType   = strtolower($tableColumn['type']);
        $length   = $tableColumn['length'] ?? null;
320 321 322
        $unsigned = false;

        if (strpos($dbType, ' unsigned') !== false) {
323
            $dbType   = str_replace(' unsigned', '', $dbType);
324 325 326
            $unsigned = true;
        }

327 328
        $fixed   = false;
        $type    = $this->_platform->getDoctrineTypeMapping($dbType);
329
        $default = $tableColumn['dflt_value'];
330
        if ($default === 'NULL') {
331 332
            $default = null;
        }
333

334
        if ($default !== null) {
335 336 337 338
            // SQLite returns the default value as a literal expression, so we need to parse it
            if (preg_match('/^\'(.*)\'$/s', $default, $matches)) {
                $default = str_replace("''", "'", $matches[1]);
            }
339
        }
340

341 342
        $notnull = (bool) $tableColumn['notnull'];

343
        if (! isset($tableColumn['name'])) {
344 345 346
            $tableColumn['name'] = '';
        }

347
        $precision = null;
348
        $scale     = null;
349

350 351
        switch ($dbType) {
            case 'char':
352
                $fixed = true;
353 354 355 356 357 358
                break;
            case 'float':
            case 'double':
            case 'real':
            case 'decimal':
            case 'numeric':
359
                if (isset($tableColumn['length'])) {
360
                    if (strpos($tableColumn['length'], ',') === false) {
361
                        $tableColumn['length'] .= ',0';
Steve Müller's avatar
Steve Müller committed
362
                    }
Grégoire Paris's avatar
Grégoire Paris committed
363

364
                    [$precision, $scale] = array_map('trim', explode(',', $tableColumn['length']));
365
                }
Grégoire Paris's avatar
Grégoire Paris committed
366

367 368 369 370
                $length = null;
                break;
        }

371
        $options = [
372 373 374 375 376 377 378
            'length'   => $length,
            'unsigned' => (bool) $unsigned,
            'fixed'    => $fixed,
            'notnull'  => $notnull,
            'default'  => $default,
            'precision' => $precision,
            'scale'     => $scale,
379
            'autoincrement' => false,
380
        ];
381

382
        return new Column($tableColumn['name'], Type::getType($type), $options);
383
    }
384

Benjamin Morel's avatar
Benjamin Morel committed
385 386 387
    /**
     * {@inheritdoc}
     */
388 389 390 391
    protected function _getPortableViewDefinition($view)
    {
        return new View($view['name'], $view['sql']);
    }
392

Benjamin Morel's avatar
Benjamin Morel committed
393 394 395
    /**
     * {@inheritdoc}
     */
396 397
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
398
        $list = [];
Benjamin Morel's avatar
Benjamin Morel committed
399
        foreach ($tableForeignKeys as $value) {
400
            $value = array_change_key_case($value, CASE_LOWER);
401 402 403
            $name  = $value['constraint_name'];
            if (! isset($list[$name])) {
                if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') {
404 405
                    $value['on_delete'] = null;
                }
Grégoire Paris's avatar
Grégoire Paris committed
406

407
                if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') {
408 409 410
                    $value['on_update'] = null;
                }

411
                $list[$name] = [
412
                    'name' => $name,
413 414
                    'local' => [],
                    'foreign' => [],
415 416 417
                    'foreignTable' => $value['table'],
                    'onDelete' => $value['on_delete'],
                    'onUpdate' => $value['on_update'],
418
                    'deferrable' => $value['deferrable'],
419
                    'deferred' => $value['deferred'],
420
                ];
421
            }
Grégoire Paris's avatar
Grégoire Paris committed
422

423
            $list[$name]['local'][]   = $value['from'];
424
            $list[$name]['foreign'][] = $value['to'];
425 426
        }

427
        $result = [];
Steve Müller's avatar
Steve Müller committed
428
        foreach ($list as $constraint) {
429
            $result[] = new ForeignKeyConstraint(
430 431 432 433
                array_values($constraint['local']),
                $constraint['foreignTable'],
                array_values($constraint['foreign']),
                $constraint['name'],
434
                [
435 436
                    'onDelete' => $constraint['onDelete'],
                    'onUpdate' => $constraint['onUpdate'],
437
                    'deferrable' => $constraint['deferrable'],
438
                    'deferred' => $constraint['deferred'],
439
                ]
440 441 442 443 444 445
            );
        }

        return $result;
    }

Benjamin Morel's avatar
Benjamin Morel committed
446
    /**
447
     * @param Table|string $table
Benjamin Morel's avatar
Benjamin Morel committed
448
     *
449
     * @return TableDiff
Benjamin Morel's avatar
Benjamin Morel committed
450
     *
451
     * @throws DBALException
Benjamin Morel's avatar
Benjamin Morel committed
452
     */
Sergei Morozov's avatar
Sergei Morozov committed
453
    private function getTableDiffForAlterForeignKey($table)
454
    {
455
        if (! $table instanceof Table) {
456
            $tableDetails = $this->tryMethod('listTableDetails', $table);
Sergei Morozov's avatar
Sergei Morozov committed
457 458

            if ($tableDetails === false) {
Benjamin Morel's avatar
Benjamin Morel committed
459
                throw new DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table));
460 461 462 463 464
            }

            $table = $tableDetails;
        }

465
        $tableDiff            = new TableDiff($table->getName());
466 467 468 469
        $tableDiff->fromTable = $table;

        return $tableDiff;
    }
470

471
    private function parseColumnCollationFromSQL(string $column, string $sql): ?string
472
    {
473
        $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
474
            . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
475

476 477
        if (preg_match($pattern, $sql, $match) !== 1) {
            return null;
478 479
        }

480
        return $match[1];
481
    }
482

483
    private function parseTableCommentFromSQL(string $table, string $sql): ?string
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
    {
        $pattern = '/\s* # Allow whitespace characters at start of line
CREATE\sTABLE # Match "CREATE TABLE"
(?:\W"' . preg_quote($this->_platform->quoteSingleIdentifier($table), '/') . '"\W|\W' . preg_quote($table, '/')
            . '\W) # Match table name (quoted and unquoted)
( # Start capture
   (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s)
)/ix';

        if (preg_match($pattern, $sql, $match) !== 1) {
            return null;
        }

        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));

        return $comment === '' ? null : $comment;
    }

502
    private function parseColumnCommentFromSQL(string $column, string $sql): ?string
503
    {
504
        $pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column)) . '\W|\W' . preg_quote($column)
505
            . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
506

507 508
        if (preg_match($pattern, $sql, $match) !== 1) {
            return null;
509 510
        }

511 512
        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));

513
        return $comment === '' ? null : $comment;
514
    }
515

516
    private function getCreateTableSQL(string $table): ?string
517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
    {
        return $this->_conn->fetchColumn(
            <<<'SQL'
SELECT sql
  FROM (
      SELECT *
        FROM sqlite_master
   UNION ALL
      SELECT *
        FROM sqlite_temp_master
  )
WHERE type = 'table'
AND name = ?
SQL
            ,
            [$table]
        ) ?: null;
    }
535 536 537 538

    /**
     * @param string $tableName
     */
539
    public function listTableDetails($tableName): Table
540 541 542 543 544 545 546 547 548 549 550 551 552
    {
        $table = parent::listTableDetails($tableName);

        $tableCreateSql = $this->getCreateTableSQL($tableName) ?? '';

        $comment = $this->parseTableCommentFromSQL($tableName, $tableCreateSql);

        if ($comment !== null) {
            $table->addOption('comment', $comment);
        }

        return $table;
    }
553
}