SqlitePlatform.php 34.7 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

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
     *
     * @deprecated Use application-generated UUIDs instead
49 50 51 52 53 54 55 56 57
     */
    public function getGuidExpression()
    {
        return "HEX(RANDOMBLOB(4)) || '-' || HEX(RANDOMBLOB(2)) || '-4' || "
            . "SUBSTR(HEX(RANDOMBLOB(2)), 2) || '-' || "
            . "SUBSTR('89AB', 1 + (ABS(RANDOM()) % 4), 1) || "
            . "SUBSTR(HEX(RANDOMBLOB(2)), 2) || '-' || HEX(RANDOMBLOB(6))";
    }

58
    /**
59 60 61
     * @param string $type
     *
     * @return string
62 63 64 65 66 67
     */
    public function getNowExpression($type = 'timestamp')
    {
        switch ($type) {
            case 'time':
                return 'time(\'now\')';
Sergei Morozov's avatar
Sergei Morozov committed
68

69 70
            case 'date':
                return 'date(\'now\')';
Sergei Morozov's avatar
Sergei Morozov committed
71

72 73 74 75 76 77
            case 'timestamp':
            default:
                return 'datetime(\'now\')';
        }
    }

78
    /**
79
     * {@inheritDoc}
80
     */
81
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
82
    {
83
        $trimChar = $char !== false ? ', ' . $char : '';
84

85
        switch ($pos) {
86
            case TrimMode::LEADING:
87 88 89
                $trimFn = 'LTRIM';
                break;

90
            case TrimMode::TRAILING:
91 92 93 94 95
                $trimFn = 'RTRIM';
                break;

            default:
                $trimFn = 'TRIM';
96 97 98 99 100
        }

        return $trimFn . '(' . $str . $trimChar . ')';
    }

101
    /**
102
     * {@inheritDoc}
103 104 105 106 107 108 109 110
     *
     * 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 . ')';
        }
111

112 113 114
        return 'SUBSTR(' . $value . ', ' . $position . ', LENGTH(' . $value . '))';
    }

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

124
        return 'LOCATE(' . $str . ', ' . $substr . ', ' . $startPos . ')';
125 126
    }

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

139 140 141 142 143 144 145 146 147 148 149 150 151 152
        switch ($unit) {
            case DateIntervalUnit::WEEK:
                $interval *= 7;
                $unit      = DateIntervalUnit::DAY;
                break;

            case DateIntervalUnit::QUARTER:
                $interval *= 3;
                $unit      = DateIntervalUnit::MONTH;
                break;
        }

        if (! is_numeric($interval)) {
            $interval = "' || " . $interval . " || '";
153
        }
154 155

        return 'DATE(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')";
156 157
    }

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

166 167 168
    /**
     * {@inheritDoc}
     */
169
    protected function _getTransactionIsolationLevelSQL($level)
romanb's avatar
romanb committed
170 171
    {
        switch ($level) {
172
            case TransactionIsolationLevel::READ_UNCOMMITTED:
173
                return '0';
Sergei Morozov's avatar
Sergei Morozov committed
174

175 176 177
            case TransactionIsolationLevel::READ_COMMITTED:
            case TransactionIsolationLevel::REPEATABLE_READ:
            case TransactionIsolationLevel::SERIALIZABLE:
178
                return '1';
Sergei Morozov's avatar
Sergei Morozov committed
179

romanb's avatar
romanb committed
180
            default:
181
                return parent::_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
182 183
        }
    }
184

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

193
    /**
194
     * {@inheritDoc}
195
     */
196 197
    public function prefersIdentityColumns()
    {
198 199
        return true;
    }
200 201

    /**
202
     * {@inheritDoc}
203
     */
204
    public function getBooleanTypeDeclarationSQL(array $column)
205 206 207
    {
        return 'BOOLEAN';
    }
208

209
    /**
210
     * {@inheritDoc}
211
     */
212
    public function getIntegerTypeDeclarationSQL(array $column)
213
    {
214
        return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column);
215 216
    }

217
    /**
218
     * {@inheritDoc}
219
     */
220
    public function getBigIntTypeDeclarationSQL(array $column)
221
    {
222 223 224
        // SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT columns
        if (! empty($column['autoincrement'])) {
            return $this->getIntegerTypeDeclarationSQL($column);
225 226
        }

227
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
228 229
    }

230
    /**
231
     * @param array<string, mixed> $column
232 233
     *
     * @return string
234
     */
235
    public function getTinyIntTypeDeclarationSql(array $column)
236
    {
237 238 239
        // SQLite autoincrement is implicit for INTEGER PKs, but not for TINYINT columns
        if (! empty($column['autoincrement'])) {
            return $this->getIntegerTypeDeclarationSQL($column);
240 241
        }

242
        return 'TINYINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
243 244
    }

245
    /**
246
     * {@inheritDoc}
247
     */
248
    public function getSmallIntTypeDeclarationSQL(array $column)
249
    {
250 251 252
        // SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT columns
        if (! empty($column['autoincrement'])) {
            return $this->getIntegerTypeDeclarationSQL($column);
253 254
        }

255
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
256 257
    }

258
    /**
259
     * @param array<string, mixed> $column
260 261
     *
     * @return string
262
     */
263
    public function getMediumIntTypeDeclarationSql(array $column)
264
    {
265 266 267
        // SQLite autoincrement is implicit for INTEGER PKs, but not for MEDIUMINT columns
        if (! empty($column['autoincrement'])) {
            return $this->getIntegerTypeDeclarationSQL($column);
268 269
        }

270
        return 'MEDIUMINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
271 272
    }

273
    /**
274
     * {@inheritDoc}
275
     */
276
    public function getDateTimeTypeDeclarationSQL(array $column)
277 278 279
    {
        return 'DATETIME';
    }
280

281
    /**
282
     * {@inheritDoc}
283
     */
284
    public function getDateTypeDeclarationSQL(array $column)
285 286 287
    {
        return 'DATE';
    }
288

289
    /**
290
     * {@inheritDoc}
291
     */
292
    public function getTimeTypeDeclarationSQL(array $column)
293 294 295 296
    {
        return 'TIME';
    }

297
    /**
298
     * {@inheritDoc}
299
     */
300
    protected function _getCommonIntegerTypeDeclarationSQL(array $column)
301
    {
302
        // sqlite autoincrement is only possible for the primary key
303
        if (! empty($column['autoincrement'])) {
304
            return ' PRIMARY KEY AUTOINCREMENT';
305 306
        }

307
        return ! empty($column['unsigned']) ? ' UNSIGNED' : '';
308 309
    }

310 311 312 313 314 315
    /**
     * {@inheritDoc}
     */
    public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey)
    {
        return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint(
316 317 318
            $foreignKey->getQuotedLocalColumns($this),
            str_replace('.', '__', $foreignKey->getQuotedForeignTableName($this)),
            $foreignKey->getQuotedForeignColumns($this),
319 320 321 322 323
            $foreignKey->getName(),
            $foreignKey->getOptions()
        ));
    }

324
    /**
325
     * {@inheritDoc}
326
     */
327
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
328
    {
329
        $name        = str_replace('.', '__', $name);
330
        $queryFields = $this->getColumnDeclarationListSQL($columns);
331

332 333 334 335 336 337
        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
338
        $queryFields .= $this->getNonAutoincrementPrimaryKeyDefinition($columns, $options);
339

340 341
        if (isset($options['foreignKeys'])) {
            foreach ($options['foreignKeys'] as $foreignKey) {
342
                $queryFields .= ', ' . $this->getForeignKeyDeclarationSQL($foreignKey);
343 344 345
            }
        }

346 347 348 349 350 351 352 353
        $tableComment = '';
        if (isset($options['comment'])) {
            $comment = trim($options['comment'], " '");

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

        $query = ['CREATE TABLE ' . $name . ' ' . $tableComment . '(' . $queryFields . ')'];
354

355
        if (isset($options['alter']) && $options['alter'] === true) {
356
            return $query;
357
        }
358

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

365
        if (isset($options['unique']) && ! empty($options['unique'])) {
Benjamin Morel's avatar
Benjamin Morel committed
366
            foreach ($options['unique'] as $indexDef) {
367 368 369 370
                $query[] = $this->getCreateIndexSQL($indexDef, $name);
            }
        }

371 372 373
        return $query;
    }

Timo Bakx's avatar
Timo Bakx committed
374 375 376
    /**
     * Generate a PRIMARY KEY definition if no autoincrement value is used
     *
377 378
     * @param mixed[][] $columns
     * @param mixed[]   $options
Timo Bakx's avatar
Timo Bakx committed
379
     */
380
    private function getNonAutoincrementPrimaryKeyDefinition(array $columns, array $options): string
Timo Bakx's avatar
Timo Bakx committed
381 382 383 384 385 386 387 388
    {
        if (empty($options['primary'])) {
            return '';
        }

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

        foreach ($keyColumns as $keyColumn) {
389
            if (! empty($columns[$keyColumn]['autoincrement'])) {
Timo Bakx's avatar
Timo Bakx committed
390 391 392 393 394 395 396
                return '';
            }
        }

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

397
    /**
398
     * {@inheritDoc}
399
     */
400
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
401 402 403 404
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
                : ($length ? 'VARCHAR(' . $length . ')' : 'TEXT');
    }
405

Steve Müller's avatar
Steve Müller committed
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return 'BLOB';
    }

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

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

430 431 432
    /**
     * {@inheritDoc}
     */
433
    public function getClobTypeDeclarationSQL(array $column)
434 435 436
    {
        return 'CLOB';
    }
437

438 439 440
    /**
     * {@inheritDoc}
     */
441
    public function getListTableConstraintsSQL($table)
442
    {
443
        $table = str_replace('.', '__', $table);
444

445 446 447 448
        return sprintf(
            "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = %s AND sql NOT NULL ORDER BY name",
            $this->quoteStringLiteral($table)
        );
449 450
    }

451 452 453
    /**
     * {@inheritDoc}
     */
454
    public function getListTableColumnsSQL($table, $database = null)
455
    {
456
        $table = str_replace('.', '__', $table);
457

458
        return sprintf('PRAGMA table_info(%s)', $this->quoteStringLiteral($table));
459 460
    }

461 462 463
    /**
     * {@inheritDoc}
     */
464
    public function getListTableIndexesSQL($table, $database = null)
465
    {
466
        $table = str_replace('.', '__', $table);
467

468
        return sprintf('PRAGMA index_list(%s)', $this->quoteStringLiteral($table));
469 470
    }

471 472 473
    /**
     * {@inheritDoc}
     */
474
    public function getListTablesSQL()
475
    {
jsor's avatar
jsor committed
476
        return "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND name != 'geometry_columns' AND name != 'spatial_ref_sys' "
477
             . 'UNION ALL SELECT name FROM sqlite_temp_master '
478 479 480
             . "WHERE type = 'table' ORDER BY name";
    }

481 482 483
    /**
     * {@inheritDoc}
     */
484
    public function getListViewsSQL($database)
485 486 487 488
    {
        return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL";
    }

489 490 491
    /**
     * {@inheritDoc}
     */
492
    public function getCreateViewSQL($name, $sql)
493 494 495 496
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

497 498 499
    /**
     * {@inheritDoc}
     */
500
    public function getDropViewSQL($name)
501
    {
502
        return 'DROP VIEW ' . $name;
503 504
    }

505 506 507
    /**
     * {@inheritDoc}
     */
508 509 510 511
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
    {
        $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey);

512 513
        $query .= ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false ? ' ' : ' NOT ') . 'DEFERRABLE';
        $query .= ' INITIALLY ' . ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false ? 'DEFERRED' : 'IMMEDIATE');
514 515 516 517

        return $query;
    }

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

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

534 535 536 537 538 539 540 541
    /**
     * {@inheritDoc}
     */
    public function supportsInlineColumnComments()
    {
        return true;
    }

542
    /**
543
     * {@inheritDoc}
544 545 546 547 548
     */
    public function getName()
    {
        return 'sqlite';
    }
549 550

    /**
551
     * {@inheritDoc}
552
     */
553
    public function getTruncateTableSQL($tableName, $cascade = false)
554
    {
555
        $tableIdentifier = new Identifier($tableName);
556
        $tableName       = str_replace('.', '__', $tableIdentifier->getQuotedName($this));
557

558
        return 'DELETE FROM ' . $tableName;
559
    }
560 561

    /**
Benjamin Morel's avatar
Benjamin Morel committed
562
     * User-defined function for Sqlite that is used with PDO::sqliteCreateFunction().
563
     *
564
     * @param int|float $value
565
     *
566 567
     * @return float
     */
568
    public static function udfSqrt($value)
569 570 571 572 573
    {
        return sqrt($value);
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
574
     * User-defined function for Sqlite that implements MOD(a, b).
575
     *
576 577
     * @param int $a
     * @param int $b
578
     *
579
     * @return int
580
     */
581
    public static function udfMod($a, $b)
582
    {
583
        return $a % $b;
584
    }
585 586

    /**
587 588 589
     * @param string $str
     * @param string $substr
     * @param int    $offset
590
     *
591
     * @return int
592
     */
593
    public static function udfLocate($str, $substr, $offset = 0)
594
    {
595 596 597
        // 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
598
            $offset -= 1;
599 600
        }

601
        $pos = strpos($str, $substr, $offset);
602

603
        if ($pos !== false) {
604
            return $pos + 1;
605
        }
606

607 608
        return 0;
    }
609

610 611 612
    /**
     * {@inheritDoc}
     */
613
    public function getForUpdateSQL()
614 615 616
    {
        return '';
    }
617

618 619 620 621 622
    /**
     * {@inheritDoc}
     */
    public function getInlineColumnCommentSQL($comment)
    {
Steve Müller's avatar
Steve Müller committed
623
        return '--' . str_replace("\n", "\n--", $comment) . "\n";
624 625
    }

626
    private function getInlineTableCommentSQL(string $comment): string
627 628 629 630
    {
        return $this->getInlineColumnCommentSQL($comment);
    }

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

672 673 674
    /**
     * {@inheritDoc}
     */
675 676
    protected function getReservedKeywordsClass()
    {
677
        return Keywords\SQLiteKeywords::class;
678
    }
679

680 681 682 683 684
    /**
     * {@inheritDoc}
     */
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
685
        if (! $diff->fromTable instanceof Table) {
686 687 688
            throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema');
        }

689
        $sql = [];
690
        foreach ($diff->fromTable->getIndexes() as $index) {
691 692
            if ($index->isPrimary()) {
                continue;
693
            }
694 695

            $sql[] = $this->getDropIndexSQL($index, $diff->name);
696 697 698 699 700 701 702 703 704 705
        }

        return $sql;
    }

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

710
        $sql       = [];
Sergei Morozov's avatar
Sergei Morozov committed
711 712 713 714 715 716
        $tableName = $diff->getNewName();

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

Benjamin Morel's avatar
Benjamin Morel committed
717
        foreach ($this->getIndexesInAlteredTable($diff) as $index) {
718 719 720 721
            if ($index->isPrimary()) {
                continue;
            }

722
            $sql[] = $this->getCreateIndexSQL($index, $tableName->getQuotedName($this));
723 724 725 726
        }

        return $sql;
    }
727 728 729 730 731 732

    /**
     * {@inheritDoc}
     */
    protected function doModifyLimitQuery($query, $limit, $offset)
    {
733
        if ($limit === null && $offset > 0) {
734 735 736 737 738
            return $query . ' LIMIT -1 OFFSET ' . $offset;
        }

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

740
    /**
741
     * {@inheritDoc}
742
     */
743
    public function getBlobTypeDeclarationSQL(array $column)
744 745 746
    {
        return 'BLOB';
    }
747

748 749 750
    /**
     * {@inheritDoc}
     */
751 752
    public function getTemporaryTableName($tableName)
    {
753
        $tableName = str_replace('.', '__', $tableName);
754

755 756
        return $tableName;
    }
757 758

    /**
759 760
     * {@inheritDoc}
     *
761 762 763 764 765 766 767 768 769 770
     * 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;
    }
771

772 773 774 775
    /**
     * {@inheritDoc}
     */
    public function supportsForeignKeyConstraints()
776 777 778 779
    {
        return true;
    }

780
    public function supportsCreateDropForeignKeyConstraints(): bool
781 782 783 784
    {
        return false;
    }

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)
    {
798
        throw new DBALException('Sqlite platform does not support alter foreign key, the table must be fully recreated using getAlterTableSQL.');
799 800 801 802 803 804 805
    }

    /**
     * {@inheritdoc}
     */
    public function getDropForeignKeySQL($foreignKey, $table)
    {
806
        throw new DBALException('Sqlite platform does not support alter foreign key, the table must be fully recreated using getAlterTableSQL.');
807 808
    }

809 810 811 812 813 814 815
    /**
     * {@inheritDoc}
     */
    public function getCreateConstraintSQL(Constraint $constraint, $table)
    {
        throw new DBALException('Sqlite platform does not support alter constraint.');
    }
816 817 818

    /**
     * {@inheritDoc}
819 820
     *
     * @param int|null $createFlags
821 822 823
     */
    public function getCreateTableSQL(Table $table, $createFlags = null)
    {
824
        $createFlags = $createFlags ?? self::CREATE_INDEXES | self::CREATE_FOREIGNKEYS;
825 826 827

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

829
    /**
830 831 832 833
     * @param string      $table
     * @param string|null $database
     *
     * @return string
834 835 836 837 838
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
        $table = str_replace('.', '__', $table);

839
        return sprintf('PRAGMA foreign_key_list(%s)', $this->quoteStringLiteral($table));
840 841 842 843 844 845 846
    }

    /**
     * {@inheritDoc}
     */
    public function getAlterTableSQL(TableDiff $diff)
    {
847
        $sql = $this->getSimpleAlterTableSQL($diff);
848
        if ($sql !== false) {
849 850 851
            return $sql;
        }

852
        $fromTable = $diff->fromTable;
853
        if (! $fromTable instanceof Table) {
854 855 856 857 858
            throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema');
        }

        $table = clone $fromTable;

859
        $columns        = [];
860 861
        $oldColumnNames = [];
        $newColumnNames = [];
862
        $columnSql      = [];
863 864

        foreach ($table->getColumns() as $columnName => $column) {
865 866
            $columnName                  = strtolower($columnName);
            $columns[$columnName]        = $column;
867 868 869
            $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this);
        }

870 871 872 873 874
        foreach ($diff->removedColumns as $columnName => $column) {
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
            }

875
            $columnName = strtolower($columnName);
876 877
            if (! isset($columns[$columnName])) {
                continue;
878
            }
879 880 881 882 883 884

            unset(
                $columns[$columnName],
                $oldColumnNames[$columnName],
                $newColumnNames[$columnName]
            );
885 886 887 888 889 890 891
        }

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

892 893 894 895 896 897 898
            $oldColumnName = strtolower($oldColumnName);
            if (isset($columns[$oldColumnName])) {
                unset($columns[$oldColumnName]);
            }

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

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

            $newColumnNames[$oldColumnName] = $column->getQuotedName($this);
904 905 906 907 908 909 910
        }

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

911 912
            if (isset($columns[$oldColumnName])) {
                unset($columns[$oldColumnName]);
913 914
            }

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

917 918
            if (! isset($newColumnNames[$oldColumnName])) {
                continue;
919
            }
920 921

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

924 925 926 927
        foreach ($diff->addedColumns as $columnName => $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }
928

929
            $columns[strtolower($columnName)] = $column;
930 931
        }

932
        $sql      = [];
933
        $tableSql = [];
934 935
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
            $dataTable = new Table('__temp__' . $table->getName());
936

937
            $newTable = new Table($table->getQuotedName($this), $columns, $this->getPrimaryIndexInAlteredTable($diff), $this->getForeignKeysInAlteredTable($diff), 0, $table->getOptions());
938
            $newTable->addOption('alter', true);
939

940 941 942
            $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));
943
            $sql[] = $this->getDropTableSQL($fromTable);
944

945
            $sql   = array_merge($sql, $this->getCreateTableSQL($newTable));
946 947 948
            $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
949 950 951 952 953 954 955 956
            $newName = $diff->getNewName();

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

959 960
            $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff));
        }
961 962 963 964

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

Benjamin Morel's avatar
Benjamin Morel committed
965
    /**
966
     * @return string[]|false
Benjamin Morel's avatar
Benjamin Morel committed
967
     */
968 969
    private function getSimpleAlterTableSQL(TableDiff $diff)
    {
970 971
        // Suppress changes on integer type autoincrement columns.
        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
972 973
            if (
                ! $columnDiff->fromColumn instanceof Column ||
974 975
                ! $columnDiff->column instanceof Column ||
                ! $columnDiff->column->getAutoincrement() ||
976
                ! $columnDiff->column->getType() instanceof Types\IntegerType
977 978 979 980
            ) {
                continue;
            }

981
            if (! $columnDiff->hasChanged('type') && $columnDiff->hasChanged('unsigned')) {
982 983 984 985 986
                unset($diff->changedColumns[$oldColumnName]);

                continue;
            }

987
            $fromColumnType = $columnDiff->fromColumn->getType();
988

989 990
            if (! ($fromColumnType instanceof Types\SmallIntType) && ! ($fromColumnType instanceof Types\BigIntType)) {
                continue;
991
            }
992 993

            unset($diff->changedColumns[$oldColumnName]);
994 995
        }

996 997
        if (
            ! empty($diff->renamedColumns) || ! empty($diff->addedForeignKeys) || ! empty($diff->addedIndexes)
998 999
                || ! empty($diff->changedColumns) || ! empty($diff->changedForeignKeys) || ! empty($diff->changedIndexes)
                || ! empty($diff->removedColumns) || ! empty($diff->removedForeignKeys) || ! empty($diff->removedIndexes)
1000
                || ! empty($diff->renamedIndexes)
1001 1002 1003 1004 1005 1006
        ) {
            return false;
        }

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

1007 1008
        $sql       = [];
        $tableSql  = [];
1009
        $columnSql = [];
1010

Benjamin Morel's avatar
Benjamin Morel committed
1011
        foreach ($diff->addedColumns as $column) {
1012 1013 1014 1015
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

1016 1017 1018
            $definition = array_merge(['unique' => null, 'autoincrement' => null, 'default' => null], $column->toArray());
            $type       = $definition['type'];

1019
            switch (true) {
1020 1021 1022 1023
                case isset($definition['columnDefinition']) || $definition['autoincrement'] || $definition['unique']:
                case $type instanceof Types\DateTimeType && $definition['default'] === $this->getCurrentTimestampSQL():
                case $type instanceof Types\DateType && $definition['default'] === $this->getCurrentDateSQL():
                case $type instanceof Types\TimeType && $definition['default'] === $this->getCurrentTimeSQL():
1024 1025 1026
                    return false;
            }

1027 1028 1029
            $definition['name'] = $column->getQuotedName($this);
            if ($type instanceof Types\StringType && $definition['length'] === null) {
                $definition['length'] = 255;
1030 1031
            }

1032
            $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN ' . $this->getColumnDeclarationSQL($definition['name'], $definition);
1033 1034
        }

1035
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
1036
            if ($diff->newName !== false) {
1037
                $newTable = new Identifier($diff->newName);
1038
                $sql[]    = 'ALTER TABLE ' . $table->getQuotedName($this) . ' RENAME TO ' . $newTable->getQuotedName($this);
1039 1040
            }
        }
1041 1042 1043 1044

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

Benjamin Morel's avatar
Benjamin Morel committed
1045
    /**
1046
     * @return string[]
Benjamin Morel's avatar
Benjamin Morel committed
1047
     */
1048
    private function getColumnNamesInAlteredTable(TableDiff $diff)
1049
    {
1050
        $columns = [];
1051

1052 1053 1054 1055 1056 1057
        foreach ($diff->fromTable->getColumns() as $columnName => $column) {
            $columns[strtolower($columnName)] = $column->getName();
        }

        foreach ($diff->removedColumns as $columnName => $column) {
            $columnName = strtolower($columnName);
1058 1059
            if (! isset($columns[$columnName])) {
                continue;
1060
            }
1061 1062

            unset($columns[$columnName]);
1063 1064 1065
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
1066
            $columnName                          = $column->getName();
1067
            $columns[strtolower($oldColumnName)] = $columnName;
1068
            $columns[strtolower($columnName)]    = $columnName;
1069 1070 1071
        }

        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
1072
            $columnName                          = $columnDiff->column->getName();
1073
            $columns[strtolower($oldColumnName)] = $columnName;
1074
            $columns[strtolower($columnName)]    = $columnName;
1075 1076
        }

Sergei Morozov's avatar
Sergei Morozov committed
1077 1078
        foreach ($diff->addedColumns as $column) {
            $columnName                       = $column->getName();
1079 1080 1081 1082 1083 1084
            $columns[strtolower($columnName)] = $columnName;
        }

        return $columns;
    }

Benjamin Morel's avatar
Benjamin Morel committed
1085
    /**
1086
     * @return Index[]
Benjamin Morel's avatar
Benjamin Morel committed
1087
     */
1088 1089
    private function getIndexesInAlteredTable(TableDiff $diff)
    {
1090
        $indexes     = $diff->fromTable->getIndexes();
1091 1092 1093
        $columnNames = $this->getColumnNamesInAlteredTable($diff);

        foreach ($indexes as $key => $index) {
1094
            foreach ($diff->renamedIndexes as $oldIndexName => $renamedIndex) {
1095 1096
                if (strtolower($key) !== strtolower($oldIndexName)) {
                    continue;
1097
                }
1098 1099

                unset($indexes[$key]);
1100 1101
            }

1102
            $changed      = false;
1103
            $indexColumns = [];
1104 1105
            foreach ($index->getColumns() as $columnName) {
                $normalizedColumnName = strtolower($columnName);
1106
                if (! isset($columnNames[$normalizedColumnName])) {
1107 1108 1109
                    unset($indexes[$key]);
                    continue 2;
                }
1110 1111 1112 1113 1114 1115 1116

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

                $changed = true;
1117 1118
            }

1119 1120
            if (! $changed) {
                continue;
1121
            }
1122 1123

            $indexes[$key] = new Index($index->getName(), $indexColumns, $index->isUnique(), $index->isPrimary(), $index->getFlags());
1124 1125 1126
        }

        foreach ($diff->removedIndexes as $index) {
1127
            $indexName = strtolower($index->getName());
1128 1129
            if (! strlen($indexName) || ! isset($indexes[$indexName])) {
                continue;
1130
            }
1131 1132

            unset($indexes[$indexName]);
1133 1134
        }

1135
        foreach (array_merge($diff->changedIndexes, $diff->addedIndexes, $diff->renamedIndexes) as $index) {
1136 1137 1138 1139 1140
            $indexName = strtolower($index->getName());
            if (strlen($indexName)) {
                $indexes[$indexName] = $index;
            } else {
                $indexes[] = $index;
1141 1142 1143
            }
        }

1144 1145 1146
        return $indexes;
    }

Benjamin Morel's avatar
Benjamin Morel committed
1147
    /**
1148
     * @return ForeignKeyConstraint[]
Benjamin Morel's avatar
Benjamin Morel committed
1149
     */
1150 1151 1152 1153 1154 1155
    private function getForeignKeysInAlteredTable(TableDiff $diff)
    {
        $foreignKeys = $diff->fromTable->getForeignKeys();
        $columnNames = $this->getColumnNamesInAlteredTable($diff);

        foreach ($foreignKeys as $key => $constraint) {
1156
            $changed      = false;
1157
            $localColumns = [];
1158 1159
            foreach ($constraint->getLocalColumns() as $columnName) {
                $normalizedColumnName = strtolower($columnName);
1160
                if (! isset($columnNames[$normalizedColumnName])) {
1161 1162 1163
                    unset($foreignKeys[$key]);
                    continue 2;
                }
1164 1165 1166 1167 1168 1169 1170

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

                $changed = true;
1171 1172
            }

1173 1174
            if (! $changed) {
                continue;
1175
            }
1176 1177

            $foreignKeys[$key] = new ForeignKeyConstraint($localColumns, $constraint->getForeignTableName(), $constraint->getForeignColumns(), $constraint->getName(), $constraint->getOptions());
1178 1179 1180
        }

        foreach ($diff->removedForeignKeys as $constraint) {
Sergei Morozov's avatar
Sergei Morozov committed
1181 1182 1183 1184
            if (! $constraint instanceof ForeignKeyConstraint) {
                $constraint = new Identifier($constraint);
            }

1185
            $constraintName = strtolower($constraint->getName());
1186 1187
            if (! strlen($constraintName) || ! isset($foreignKeys[$constraintName])) {
                continue;
1188
            }
1189 1190

            unset($foreignKeys[$constraintName]);
1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204
        }

        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
1205
    /**
1206
     * @return Index[]
Benjamin Morel's avatar
Benjamin Morel committed
1207
     */
1208 1209
    private function getPrimaryIndexInAlteredTable(TableDiff $diff)
    {
1210
        $primaryIndex = [];
1211 1212

        foreach ($this->getIndexesInAlteredTable($diff) as $index) {
1213 1214
            if (! $index->isPrimary()) {
                continue;
1215
            }
1216 1217

            $primaryIndex = [$index->getName() => $index];
1218 1219 1220 1221
        }

        return $primaryIndex;
    }
1222
}