SqliteSchemaManager.php 15.6 KB
Newer Older
romanb's avatar
romanb committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<?php
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
Benjamin Eberlei's avatar
Benjamin Eberlei committed
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
romanb's avatar
romanb committed
18 19
 */

20
namespace Doctrine\DBAL\Schema;
romanb's avatar
romanb committed
21

22
use Doctrine\DBAL\DBALException;
23 24
use Doctrine\DBAL\Types\StringType;
use Doctrine\DBAL\Types\TextType;
25
use Doctrine\DBAL\Types\Type;
26

romanb's avatar
romanb committed
27
/**
Benjamin Morel's avatar
Benjamin Morel committed
28
 * Sqlite SchemaManager.
romanb's avatar
romanb committed
29
 *
Benjamin Morel's avatar
Benjamin Morel committed
30 31 32 33 34
 * @author Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @author Lukas Smith <smith@pooteeweet.org> (PEAR MDB2 library)
 * @author Jonathan H. Wage <jonwage@gmail.com>
 * @author Martin Hasoň <martin.hason@gmail.com>
 * @since  2.0
romanb's avatar
romanb committed
35
 */
36
class SqliteSchemaManager extends AbstractSchemaManager
37
{
romanb's avatar
romanb committed
38 39 40 41
    /**
     * {@inheritdoc}
     */
    public function dropDatabase($database)
42
    {
43 44
        if (file_exists($database)) {
            unlink($database);
45 46 47
        }
    }

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

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
    /**
     * {@inheritdoc}
     */
    public function renameTable($name, $newName)
    {
        $tableDiff = new TableDiff($name);
        $tableDiff->fromTable = $this->listTableDetails($name);
        $tableDiff->newName = $newName;
        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
        $tableDiff->addedForeignKeys[] = $foreignKey;

        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
        $tableDiff->changedForeignKeys[] = $foreignKey;

        $this->alterTable($tableDiff);
    }

    /**
     * {@inheritdoc}
     */
    public function dropForeignKey($foreignKey, $table)
    {
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
        $tableDiff->removedForeignKeys[] = $foreignKey;

        $this->alterTable($tableDiff);
    }

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

        if ( ! empty($tableForeignKeys)) {
            $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'");
            $createSql = isset($createSql[0]['sql']) ? $createSql[0]['sql'] : '';
122

123 124 125 126 127 128 129 130 131 132 133
            if (preg_match_all('#
                    (?:CONSTRAINT\s+([^\s]+)\s+)?
                    (?:FOREIGN\s+KEY[^\)]+\)\s*)?
                    REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
                    (?:
                        [^,]*?
                        (NOT\s+DEFERRABLE|DEFERRABLE)
                        (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
                    )?#isx',
                    $createSql, $match)) {

134
                $names = array_reverse($match[1]);
135 136
                $deferrable = array_reverse($match[2]);
                $deferred = array_reverse($match[3]);
137
            } else {
138
                $names = $deferrable = $deferred = array();
139 140 141 142 143
            }

            foreach ($tableForeignKeys as $key => $value) {
                $id = $value['id'];
                $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && '' != $names[$id] ? $names[$id] : $id;
144 145
                $tableForeignKeys[$key]['deferrable'] = isset($deferrable[$id]) && 'deferrable' == strtolower($deferrable[$id]) ? true : false;
                $tableForeignKeys[$key]['deferred'] = isset($deferred[$id]) && 'deferred' == strtolower($deferred[$id]) ? true : false;
146 147 148 149 150 151
            }
        }

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

Benjamin Morel's avatar
Benjamin Morel committed
152 153 154
    /**
     * {@inheritdoc}
     */
155 156 157 158 159
    protected function _getPortableTableDefinition($table)
    {
        return $table['name'];
    }

160
    /**
Benjamin Morel's avatar
Benjamin Morel committed
161 162
     * {@inheritdoc}
     *
163 164 165 166 167 168 169 170
     * @license New BSD License
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
     */
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
    {
        $indexBuffer = array();

        // fetch primary
Steve Müller's avatar
Steve Müller committed
171
        $stmt = $this->_conn->executeQuery("PRAGMA TABLE_INFO ('$tableName')");
172
        $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC);
173

174
        usort($indexArray, function($a, $b) {
175 176 177
            if ($a['pk'] == $b['pk']) {
                return $a['cid'] - $b['cid'];
            }
178

179 180
            return $a['pk'] - $b['pk'];
        });
Steve Müller's avatar
Steve Müller committed
181
        foreach ($indexArray as $indexColumnRow) {
182
            if ($indexColumnRow['pk'] != "0") {
183 184 185 186 187 188 189 190 191 192
                $indexBuffer[] = array(
                    'key_name' => 'primary',
                    'primary' => true,
                    'non_unique' => false,
                    'column_name' => $indexColumnRow['name']
                );
            }
        }

        // fetch regular indexes
Steve Müller's avatar
Steve Müller committed
193
        foreach ($tableIndexes as $tableIndex) {
194 195 196 197 198 199 200
            // Ignore indexes with reserved names, e.g. autoindexes
            if (strpos($tableIndex['name'], 'sqlite_') !== 0) {
                $keyName = $tableIndex['name'];
                $idx = array();
                $idx['key_name'] = $keyName;
                $idx['primary'] = false;
                $idx['non_unique'] = $tableIndex['unique']?false:true;
201

Steve Müller's avatar
Steve Müller committed
202
                $stmt = $this->_conn->executeQuery("PRAGMA INDEX_INFO ('{$keyName}')");
203
                $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC);
204

Steve Müller's avatar
Steve Müller committed
205
                foreach ($indexArray as $indexColumnRow) {
206 207 208
                    $idx['column_name'] = $indexColumnRow['name'];
                    $indexBuffer[] = $idx;
                }
209 210 211 212 213 214
            }
        }

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

Benjamin Morel's avatar
Benjamin Morel committed
215 216 217
    /**
     * {@inheritdoc}
     */
218 219 220 221 222 223 224 225
    protected function _getPortableTableIndexDefinition($tableIndex)
    {
        return array(
            'name' => $tableIndex['name'],
            'unique' => (bool) $tableIndex['unique']
        );
    }

Benjamin Morel's avatar
Benjamin Morel committed
226 227 228
    /**
     * {@inheritdoc}
     */
229 230 231
    protected function _getPortableTableColumnList($table, $database, $tableColumns)
    {
        $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
232 233

        // find column with autoincrement
234 235
        $autoincrementColumn = null;
        $autoincrementCount = 0;
236

237
        foreach ($tableColumns as $tableColumn) {
238
            if ('0' != $tableColumn['pk']) {
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
                $autoincrementCount++;
                if (null === $autoincrementColumn && 'integer' == strtolower($tableColumn['type'])) {
                    $autoincrementColumn = $tableColumn['name'];
                }
            }
        }

        if (1 == $autoincrementCount && null !== $autoincrementColumn) {
            foreach ($list as $column) {
                if ($autoincrementColumn == $column->getName()) {
                    $column->setAutoincrement(true);
                }
            }
        }

254
        // inspect column collation and comments
255 256 257 258 259 260 261 262 263
        $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'");
        $createSql = isset($createSql[0]['sql']) ? $createSql[0]['sql'] : '';

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

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

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

267 268
            if (false !== $comment) {
                $type = $this->extractDoctrineTypeFromComment($comment, null);
Steve Müller's avatar
Steve Müller committed
269

270 271
                if (null !== $type) {
                    $column->setType(Type::getType($type));
Steve Müller's avatar
Steve Müller committed
272

273 274 275 276 277
                    $comment = $this->removeDoctrineTypeFromComment($comment, $type);
                }

                $column->setComment($comment);
            }
278 279
        }

280 281 282
        return $list;
    }

Benjamin Morel's avatar
Benjamin Morel committed
283 284 285
    /**
     * {@inheritdoc}
     */
286 287
    protected function _getPortableTableColumnDefinition($tableColumn)
    {
288
        $parts = explode('(', $tableColumn['type']);
289
        $tableColumn['type'] = trim($parts[0]);
290 291
        if (isset($parts[1])) {
            $length = trim($parts[1], ')');
292 293 294 295 296
            $tableColumn['length'] = $length;
        }

        $dbType = strtolower($tableColumn['type']);
        $length = isset($tableColumn['length']) ? $tableColumn['length'] : null;
297 298 299 300 301 302 303
        $unsigned = false;

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

304
        $fixed = false;
305
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
306
        $default = $tableColumn['dflt_value'];
307
        if ($default == 'NULL') {
308 309
            $default = null;
        }
310 311 312 313
        if ($default !== null) {
            // SQLite returns strings wrapped in single quotes, so we need to strip them
            $default = preg_replace("/^'(.*)'$/", '\1', $default);
        }
314 315 316 317 318 319
        $notnull = (bool) $tableColumn['notnull'];

        if ( ! isset($tableColumn['name'])) {
            $tableColumn['name'] = '';
        }

320 321 322
        $precision = null;
        $scale = null;

323 324
        switch ($dbType) {
            case 'char':
325
                $fixed = true;
326 327 328 329 330 331
                break;
            case 'float':
            case 'double':
            case 'real':
            case 'decimal':
            case 'numeric':
332
                if (isset($tableColumn['length'])) {
333 334
                    if (strpos($tableColumn['length'], ',') === false) {
                        $tableColumn['length'] .= ",0";
Steve Müller's avatar
Steve Müller committed
335
                    }
336
                    list($precision, $scale) = array_map('trim', explode(',', $tableColumn['length']));
337
                }
338 339 340 341
                $length = null;
                break;
        }

342
        $options = array(
343 344 345 346 347 348 349
            'length'   => $length,
            'unsigned' => (bool) $unsigned,
            'fixed'    => $fixed,
            'notnull'  => $notnull,
            'default'  => $default,
            'precision' => $precision,
            'scale'     => $scale,
350
            'autoincrement' => false,
351
        );
352

353
        return new Column($tableColumn['name'], \Doctrine\DBAL\Types\Type::getType($type), $options);
354
    }
355

Benjamin Morel's avatar
Benjamin Morel committed
356 357 358
    /**
     * {@inheritdoc}
     */
359 360 361 362
    protected function _getPortableViewDefinition($view)
    {
        return new View($view['name'], $view['sql']);
    }
363

Benjamin Morel's avatar
Benjamin Morel committed
364 365 366
    /**
     * {@inheritdoc}
     */
367 368 369
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
    {
        $list = array();
Benjamin Morel's avatar
Benjamin Morel committed
370
        foreach ($tableForeignKeys as $value) {
371
            $value = array_change_key_case($value, CASE_LOWER);
372 373
            $name = $value['constraint_name'];
            if ( ! isset($list[$name])) {
374 375 376 377 378 379 380
                if ( ! isset($value['on_delete']) || $value['on_delete'] == "RESTRICT") {
                    $value['on_delete'] = null;
                }
                if ( ! isset($value['on_update']) || $value['on_update'] == "RESTRICT") {
                    $value['on_update'] = null;
                }

381 382
                $list[$name] = array(
                    'name' => $name,
383 384 385 386 387
                    'local' => array(),
                    'foreign' => array(),
                    'foreignTable' => $value['table'],
                    'onDelete' => $value['on_delete'],
                    'onUpdate' => $value['on_update'],
388 389
                    'deferrable' => $value['deferrable'],
                    'deferred'=> $value['deferred'],
390 391
                );
            }
392 393
            $list[$name]['local'][] = $value['from'];
            $list[$name]['foreign'][] = $value['to'];
394 395 396
        }

        $result = array();
Steve Müller's avatar
Steve Müller committed
397
        foreach ($list as $constraint) {
398 399 400 401 402 403
            $result[] = new ForeignKeyConstraint(
                array_values($constraint['local']), $constraint['foreignTable'],
                array_values($constraint['foreign']), $constraint['name'],
                array(
                    'onDelete' => $constraint['onDelete'],
                    'onUpdate' => $constraint['onUpdate'],
404 405
                    'deferrable' => $constraint['deferrable'],
                    'deferred'=> $constraint['deferred'],
406 407 408 409 410 411 412
                )
            );
        }

        return $result;
    }

Benjamin Morel's avatar
Benjamin Morel committed
413 414 415 416 417 418 419 420
    /**
     * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey
     * @param \Doctrine\DBAL\Schema\Table|string         $table
     *
     * @return \Doctrine\DBAL\Schema\TableDiff
     *
     * @throws \Doctrine\DBAL\DBALException
     */
421 422 423 424 425
    private function getTableDiffForAlterForeignKey(ForeignKeyConstraint $foreignKey, $table)
    {
        if ( ! $table instanceof Table) {
            $tableDetails = $this->tryMethod('listTableDetails', $table);
            if (false === $table) {
Benjamin Morel's avatar
Benjamin Morel committed
426
                throw new DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table));
427 428 429 430 431 432 433 434 435 436
            }

            $table = $tableDetails;
        }

        $tableDiff = new TableDiff($table->getName());
        $tableDiff->fromTable = $table;

        return $tableDiff;
    }
437

438 439 440 441 442 443
    /**
     * @param string $column
     * @param string $sql
     *
     * @return string|false
     */
444 445
    private function parseColumnCollationFromSQL($column, $sql)
    {
446 447 448 449
        $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
                 . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}isx';

        if (preg_match($pattern, $sql, $match) === 1) {
450 451 452 453 454
            return $match[1];
        }

        return false;
    }
455

Steve Müller's avatar
Steve Müller committed
456 457 458 459 460 461
    /**
     * @param string $column
     * @param string $sql
     *
     * @return string|false
     */
462 463 464 465 466 467 468 469 470 471 472 473 474 475
    private function parseColumnCommentFromSQL($column, $sql)
    {
        if (preg_match(
            '{[\s(,](?:'.preg_quote($this->_platform->quoteSingleIdentifier($column)).'|'.preg_quote($column).')
            (?:\(.*?\)|[^,(])*?,?((?:\s*--[^\n]*\n?)+)
            }isx', $sql, $match
        )) {
            $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));

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

        return false;
    }
476
}