SqliteSchemaManager.php 15 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
use Doctrine\DBAL\FetchMode;
8 9
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
10
use Doctrine\DBAL\Types\Type;
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
use const CASE_LOWER;
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;
30

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

        unlink($database);
46 47
    }

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

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

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

        $this->alterTable($tableDiff);
    }

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

        $this->alterTable($tableDiff);
    }

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

        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function listTableForeignKeys($table, $database = null)
    {
113
        if ($database === null) {
114 115
            $database = $this->_conn->getDatabase();
        }
116
        $sql              = $this->_platform->getListTableForeignKeysSQL($table, $database);
117 118
        $tableForeignKeys = $this->_conn->fetchAll($sql);

119
        if (! empty($tableForeignKeys)) {
120 121 122 123 124 125
            $createSql = $this->_conn->fetchAll(
                sprintf(
                    "SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '%s'",
                    $table
                )
            );
126
            $createSql = $createSql[0]['sql'] ?? '';
127

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

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

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

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

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

        // fetch primary
177 178 179
        $stmt       = $this->_conn->executeQuery(
            sprintf("PRAGMA TABLE_INFO ('%s')", $tableName)
        );
180
        $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);
181

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

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

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

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

            $keyName           = $tableIndex['name'];
            $idx               = [];
            $idx['key_name']   = $keyName;
            $idx['primary']    = false;
            $idx['non_unique'] = $tableIndex['unique']?false:true;

215 216 217
            $stmt       = $this->_conn->executeQuery(
                sprintf("PRAGMA INDEX_INFO ('%s')", $keyName)
            );
218 219 220 221 222
            $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);

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

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

Benjamin Morel's avatar
Benjamin Morel committed
229 230 231
    /**
     * {@inheritdoc}
     */
232 233
    protected function _getPortableTableIndexDefinition($tableIndex)
    {
234
        return [
235
            'name' => $tableIndex['name'],
236
            'unique' => (bool) $tableIndex['unique'],
237
        ];
238 239
    }

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

        // find column with autoincrement
248
        $autoincrementColumn = null;
249
        $autoincrementCount  = 0;
250

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

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

            $autoincrementColumn = $tableColumn['name'];
262 263
        }

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

                $column->setAutoincrement(true);
271 272 273
            }
        }

274
        // inspect column collation and comments
275 276 277 278 279 280
        $createSql = $this->_conn->fetchAll(
            sprintf(
                "SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '%s'",
                $table
            )
        );
281
        $createSql = $createSql[0]['sql'] ?? '';
282 283 284 285 286 287 288

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

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

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

292 293 294
            if ($comment === null) {
                continue;
            }
Steve Müller's avatar
Steve Müller committed
295

296
            $type = $this->extractDoctrineTypeFromComment($comment, null);
Steve Müller's avatar
Steve Müller committed
297

298 299
            if ($type !== null) {
                $column->setType(Type::getType($type));
300

301
                $comment = $this->removeDoctrineTypeFromComment($comment, $type);
302
            }
303 304

            $column->setComment($comment);
305 306
        }

307 308 309
        return $list;
    }

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

Gabriel Caruso's avatar
Gabriel Caruso committed
322 323
        $dbType   = strtolower($tableColumn['type']);
        $length   = $tableColumn['length'] ?? null;
324 325 326
        $unsigned = false;

        if (strpos($dbType, ' unsigned') !== false) {
327
            $dbType   = str_replace(' unsigned', '', $dbType);
328 329 330
            $unsigned = true;
        }

331 332
        $fixed   = false;
        $type    = $this->_platform->getDoctrineTypeMapping($dbType);
333
        $default = $tableColumn['dflt_value'];
334
        if ($default === 'NULL') {
335 336
            $default = null;
        }
337
        if ($default !== null) {
338 339
            // SQLite returns strings wrapped in single quotes, so we need to strip them
            $default = preg_replace("/^'(.*)'$/", '\1', $default);
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
                    }
363
                    [$precision, $scale] = array_map('trim', explode(',', $tableColumn['length']));
364
                }
365 366 367 368
                $length = null;
                break;
        }

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

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

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

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

408
                $list[$name] = [
409
                    'name' => $name,
410 411
                    'local' => [],
                    'foreign' => [],
412 413 414
                    'foreignTable' => $value['table'],
                    'onDelete' => $value['on_delete'],
                    'onUpdate' => $value['on_update'],
415 416
                    'deferrable' => $value['deferrable'],
                    'deferred'=> $value['deferred'],
417
                ];
418
            }
419
            $list[$name]['local'][]   = $value['from'];
420
            $list[$name]['foreign'][] = $value['to'];
421 422
        }

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

        return $result;
    }

Benjamin Morel's avatar
Benjamin Morel committed
442
    /**
443
     * @param Table|string $table
Benjamin Morel's avatar
Benjamin Morel committed
444
     *
445
     * @return TableDiff
Benjamin Morel's avatar
Benjamin Morel committed
446
     *
447
     * @throws DBALException
Benjamin Morel's avatar
Benjamin Morel committed
448
     */
449 450
    private function getTableDiffForAlterForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
451
        if (! $table instanceof Table) {
452
            $tableDetails = $this->tryMethod('listTableDetails', $table);
453
            if ($table === false) {
Benjamin Morel's avatar
Benjamin Morel committed
454
                throw new DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table));
455 456 457 458 459
            }

            $table = $tableDetails;
        }

460
        $tableDiff            = new TableDiff($table->getName());
461 462 463 464
        $tableDiff->fromTable = $table;

        return $tableDiff;
    }
465

466
    private function parseColumnCollationFromSQL(string $column, string $sql) : ?string
467
    {
468
        $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
469
            . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
470

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

475
        return $match[1];
476
    }
477

478
    private function parseColumnCommentFromSQL(string $column, string $sql) : ?string
479
    {
480
        $pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column)) . '\W|\W' . preg_quote($column)
481
            . '\W)(?:\(.*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
482

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

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

489
        return $comment === '' ? null : $comment;
490
    }
491
}