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
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;
26
use function trim;
27

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

44 45
    /**
     * {@inheritDoc}
46 47
     *
     * @deprecated Use application-generated UUIDs instead
48 49 50 51 52 53 54 55 56
     */
    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))";
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

138 139 140 141 142 143 144 145 146 147 148 149 150 151
        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 . " || '";
152
        }
153 154

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

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

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

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

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

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

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

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

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

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

226
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
227 228
    }

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

241
        return 'TINYINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
242 243
    }

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

254
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
255 256
    }

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

269
        return 'MEDIUMINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
270 271
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

370 371 372
        return $query;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return $query;
    }

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

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

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

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

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

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

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

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

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

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

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

606 607
        return 0;
    }
608

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

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

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

630 631 632
    /**
     * {@inheritDoc}
     */
633 634
    protected function initializeDoctrineTypeMappings()
    {
635
        $this->doctrineTypeMapping = [
636 637 638 639 640 641 642 643 644 645 646 647 648 649 650
            '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',
651
            'longvarchar'      => 'string',
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666
            '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',
667
            'blob'             => 'blob',
668
        ];
669
    }
670

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

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

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

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

        return $sql;
    }

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

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

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

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

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

        return $sql;
    }
726 727 728 729 730 731

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

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

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

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

754 755
        return $tableName;
    }
756 757

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

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

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

784 785 786 787 788 789 790 791 792 793 794 795 796
    /**
     * {@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)
    {
797
        throw new DBALException('Sqlite platform does not support alter foreign key, the table must be fully recreated using getAlterTableSQL.');
798 799 800 801 802 803 804
    }

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

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

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

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

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

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

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

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

        $table = clone $fromTable;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                continue;
            }

985
            $fromColumnType = $columnDiff->fromColumn->getType();
986

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

            unset($diff->changedColumns[$oldColumnName]);
992 993
        }

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

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

1004 1005
        $sql       = [];
        $tableSql  = [];
1006
        $columnSql = [];
1007

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

1013
            $field = array_merge(['unique' => null, 'autoincrement' => null, 'default' => null], $column->toArray());
1014
            $type  = $field['type'];
1015 1016
            switch (true) {
                case isset($field['columnDefinition']) || $field['autoincrement'] || $field['unique']:
1017 1018 1019
                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():
1020 1021 1022 1023
                    return false;
            }

            $field['name'] = $column->getQuotedName($this);
1024
            if ($type instanceof Types\StringType && $field['length'] === null) {
1025 1026 1027
                $field['length'] = 255;
            }

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

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

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

Benjamin Morel's avatar
Benjamin Morel committed
1041
    /**
1042
     * @return string[]
Benjamin Morel's avatar
Benjamin Morel committed
1043
     */
1044
    private function getColumnNamesInAlteredTable(TableDiff $diff)
1045
    {
1046
        $columns = [];
1047

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

        foreach ($diff->removedColumns as $columnName => $column) {
            $columnName = strtolower($columnName);
1054 1055
            if (! isset($columns[$columnName])) {
                continue;
1056
            }
1057 1058

            unset($columns[$columnName]);
1059 1060 1061
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
1062
            $columnName                          = $column->getName();
1063
            $columns[strtolower($oldColumnName)] = $columnName;
1064
            $columns[strtolower($columnName)]    = $columnName;
1065 1066 1067
        }

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

Sergei Morozov's avatar
Sergei Morozov committed
1073 1074
        foreach ($diff->addedColumns as $column) {
            $columnName                       = $column->getName();
1075 1076 1077 1078 1079 1080
            $columns[strtolower($columnName)] = $columnName;
        }

        return $columns;
    }

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

        foreach ($indexes as $key => $index) {
1090
            foreach ($diff->renamedIndexes as $oldIndexName => $renamedIndex) {
1091 1092
                if (strtolower($key) !== strtolower($oldIndexName)) {
                    continue;
1093
                }
1094 1095

                unset($indexes[$key]);
1096 1097
            }

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

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

                $changed = true;
1113 1114
            }

1115 1116
            if (! $changed) {
                continue;
1117
            }
1118 1119

            $indexes[$key] = new Index($index->getName(), $indexColumns, $index->isUnique(), $index->isPrimary(), $index->getFlags());
1120 1121 1122
        }

        foreach ($diff->removedIndexes as $index) {
1123
            $indexName = strtolower($index->getName());
1124 1125
            if (! strlen($indexName) || ! isset($indexes[$indexName])) {
                continue;
1126
            }
1127 1128

            unset($indexes[$indexName]);
1129 1130
        }

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

1140 1141 1142
        return $indexes;
    }

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

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

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

                $changed = true;
1167 1168
            }

1169 1170
            if (! $changed) {
                continue;
1171
            }
1172 1173

            $foreignKeys[$key] = new ForeignKeyConstraint($localColumns, $constraint->getForeignTableName(), $constraint->getForeignColumns(), $constraint->getName(), $constraint->getOptions());
1174 1175 1176
        }

        foreach ($diff->removedForeignKeys as $constraint) {
Sergei Morozov's avatar
Sergei Morozov committed
1177 1178 1179 1180
            if (! $constraint instanceof ForeignKeyConstraint) {
                $constraint = new Identifier($constraint);
            }

1181
            $constraintName = strtolower($constraint->getName());
1182 1183
            if (! strlen($constraintName) || ! isset($foreignKeys[$constraintName])) {
                continue;
1184
            }
1185 1186

            unset($foreignKeys[$constraintName]);
1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200
        }

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

        foreach ($this->getIndexesInAlteredTable($diff) as $index) {
1209 1210
            if (! $index->isPrimary()) {
                continue;
1211
            }
1212 1213

            $primaryIndex = [$index->getName() => $index];
1214 1215 1216 1217
        }

        return $primaryIndex;
    }
1218
}