SqlitePlatform.php 34.5 KB
Newer Older
1 2
<?php

3
namespace Doctrine\DBAL\Platforms;
4

5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\Schema\Column;
7
use Doctrine\DBAL\Schema\Constraint;
8
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
9
use Doctrine\DBAL\Schema\Identifier;
10 11 12
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
13
use Doctrine\DBAL\TransactionIsolationLevel;
14
use Doctrine\DBAL\Types;
15
use InvalidArgumentException;
16 17 18 19 20 21 22 23 24 25 26
use function array_merge;
use function array_unique;
use function array_values;
use function implode;
use function is_numeric;
use function sprintf;
use function sqrt;
use function str_replace;
use function strlen;
use function strpos;
use function strtolower;
27
use function trim;
28

29
/**
30 31
 * The SqlitePlatform class describes the specifics and dialects of the SQLite
 * database platform.
32
 *
Benjamin Morel's avatar
Benjamin Morel committed
33
 * @todo   Rename: SQLitePlatform
34
 */
35
class SqlitePlatform extends AbstractPlatform
36 37
{
    /**
38
     * {@inheritDoc}
39 40 41
     */
    public function getRegexpExpression()
    {
42
        return 'REGEXP';
43 44 45
    }

    /**
46
     * {@inheritDoc}
47 48 49 50 51 52 53 54 55 56 57 58 59 60
     */
    public function getNowExpression($type = 'timestamp')
    {
        switch ($type) {
            case 'time':
                return 'time(\'now\')';
            case 'date':
                return 'date(\'now\')';
            case 'timestamp':
            default:
                return 'datetime(\'now\')';
        }
    }

61
    /**
62
     * {@inheritDoc}
63
     */
64
    public function getTrimExpression(string $str, int $mode = TrimMode::UNSPECIFIED, ?string $char = null) : string
65
    {
66 67 68 69 70
        switch ($mode) {
            case TrimMode::UNSPECIFIED:
            case TrimMode::BOTH:
                $trimFn = 'TRIM';
                break;
71

72
            case TrimMode::LEADING:
73 74 75
                $trimFn = 'LTRIM';
                break;

76
            case TrimMode::TRAILING:
77 78 79 80
                $trimFn = 'RTRIM';
                break;

            default:
81 82 83 84 85 86 87 88 89 90 91 92
                throw new InvalidArgumentException(
                    sprintf(
                        'The value of $mode is expected to be one of the TrimMode constants, %d given',
                        $mode
                    )
                );
        }

        $arguments = [$str];

        if ($char !== null) {
            $arguments[] = $char;
93 94
        }

95
        return sprintf('%s(%s)', $trimFn, implode(', ', $arguments));
96 97
    }

98
    /**
99
     * {@inheritDoc}
100 101 102 103 104 105 106 107
     *
     * SQLite only supports the 2 parameter variant of this function
     */
    public function getSubstringExpression($value, $position, $length = null)
    {
        if ($length !== null) {
            return 'SUBSTR(' . $value . ', ' . $position . ', ' . $length . ')';
        }
108

109 110 111
        return 'SUBSTR(' . $value . ', ' . $position . ', LENGTH(' . $value . '))';
    }

112
    /**
113
     * {@inheritDoc}
114 115 116
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
117 118
        if ($startPos === false) {
            return 'LOCATE(' . $str . ', ' . $substr . ')';
119
        }
120

121
        return 'LOCATE(' . $str . ', ' . $substr . ', ' . $startPos . ')';
122 123
    }

124
    /**
125
     * {@inheritdoc}
126
     */
127
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
128
    {
129
        switch ($unit) {
130 131 132
            case DateIntervalUnit::SECOND:
            case DateIntervalUnit::MINUTE:
            case DateIntervalUnit::HOUR:
133
                return 'DATETIME(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')";
134

135 136
            default:
                switch ($unit) {
137
                    case DateIntervalUnit::WEEK:
138
                        $interval *= 7;
139
                        $unit      = DateIntervalUnit::DAY;
140 141
                        break;

142
                    case DateIntervalUnit::QUARTER:
143
                        $interval *= 3;
144
                        $unit      = DateIntervalUnit::MONTH;
145 146 147
                        break;
                }

Fogs's avatar
Fogs committed
148
                if (! is_numeric($interval)) {
149 150 151
                    $interval = "' || " . $interval . " || '";
                }

152
                return 'DATE(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')";
153
        }
154 155
    }

156 157 158
    /**
     * {@inheritDoc}
     */
159
    public function getDateDiffExpression($date1, $date2)
160
    {
161
        return sprintf("JULIANDAY(%s, 'start of day') - JULIANDAY(%s, 'start of day')", $date1, $date2);
162 163
    }

164 165 166
    /**
     * {@inheritDoc}
     */
167
    protected function _getTransactionIsolationLevelSQL($level)
romanb's avatar
romanb committed
168 169
    {
        switch ($level) {
170
            case TransactionIsolationLevel::READ_UNCOMMITTED:
romanb's avatar
romanb committed
171
                return 0;
172 173 174
            case TransactionIsolationLevel::READ_COMMITTED:
            case TransactionIsolationLevel::REPEATABLE_READ:
            case TransactionIsolationLevel::SERIALIZABLE:
romanb's avatar
romanb committed
175 176
                return 1;
            default:
177
                return parent::_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
178 179
        }
    }
180

181 182 183
    /**
     * {@inheritDoc}
     */
184
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
185
    {
186
        return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
187
    }
188

189
    /**
190
     * {@inheritDoc}
191
     */
192 193
    public function prefersIdentityColumns()
    {
194 195
        return true;
    }
196 197

    /**
198
     * {@inheritDoc}
199
     */
200
    public function getBooleanTypeDeclarationSQL(array $field)
201 202 203
    {
        return 'BOOLEAN';
    }
204

205
    /**
206
     * {@inheritDoc}
207
     */
208
    public function getIntegerTypeDeclarationSQL(array $field)
209
    {
210
        return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($field);
211 212
    }

213
    /**
214
     * {@inheritDoc}
215
     */
216
    public function getBigIntTypeDeclarationSQL(array $field)
217
    {
218
        //  SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT fields.
219
        if (! empty($field['autoincrement'])) {
220 221 222
            return $this->getIntegerTypeDeclarationSQL($field);
        }

223
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
224 225
    }

226
    /**
227
     * {@inheritDoc}
228
     */
229 230
    public function getTinyIntTypeDeclarationSql(array $field)
    {
231
        //  SQLite autoincrement is implicit for INTEGER PKs, but not for TINYINT fields.
232
        if (! empty($field['autoincrement'])) {
233 234 235
            return $this->getIntegerTypeDeclarationSQL($field);
        }

236
        return 'TINYINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
237 238
    }

239
    /**
240
     * {@inheritDoc}
241
     */
242
    public function getSmallIntTypeDeclarationSQL(array $field)
243
    {
244
        //  SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT fields.
245
        if (! empty($field['autoincrement'])) {
246 247 248
            return $this->getIntegerTypeDeclarationSQL($field);
        }

249
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
250 251
    }

252
    /**
253
     * {@inheritDoc}
254
     */
255 256
    public function getMediumIntTypeDeclarationSql(array $field)
    {
257
        //  SQLite autoincrement is implicit for INTEGER PKs, but not for MEDIUMINT fields.
258
        if (! empty($field['autoincrement'])) {
259 260 261
            return $this->getIntegerTypeDeclarationSQL($field);
        }

262
        return 'MEDIUMINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
263 264
    }

265
    /**
266
     * {@inheritDoc}
267
     */
268
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
269 270 271
    {
        return 'DATETIME';
    }
272

273
    /**
274
     * {@inheritDoc}
275
     */
276
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
277 278 279
    {
        return 'DATE';
    }
280

281
    /**
282
     * {@inheritDoc}
283
     */
284
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
285 286 287 288
    {
        return 'TIME';
    }

289
    /**
290
     * {@inheritDoc}
291
     */
292
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
293
    {
294
        // sqlite autoincrement is only possible for the primary key
295
        if (! empty($columnDef['autoincrement'])) {
296
            return ' PRIMARY KEY AUTOINCREMENT';
297 298
        }

299
        return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : '';
300 301
    }

302 303 304 305 306 307
    /**
     * {@inheritDoc}
     */
    public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey)
    {
        return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint(
308 309 310
            $foreignKey->getQuotedLocalColumns($this),
            str_replace('.', '__', $foreignKey->getQuotedForeignTableName($this)),
            $foreignKey->getQuotedForeignColumns($this),
311 312 313 314 315
            $foreignKey->getName(),
            $foreignKey->getOptions()
        ));
    }

316
    /**
317
     * {@inheritDoc}
318
     */
319
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
320
    {
Guilherme Blanco's avatar
Guilherme Blanco committed
321
        $tableName   = str_replace('.', '__', $tableName);
322
        $queryFields = $this->getColumnDeclarationListSQL($columns);
323

324 325 326 327 328 329
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
            foreach ($options['uniqueConstraints'] as $name => $definition) {
                $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
            }
        }

Timo Bakx's avatar
Timo Bakx committed
330
        $queryFields .= $this->getNonAutoincrementPrimaryKeyDefinition($columns, $options);
331

332 333
        if (isset($options['foreignKeys'])) {
            foreach ($options['foreignKeys'] as $foreignKey) {
334
                $queryFields .= ', ' . $this->getForeignKeyDeclarationSQL($foreignKey);
335 336 337
            }
        }

338 339 340 341 342 343 344
        $tableComment = '';
        if (isset($options['comment'])) {
            $comment = trim($options['comment'], " '");

            $tableComment = $this->getInlineTableCommentSQL($comment);
        }

345
        $query = ['CREATE TABLE ' . $tableName . ' ' . $tableComment . '(' . $queryFields . ')'];
346

347
        if (isset($options['alter']) && $options['alter'] === true) {
348
            return $query;
349
        }
350

351
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
Benjamin Morel's avatar
Benjamin Morel committed
352
            foreach ($options['indexes'] as $indexDef) {
353
                $query[] = $this->getCreateIndexSQL($indexDef, $tableName);
354 355
            }
        }
356

357
        if (isset($options['unique']) && ! empty($options['unique'])) {
Benjamin Morel's avatar
Benjamin Morel committed
358
            foreach ($options['unique'] as $indexDef) {
359
                $query[] = $this->getCreateIndexSQL($indexDef, $tableName);
360 361 362
            }
        }

363 364 365
        return $query;
    }

Timo Bakx's avatar
Timo Bakx committed
366 367 368
    /**
     * Generate a PRIMARY KEY definition if no autoincrement value is used
     *
369 370
     * @param mixed[][] $columns
     * @param mixed[]   $options
Timo Bakx's avatar
Timo Bakx committed
371 372 373 374 375 376 377 378 379 380
     */
    private function getNonAutoincrementPrimaryKeyDefinition(array $columns, array $options) : string
    {
        if (empty($options['primary'])) {
            return '';
        }

        $keyColumns = array_unique(array_values($options['primary']));

        foreach ($keyColumns as $keyColumn) {
381
            if (! empty($columns[$keyColumn]['autoincrement'])) {
Timo Bakx's avatar
Timo Bakx committed
382 383 384 385 386 387 388
                return '';
            }
        }

        return ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
    }

389
    /**
390
     * {@inheritDoc}
391
     */
392
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
393
    {
394 395
        return $fixed
            ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
Guilherme Blanco's avatar
Guilherme Blanco committed
396
            : ($length ? 'VARCHAR(' . $length . ')' : 'TEXT');
397
    }
398

Steve Müller's avatar
Steve Müller committed
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return 'BLOB';
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 0;
    }

    /**
     * {@inheritdoc}
     */
    public function getBinaryDefaultLength()
    {
        return 0;
    }

423 424 425
    /**
     * {@inheritDoc}
     */
426
    public function getClobTypeDeclarationSQL(array $field)
427 428 429
    {
        return 'CLOB';
    }
430

431 432 433
    /**
     * {@inheritDoc}
     */
434
    public function getListTableConstraintsSQL($table)
435
    {
436
        $table = str_replace('.', '__', $table);
437

438 439 440 441
        return sprintf(
            "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = %s AND sql NOT NULL ORDER BY name",
            $this->quoteStringLiteral($table)
        );
442 443
    }

444 445 446
    /**
     * {@inheritDoc}
     */
447
    public function getListTableColumnsSQL($table, $currentDatabase = null)
448
    {
449
        $table = str_replace('.', '__', $table);
450

451
        return sprintf('PRAGMA table_info(%s)', $this->quoteStringLiteral($table));
452 453
    }

454 455 456
    /**
     * {@inheritDoc}
     */
457
    public function getListTableIndexesSQL($table, $currentDatabase = null)
458
    {
459
        $table = str_replace('.', '__', $table);
460

461
        return sprintf('PRAGMA index_list(%s)', $this->quoteStringLiteral($table));
462 463
    }

464 465 466
    /**
     * {@inheritDoc}
     */
467
    public function getListTablesSQL()
468
    {
jsor's avatar
jsor committed
469
        return "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND name != 'geometry_columns' AND name != 'spatial_ref_sys' "
470
             . 'UNION ALL SELECT name FROM sqlite_temp_master '
471 472 473
             . "WHERE type = 'table' ORDER BY name";
    }

474 475 476
    /**
     * {@inheritDoc}
     */
477
    public function getListViewsSQL($database)
478 479 480 481
    {
        return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL";
    }

482 483 484
    /**
     * {@inheritDoc}
     */
485
    public function getCreateViewSQL($name, $sql)
486 487 488 489
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

490 491 492
    /**
     * {@inheritDoc}
     */
493
    public function getDropViewSQL($name)
494
    {
495
        return 'DROP VIEW ' . $name;
496 497
    }

498 499 500
    /**
     * {@inheritDoc}
     */
501 502 503 504
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
    {
        $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey);

505 506
        $query .= ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false ? ' ' : ' NOT ') . 'DEFERRABLE';
        $query .= ' INITIALLY ' . ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false ? 'DEFERRED' : 'IMMEDIATE');
507 508 509 510

        return $query;
    }

511 512 513
    /**
     * {@inheritDoc}
     */
514 515 516 517 518
    public function supportsIdentityColumns()
    {
        return true;
    }

519 520 521 522 523 524 525 526
    /**
     * {@inheritDoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

527 528 529 530 531 532 533 534
    /**
     * {@inheritDoc}
     */
    public function supportsInlineColumnComments()
    {
        return true;
    }

535
    /**
536
     * {@inheritDoc}
537 538 539 540 541
     */
    public function getName()
    {
        return 'sqlite';
    }
542 543

    /**
544
     * {@inheritDoc}
545
     */
546
    public function getTruncateTableSQL($tableName, $cascade = false)
547
    {
548
        $tableIdentifier = new Identifier($tableName);
549
        $tableName       = str_replace('.', '__', $tableIdentifier->getQuotedName($this));
550

551
        return 'DELETE FROM ' . $tableName;
552
    }
553 554

    /**
Benjamin Morel's avatar
Benjamin Morel committed
555
     * User-defined function for Sqlite that is used with PDO::sqliteCreateFunction().
556
     *
557
     * @param int|float $value
558
     *
559 560
     * @return float
     */
561
    public static function udfSqrt($value)
562 563 564 565 566
    {
        return sqrt($value);
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
567
     * User-defined function for Sqlite that implements MOD(a, b).
568
     *
569 570
     * @param int $a
     * @param int $b
571
     *
572
     * @return int
573
     */
574
    public static function udfMod($a, $b)
575
    {
576
        return $a % $b;
577
    }
578 579

    /**
580 581 582
     * @param string $str
     * @param string $substr
     * @param int    $offset
583
     *
584
     * @return int
585
     */
586
    public static function udfLocate($str, $substr, $offset = 0)
587
    {
588 589 590
        // SQL's LOCATE function works on 1-based positions, while PHP's strpos works on 0-based positions.
        // So we have to make them compatible if an offset is given.
        if ($offset > 0) {
Steve Müller's avatar
Steve Müller committed
591
            $offset -= 1;
592 593
        }

594
        $pos = strpos($str, $substr, $offset);
595

596
        if ($pos !== false) {
597
            return $pos + 1;
598
        }
599

600 601
        return 0;
    }
602

603 604 605
    /**
     * {@inheritDoc}
     */
606 607 608 609
    public function getForUpdateSql()
    {
        return '';
    }
610

611 612 613 614 615
    /**
     * {@inheritDoc}
     */
    public function getInlineColumnCommentSQL($comment)
    {
Steve Müller's avatar
Steve Müller committed
616
        return '--' . str_replace("\n", "\n--", $comment) . "\n";
617 618
    }

619 620 621 622 623
    private function getInlineTableCommentSQL(string $comment) : string
    {
        return $this->getInlineColumnCommentSQL($comment);
    }

624 625 626
    /**
     * {@inheritDoc}
     */
627 628
    protected function initializeDoctrineTypeMappings()
    {
629
        $this->doctrineTypeMapping = [
630 631
            'bigint'           => 'bigint',
            'bigserial'        => 'bigint',
632 633
            'blob'             => 'blob',
            'boolean'          => 'boolean',
634
            'char'             => 'string',
635
            'clob'             => 'text',
636 637
            'date'             => 'date',
            'datetime'         => 'datetime',
638
            'decimal'          => 'decimal',
639 640
            'double'           => 'float',
            'double precision' => 'float',
641 642 643 644 645 646 647 648 649
            'float'            => 'float',
            'image'            => 'string',
            'int'              => 'integer',
            'integer'          => 'integer',
            'longtext'         => 'text',
            'longvarchar'      => 'string',
            'mediumint'        => 'integer',
            'mediumtext'       => 'text',
            'ntext'            => 'string',
650
            'numeric'          => 'decimal',
651 652 653 654
            'nvarchar'         => 'string',
            'real'             => 'float',
            'serial'           => 'integer',
            'smallint'         => 'smallint',
655
            'string'           => 'string',
656 657 658 659 660 661 662
            'text'             => 'text',
            'time'             => 'time',
            'timestamp'        => 'datetime',
            'tinyint'          => 'boolean',
            'tinytext'         => 'text',
            'varchar'          => 'string',
            'varchar2'         => 'string',
663
        ];
664
    }
665

666 667 668
    /**
     * {@inheritDoc}
     */
669 670
    protected function getReservedKeywordsClass()
    {
671
        return Keywords\SQLiteKeywords::class;
672
    }
673

674 675 676 677 678
    /**
     * {@inheritDoc}
     */
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
679
        if (! $diff->fromTable instanceof Table) {
680 681 682
            throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema');
        }

683
        $sql = [];
684
        foreach ($diff->fromTable->getIndexes() as $index) {
685 686
            if ($index->isPrimary()) {
                continue;
687
            }
688 689

            $sql[] = $this->getDropIndexSQL($index, $diff->name);
690 691 692 693 694 695 696 697 698 699
        }

        return $sql;
    }

    /**
     * {@inheritDoc}
     */
    protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
700
        if (! $diff->fromTable instanceof Table) {
701 702 703
            throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema');
        }

704
        $sql       = [];
Sergei Morozov's avatar
Sergei Morozov committed
705 706 707 708 709 710
        $tableName = $diff->getNewName();

        if ($tableName === false) {
            $tableName = $diff->getName($this);
        }

Benjamin Morel's avatar
Benjamin Morel committed
711
        foreach ($this->getIndexesInAlteredTable($diff) as $index) {
712 713 714 715
            if ($index->isPrimary()) {
                continue;
            }

716
            $sql[] = $this->getCreateIndexSQL($index, $tableName->getQuotedName($this));
717 718 719 720
        }

        return $sql;
    }
721 722 723 724

    /**
     * {@inheritDoc}
     */
725
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
726
    {
727
        if ($limit === null && $offset > 0) {
728
            $limit = -1;
729 730 731 732
        }

        return parent::doModifyLimitQuery($query, $limit, $offset);
    }
733

734
    /**
735
     * {@inheritDoc}
736 737 738 739 740
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
        return 'BLOB';
    }
741

742 743 744
    /**
     * {@inheritDoc}
     */
745 746
    public function getTemporaryTableName($tableName)
    {
747
        $tableName = str_replace('.', '__', $tableName);
748

749 750
        return $tableName;
    }
751 752

    /**
753 754
     * {@inheritDoc}
     *
755 756 757 758 759 760 761 762 763 764
     * Sqlite Platform emulates schema by underscoring each dot and generating tables
     * into the default database.
     *
     * This hack is implemented to be able to use SQLite as testdriver when
     * using schema supporting databases.
     */
    public function canEmulateSchemas()
    {
        return true;
    }
765

766 767 768 769 770 771 772 773
    /**
     * {@inheritDoc}
     */
    public function supportsForeignKeyConstraints()
    {
        return false;
    }

774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797
    /**
     * {@inheritDoc}
     */
    public function getCreatePrimaryKeySQL(Index $index, $table)
    {
        throw new DBALException('Sqlite platform does not support alter primary key.');
    }

    /**
     * {@inheritdoc}
     */
    public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table)
    {
        throw new DBALException('Sqlite platform does not support alter foreign key.');
    }

    /**
     * {@inheritdoc}
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
        throw new DBALException('Sqlite platform does not support alter foreign key.');
    }

798 799 800 801 802 803 804
    /**
     * {@inheritDoc}
     */
    public function getCreateConstraintSQL(Constraint $constraint, $table)
    {
        throw new DBALException('Sqlite platform does not support alter constraint.');
    }
805 806 807 808 809 810

    /**
     * {@inheritDoc}
     */
    public function getCreateTableSQL(Table $table, $createFlags = null)
    {
811
        $createFlags = $createFlags ?? self::CREATE_INDEXES | self::CREATE_FOREIGNKEYS;
812 813 814

        return parent::getCreateTableSQL($table, $createFlags);
    }
815

816 817 818 819 820 821 822
    /**
     * {@inheritDoc}
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
        $table = str_replace('.', '__', $table);

823
        return sprintf('PRAGMA foreign_key_list(%s)', $this->quoteStringLiteral($table));
824 825 826 827 828 829 830
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
831
        $sql = $this->getSimpleAlterTableSQL($diff);
832
        if ($sql !== false) {
833 834 835
            return $sql;
        }

836
        $fromTable = $diff->fromTable;
837
        if (! $fromTable instanceof Table) {
838 839 840 841 842
            throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema');
        }

        $table = clone $fromTable;

843
        $columns        = [];
844 845
        $oldColumnNames = [];
        $newColumnNames = [];
846
        $columnSql      = [];
847 848

        foreach ($table->getColumns() as $columnName => $column) {
849 850
            $columnName                  = strtolower($columnName);
            $columns[$columnName]        = $column;
851 852 853
            $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this);
        }

854 855 856 857 858
        foreach ($diff->removedColumns as $columnName => $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

859
            $columnName = strtolower($columnName);
860 861
            if (! isset($columns[$columnName])) {
                continue;
862
            }
863 864 865 866 867 868

            unset(
                $columns[$columnName],
                $oldColumnNames[$columnName],
                $newColumnNames[$columnName]
            );
869 870 871 872 873 874 875
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
                continue;
            }

876 877 878 879 880 881 882
            $oldColumnName = strtolower($oldColumnName);
            if (isset($columns[$oldColumnName])) {
                unset($columns[$oldColumnName]);
            }

            $columns[strtolower($column->getName())] = $column;

883 884
            if (! isset($newColumnNames[$oldColumnName])) {
                continue;
885
            }
886 887

            $newColumnNames[$oldColumnName] = $column->getQuotedName($this);
888 889 890 891 892 893 894
        }

        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
            }

895 896
            if (isset($columns[$oldColumnName])) {
                unset($columns[$oldColumnName]);
897 898
            }

899
            $columns[strtolower($columnDiff->column->getName())] = $columnDiff->column;
900

901 902
            if (! isset($newColumnNames[$oldColumnName])) {
                continue;
903
            }
904 905

            $newColumnNames[$oldColumnName] = $columnDiff->column->getQuotedName($this);
906 907
        }

908 909 910 911
        foreach ($diff->addedColumns as $columnName => $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }
912

913
            $columns[strtolower($columnName)] = $column;
914 915
        }

916
        $sql      = [];
917
        $tableSql = [];
918

919 920
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            $dataTable = new Table('__temp__' . $table->getName());
921

922
            $newTable = new Table($table->getQuotedName($this), $columns, $this->getPrimaryIndexInAlteredTable($diff), [], $this->getForeignKeysInAlteredTable($diff), $table->getOptions());
923
            $newTable->addOption('alter', true);
924

925 926 927
            $sql = $this->getPreAlterTableIndexForeignKeySQL($diff);
            //$sql = array_merge($sql, $this->getCreateTableSQL($dataTable, 0));
            $sql[] = sprintf('CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', $dataTable->getQuotedName($this), implode(', ', $oldColumnNames), $table->getQuotedName($this));
928
            $sql[] = $this->getDropTableSQL($fromTable);
929

930
            $sql   = array_merge($sql, $this->getCreateTableSQL($newTable));
931 932 933
            $sql[] = sprintf('INSERT INTO %s (%s) SELECT %s FROM %s', $newTable->getQuotedName($this), implode(', ', $newColumnNames), implode(', ', $oldColumnNames), $dataTable->getQuotedName($this));
            $sql[] = $this->getDropTableSQL($dataTable);

Sergei Morozov's avatar
Sergei Morozov committed
934 935 936 937 938 939 940 941
            $newName = $diff->getNewName();

            if ($newName !== false) {
                $sql[] = sprintf(
                    'ALTER TABLE %s RENAME TO %s',
                    $newTable->getQuotedName($this),
                    $newName->getQuotedName($this)
                );
942 943
            }

944 945
            $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff));
        }
946 947 948 949

        return array_merge($sql, $tableSql, $columnSql);
    }

Benjamin Morel's avatar
Benjamin Morel committed
950
    /**
951
     * @return string[]|false
Benjamin Morel's avatar
Benjamin Morel committed
952
     */
953 954
    private function getSimpleAlterTableSQL(TableDiff $diff)
    {
955 956
        // Suppress changes on integer type autoincrement columns.
        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
957
            if (! $columnDiff->fromColumn instanceof Column ||
958 959
                ! $columnDiff->column instanceof Column ||
                ! $columnDiff->column->getAutoincrement() ||
960
                ! $columnDiff->column->getType() instanceof Types\IntegerType
961 962 963 964
            ) {
                continue;
            }

965
            if (! $columnDiff->hasChanged('type') && $columnDiff->hasChanged('unsigned')) {
966 967 968 969 970
                unset($diff->changedColumns[$oldColumnName]);

                continue;
            }

971
            $fromColumnType = $columnDiff->fromColumn->getType();
972

973 974
            if (! ($fromColumnType instanceof Types\SmallIntType) && ! ($fromColumnType instanceof Types\BigIntType)) {
                continue;
975
            }
976 977

            unset($diff->changedColumns[$oldColumnName]);
978 979
        }

980
        if (! empty($diff->renamedColumns) || ! empty($diff->addedForeignKeys) || ! empty($diff->addedIndexes)
981 982
                || ! empty($diff->changedColumns) || ! empty($diff->changedForeignKeys) || ! empty($diff->changedIndexes)
                || ! empty($diff->removedColumns) || ! empty($diff->removedForeignKeys) || ! empty($diff->removedIndexes)
983
                || ! empty($diff->renamedIndexes)
984 985 986 987 988 989
        ) {
            return false;
        }

        $table = new Table($diff->name);

990 991
        $sql       = [];
        $tableSql  = [];
992
        $columnSql = [];
993

Benjamin Morel's avatar
Benjamin Morel committed
994
        foreach ($diff->addedColumns as $column) {
995 996 997 998
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

999
            $field = array_merge(['unique' => null, 'autoincrement' => null, 'default' => null], $column->toArray());
1000
            $type  = $field['type'];
1001 1002
            switch (true) {
                case isset($field['columnDefinition']) || $field['autoincrement'] || $field['unique']:
1003 1004 1005
                case $type instanceof Types\DateTimeType && $field['default'] === $this->getCurrentTimestampSQL():
                case $type instanceof Types\DateType && $field['default'] === $this->getCurrentDateSQL():
                case $type instanceof Types\TimeType && $field['default'] === $this->getCurrentTimeSQL():
1006 1007 1008 1009
                    return false;
            }

            $field['name'] = $column->getQuotedName($this);
1010
            if ($type instanceof Types\StringType && $field['length'] === null) {
1011 1012 1013
                $field['length'] = 255;
            }

1014
            $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN ' . $this->getColumnDeclarationSQL($field['name'], $field);
1015 1016
        }

1017
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
1018
            if ($diff->newName !== false) {
1019
                $newTable = new Identifier($diff->newName);
1020
                $sql[]    = 'ALTER TABLE ' . $table->getQuotedName($this) . ' RENAME TO ' . $newTable->getQuotedName($this);
1021 1022
            }
        }
1023 1024 1025 1026

        return array_merge($sql, $tableSql, $columnSql);
    }

Benjamin Morel's avatar
Benjamin Morel committed
1027
    /**
1028
     * @return string[]
Benjamin Morel's avatar
Benjamin Morel committed
1029
     */
1030
    private function getColumnNamesInAlteredTable(TableDiff $diff)
1031
    {
1032
        $columns = [];
1033

1034 1035 1036 1037 1038 1039
        foreach ($diff->fromTable->getColumns() as $columnName => $column) {
            $columns[strtolower($columnName)] = $column->getName();
        }

        foreach ($diff->removedColumns as $columnName => $column) {
            $columnName = strtolower($columnName);
1040 1041
            if (! isset($columns[$columnName])) {
                continue;
1042
            }
1043 1044

            unset($columns[$columnName]);
1045 1046 1047
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
1048
            $columnName                          = $column->getName();
1049
            $columns[strtolower($oldColumnName)] = $columnName;
1050
            $columns[strtolower($columnName)]    = $columnName;
1051 1052 1053
        }

        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
1054
            $columnName                          = $columnDiff->column->getName();
1055
            $columns[strtolower($oldColumnName)] = $columnName;
1056
            $columns[strtolower($columnName)]    = $columnName;
1057 1058
        }

Sergei Morozov's avatar
Sergei Morozov committed
1059 1060
        foreach ($diff->addedColumns as $column) {
            $columnName                       = $column->getName();
1061 1062 1063 1064 1065 1066
            $columns[strtolower($columnName)] = $columnName;
        }

        return $columns;
    }

Benjamin Morel's avatar
Benjamin Morel committed
1067
    /**
1068
     * @return Index[]
Benjamin Morel's avatar
Benjamin Morel committed
1069
     */
1070 1071
    private function getIndexesInAlteredTable(TableDiff $diff)
    {
1072
        $indexes     = $diff->fromTable->getIndexes();
1073 1074 1075
        $columnNames = $this->getColumnNamesInAlteredTable($diff);

        foreach ($indexes as $key => $index) {
1076
            foreach ($diff->renamedIndexes as $oldIndexName => $renamedIndex) {
1077 1078
                if (strtolower($key) !== strtolower($oldIndexName)) {
                    continue;
1079
                }
1080 1081

                unset($indexes[$key]);
1082 1083
            }

1084
            $changed      = false;
1085
            $indexColumns = [];
1086 1087
            foreach ($index->getColumns() as $columnName) {
                $normalizedColumnName = strtolower($columnName);
1088
                if (! isset($columnNames[$normalizedColumnName])) {
1089 1090 1091
                    unset($indexes[$key]);
                    continue 2;
                }
1092 1093 1094 1095 1096 1097 1098

                $indexColumns[] = $columnNames[$normalizedColumnName];
                if ($columnName === $columnNames[$normalizedColumnName]) {
                    continue;
                }

                $changed = true;
1099 1100
            }

1101 1102
            if (! $changed) {
                continue;
1103
            }
1104 1105

            $indexes[$key] = new Index($index->getName(), $indexColumns, $index->isUnique(), $index->isPrimary(), $index->getFlags());
1106 1107 1108
        }

        foreach ($diff->removedIndexes as $index) {
1109
            $indexName = strtolower($index->getName());
1110 1111
            if (! strlen($indexName) || ! isset($indexes[$indexName])) {
                continue;
1112
            }
1113 1114

            unset($indexes[$indexName]);
1115 1116
        }

1117
        foreach (array_merge($diff->changedIndexes, $diff->addedIndexes, $diff->renamedIndexes) as $index) {
1118 1119 1120 1121 1122
            $indexName = strtolower($index->getName());
            if (strlen($indexName)) {
                $indexes[$indexName] = $index;
            } else {
                $indexes[] = $index;
1123 1124 1125
            }
        }

1126 1127 1128
        return $indexes;
    }

Benjamin Morel's avatar
Benjamin Morel committed
1129
    /**
1130
     * @return ForeignKeyConstraint[]
Benjamin Morel's avatar
Benjamin Morel committed
1131
     */
1132 1133 1134 1135 1136 1137
    private function getForeignKeysInAlteredTable(TableDiff $diff)
    {
        $foreignKeys = $diff->fromTable->getForeignKeys();
        $columnNames = $this->getColumnNamesInAlteredTable($diff);

        foreach ($foreignKeys as $key => $constraint) {
1138
            $changed      = false;
1139
            $localColumns = [];
1140 1141
            foreach ($constraint->getLocalColumns() as $columnName) {
                $normalizedColumnName = strtolower($columnName);
1142
                if (! isset($columnNames[$normalizedColumnName])) {
1143 1144 1145
                    unset($foreignKeys[$key]);
                    continue 2;
                }
1146 1147 1148 1149 1150 1151 1152

                $localColumns[] = $columnNames[$normalizedColumnName];
                if ($columnName === $columnNames[$normalizedColumnName]) {
                    continue;
                }

                $changed = true;
1153 1154
            }

1155 1156
            if (! $changed) {
                continue;
1157
            }
1158 1159

            $foreignKeys[$key] = new ForeignKeyConstraint($localColumns, $constraint->getForeignTableName(), $constraint->getForeignColumns(), $constraint->getName(), $constraint->getOptions());
1160 1161 1162
        }

        foreach ($diff->removedForeignKeys as $constraint) {
Sergei Morozov's avatar
Sergei Morozov committed
1163 1164 1165 1166
            if (! $constraint instanceof ForeignKeyConstraint) {
                $constraint = new Identifier($constraint);
            }

1167
            $constraintName = strtolower($constraint->getName());
1168 1169
            if (! strlen($constraintName) || ! isset($foreignKeys[$constraintName])) {
                continue;
1170
            }
1171 1172

            unset($foreignKeys[$constraintName]);
1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186
        }

        foreach (array_merge($diff->changedForeignKeys, $diff->addedForeignKeys) as $constraint) {
            $constraintName = strtolower($constraint->getName());
            if (strlen($constraintName)) {
                $foreignKeys[$constraintName] = $constraint;
            } else {
                $foreignKeys[] = $constraint;
            }
        }

        return $foreignKeys;
    }

Benjamin Morel's avatar
Benjamin Morel committed
1187
    /**
1188
     * @return Index[]
Benjamin Morel's avatar
Benjamin Morel committed
1189
     */
1190 1191
    private function getPrimaryIndexInAlteredTable(TableDiff $diff)
    {
1192
        $primaryIndex = [];
1193 1194

        foreach ($this->getIndexesInAlteredTable($diff) as $index) {
1195 1196
            if (! $index->isPrimary()) {
                continue;
1197
            }
1198 1199

            $primaryIndex = [$index->getName() => $index];
1200 1201 1202 1203
        }

        return $primaryIndex;
    }
1204
}