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

20
namespace Doctrine\DBAL\Platforms;
21

22
use Doctrine\DBAL\DBALException;
23
use Doctrine\DBAL\Schema\Column;
24
use Doctrine\DBAL\Schema\Constraint;
25
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
26
use Doctrine\DBAL\Schema\Identifier;
27 28 29
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
30
use Doctrine\DBAL\TransactionIsolationLevel;
31
use Doctrine\DBAL\Types;
32 33 34 35 36 37 38 39 40 41 42
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;
43

44
/**
45 46
 * The SqlitePlatform class describes the specifics and dialects of the SQLite
 * database platform.
47
 *
Benjamin Morel's avatar
Benjamin Morel committed
48
 * @since  2.0
49
 * @author Roman Borschel <roman@code-factory.org>
50
 * @author Benjamin Eberlei <kontakt@beberlei.de>
51
 * @author Martin Hasoň <martin.hason@gmail.com>
Benjamin Morel's avatar
Benjamin Morel committed
52
 * @todo   Rename: SQLitePlatform
53
 */
54
class SqlitePlatform extends AbstractPlatform
55 56
{
    /**
57
     * {@inheritDoc}
58 59 60
     */
    public function getRegexpExpression()
    {
61
        return 'REGEXP';
62 63
    }

64 65
    /**
     * {@inheritDoc}
66 67
     *
     * @deprecated Use application-generated UUIDs instead
68 69 70 71 72 73 74 75 76
     */
    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))";
    }

77
    /**
78
     * {@inheritDoc}
79 80 81 82 83 84 85 86 87 88 89 90 91 92
     */
    public function getNowExpression($type = 'timestamp')
    {
        switch ($type) {
            case 'time':
                return 'time(\'now\')';
            case 'date':
                return 'date(\'now\')';
            case 'timestamp':
            default:
                return 'datetime(\'now\')';
        }
    }

93
    /**
94
     * {@inheritDoc}
95
     */
96
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
97 98 99
    {
        $trimChar = ($char != false) ? (', ' . $char) : '';

100
        switch ($pos) {
101
            case TrimMode::LEADING:
102 103 104
                $trimFn = 'LTRIM';
                break;

105
            case TrimMode::TRAILING:
106 107 108 109 110
                $trimFn = 'RTRIM';
                break;

            default:
                $trimFn = 'TRIM';
111 112 113 114 115
        }

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

116
    /**
117
     * {@inheritDoc}
118 119 120 121 122 123 124 125
     *
     * 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 . ')';
        }
126

127 128 129
        return 'SUBSTR(' . $value . ', ' . $position . ', LENGTH(' . $value . '))';
    }

130
    /**
131
     * {@inheritDoc}
132 133 134 135 136 137
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos == false) {
            return 'LOCATE('.$str.', '.$substr.')';
        }
138 139

        return 'LOCATE('.$str.', '.$substr.', '.$startPos.')';
140 141
    }

142
    /**
143
     * {@inheritdoc}
144
     */
145
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
146
    {
147
        switch ($unit) {
148 149 150
            case DateIntervalUnit::SECOND:
            case DateIntervalUnit::MINUTE:
            case DateIntervalUnit::HOUR:
151
                return "DATETIME(" . $date . ",'" . $operator . $interval . " " . $unit . "')";
152

153 154
            default:
                switch ($unit) {
155
                    case DateIntervalUnit::WEEK:
156
                        $interval *= 7;
157
                        $unit      = DateIntervalUnit::DAY;
158 159
                        break;

160
                    case DateIntervalUnit::QUARTER:
161
                        $interval *= 3;
162
                        $unit      = DateIntervalUnit::MONTH;
163 164 165
                        break;
                }

Fogs's avatar
Fogs committed
166
                if (! is_numeric($interval)) {
167 168 169
                    $interval = "' || " . $interval . " || '";
                }

170 171
                return "DATE(" . $date . ",'" . $operator . $interval . " " . $unit . "')";
        }
172 173
    }

174 175 176
    /**
     * {@inheritDoc}
     */
177
    public function getDateDiffExpression($date1, $date2)
178
    {
179
        return sprintf("JULIANDAY(%s, 'start of day') - JULIANDAY(%s, 'start of day')", $date1, $date2);
180 181
    }

182 183 184
    /**
     * {@inheritDoc}
     */
185
    protected function _getTransactionIsolationLevelSQL($level)
romanb's avatar
romanb committed
186 187
    {
        switch ($level) {
188
            case TransactionIsolationLevel::READ_UNCOMMITTED:
romanb's avatar
romanb committed
189
                return 0;
190 191 192
            case TransactionIsolationLevel::READ_COMMITTED:
            case TransactionIsolationLevel::REPEATABLE_READ:
            case TransactionIsolationLevel::SERIALIZABLE:
romanb's avatar
romanb committed
193 194
                return 1;
            default:
195
                return parent::_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
196 197
        }
    }
198

199 200 201
    /**
     * {@inheritDoc}
     */
202
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
203
    {
204
        return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
205
    }
206

207
    /**
208
     * {@inheritDoc}
209
     */
210 211
    public function prefersIdentityColumns()
    {
212 213
        return true;
    }
214 215

    /**
216
     * {@inheritDoc}
217
     */
218
    public function getBooleanTypeDeclarationSQL(array $field)
219 220 221
    {
        return 'BOOLEAN';
    }
222

223
    /**
224
     * {@inheritDoc}
225
     */
226
    public function getIntegerTypeDeclarationSQL(array $field)
227
    {
228
        return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($field);
229 230
    }

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

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

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

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

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

267
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
268 269
    }

270
    /**
271
     * {@inheritDoc}
272
     */
273 274
    public function getMediumIntTypeDeclarationSql(array $field)
    {
275 276 277 278 279
        //  SQLite autoincrement is implicit for INTEGER PKs, but not for MEDIUMINT fields.
        if ( ! empty($field['autoincrement'])) {
            return $this->getIntegerTypeDeclarationSQL($field);
        }

280
        return 'MEDIUMINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
281 282
    }

283
    /**
284
     * {@inheritDoc}
285
     */
286
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
287 288 289
    {
        return 'DATETIME';
    }
290

291
    /**
292
     * {@inheritDoc}
293
     */
294
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
295 296 297
    {
        return 'DATE';
    }
298

299
    /**
300
     * {@inheritDoc}
301
     */
302
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
303 304 305 306
    {
        return 'TIME';
    }

307
    /**
308
     * {@inheritDoc}
309
     */
310
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
311
    {
312
        // sqlite autoincrement is only possible for the primary key
313
        if ( ! empty($columnDef['autoincrement'])) {
314
            return ' PRIMARY KEY AUTOINCREMENT';
315 316
        }

317
        return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : '';
318 319
    }

320 321 322 323 324 325
    /**
     * {@inheritDoc}
     */
    public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey)
    {
        return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint(
326 327 328
            $foreignKey->getQuotedLocalColumns($this),
            str_replace('.', '__', $foreignKey->getQuotedForeignTableName($this)),
            $foreignKey->getQuotedForeignColumns($this),
329 330 331 332 333
            $foreignKey->getName(),
            $foreignKey->getOptions()
        ));
    }

334
    /**
335
     * {@inheritDoc}
336
     */
337
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
338
    {
339
        $name = str_replace('.', '__', $name);
340
        $queryFields = $this->getColumnDeclarationListSQL($columns);
341

342 343 344 345 346 347
        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
348
        $queryFields .= $this->getNonAutoincrementPrimaryKeyDefinition($columns, $options);
349

350 351 352 353 354 355
        if (isset($options['foreignKeys'])) {
            foreach ($options['foreignKeys'] as $foreignKey) {
                $queryFields.= ', '.$this->getForeignKeyDeclarationSQL($foreignKey);
            }
        }

356
        $query = ['CREATE TABLE ' . $name . ' (' . $queryFields . ')'];
357

358 359
        if (isset($options['alter']) && true === $options['alter']) {
            return $query;
360
        }
361

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

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

374 375 376
        return $query;
    }

Timo Bakx's avatar
Timo Bakx committed
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
    /**
     * Generate a PRIMARY KEY definition if no autoincrement value is used
     *
     * @param string[] $columns
     * @param mixed[]  $options
     */
    private function getNonAutoincrementPrimaryKeyDefinition(array $columns, array $options) : string
    {
        if (empty($options['primary'])) {
            return '';
        }

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

        foreach ($keyColumns as $keyColumn) {
            if (isset($columns[$keyColumn]['autoincrement']) && ! empty($columns[$keyColumn]['autoincrement'])) {
                return '';
            }
        }

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

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

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

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

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

433 434 435
    /**
     * {@inheritDoc}
     */
436
    public function getClobTypeDeclarationSQL(array $field)
437 438 439
    {
        return 'CLOB';
    }
440

441 442 443
    /**
     * {@inheritDoc}
     */
444
    public function getListTableConstraintsSQL($table)
445
    {
446
        $table = str_replace('.', '__', $table);
447
        $table = $this->quoteStringLiteral($table);
448

449
        return "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = $table AND sql NOT NULL ORDER BY name";
450 451
    }

452 453 454
    /**
     * {@inheritDoc}
     */
455
    public function getListTableColumnsSQL($table, $currentDatabase = null)
456
    {
457
        $table = str_replace('.', '__', $table);
458
        $table = $this->quoteStringLiteral($table);
459

460
        return "PRAGMA table_info($table)";
461 462
    }

463 464 465
    /**
     * {@inheritDoc}
     */
466
    public function getListTableIndexesSQL($table, $currentDatabase = null)
467
    {
468
        $table = str_replace('.', '__', $table);
469
        $table = $this->quoteStringLiteral($table);
470

471
        return "PRAGMA index_list($table)";
472 473
    }

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

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

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

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

508 509 510
    /**
     * {@inheritDoc}
     */
511 512 513 514
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
    {
        $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey);

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

        return $query;
    }

521 522 523
    /**
     * {@inheritDoc}
     */
524 525 526 527 528
    public function supportsIdentityColumns()
    {
        return true;
    }

529 530 531 532 533 534 535 536
    /**
     * {@inheritDoc}
     */
    public function supportsColumnCollation()
    {
        return true;
    }

537 538 539 540 541 542 543 544
    /**
     * {@inheritDoc}
     */
    public function supportsInlineColumnComments()
    {
        return true;
    }

545
    /**
546
     * {@inheritDoc}
547 548 549 550 551
     */
    public function getName()
    {
        return 'sqlite';
    }
552 553

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

561
        return 'DELETE FROM ' . $tableName;
562
    }
563 564

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

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

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

604
        $pos = strpos($str, $substr, $offset);
605

606
        if ($pos !== false) {
607
            return $pos + 1;
608
        }
609

610 611
        return 0;
    }
612

613 614 615
    /**
     * {@inheritDoc}
     */
616 617 618 619
    public function getForUpdateSql()
    {
        return '';
    }
620

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

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

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

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

687
        $sql = [];
688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705
        foreach ($diff->fromTable->getIndexes() as $index) {
            if ( ! $index->isPrimary()) {
                $sql[] = $this->getDropIndexSQL($index, $diff->name);
            }
        }

        return $sql;
    }

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

706
        $sql = [];
707
        $tableName = $diff->newName ? $diff->getNewName(): $diff->getName($this);
Benjamin Morel's avatar
Benjamin Morel committed
708
        foreach ($this->getIndexesInAlteredTable($diff) as $index) {
709 710 711 712
            if ($index->isPrimary()) {
                continue;
            }

713
            $sql[] = $this->getCreateIndexSQL($index, $tableName->getQuotedName($this));
714 715 716 717
        }

        return $sql;
    }
718 719 720 721 722 723

    /**
     * {@inheritDoc}
     */
    protected function doModifyLimitQuery($query, $limit, $offset)
    {
724
        if ($limit === null && $offset > 0) {
725 726 727 728 729
            return $query . ' LIMIT -1 OFFSET ' . $offset;
        }

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

731
    /**
732
     * {@inheritDoc}
733 734 735 736 737
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
        return 'BLOB';
    }
738

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

746 747
        return $tableName;
    }
748 749

    /**
750 751
     * {@inheritDoc}
     *
752 753 754 755 756 757 758 759 760 761
     * 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;
    }
762

763 764 765 766 767 768 769 770
    /**
     * {@inheritDoc}
     */
    public function supportsForeignKeyConstraints()
    {
        return false;
    }

771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
    /**
     * {@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.');
    }

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

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

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

813 814 815 816 817 818
    /**
     * {@inheritDoc}
     */
    public function getListTableForeignKeysSQL($table, $database = null)
    {
        $table = str_replace('.', '__', $table);
819
        $table = $this->quoteStringLiteral($table);
820

821
        return "PRAGMA foreign_key_list($table)";
822 823 824 825 826 827 828
    }

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

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

        $table = clone $fromTable;

841 842 843 844
        $columns = [];
        $oldColumnNames = [];
        $newColumnNames = [];
        $columnSql = [];
845 846 847 848 849 850 851

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

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

857 858
            $columnName = strtolower($columnName);
            if (isset($columns[$columnName])) {
859 860 861 862 863
                unset(
                    $columns[$columnName],
                    $oldColumnNames[$columnName],
                    $newColumnNames[$columnName]
                );
864
            }
865 866 867 868 869 870 871
        }

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

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

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

            if (isset($newColumnNames[$oldColumnName])) {
                $newColumnNames[$oldColumnName] = $column->getQuotedName($this);
            }
882 883 884 885 886 887 888
        }

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

889 890
            if (isset($columns[$oldColumnName])) {
                unset($columns[$oldColumnName]);
891 892
            }

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

895 896
            if (isset($newColumnNames[$oldColumnName])) {
                $newColumnNames[$oldColumnName] = $columnDiff->column->getQuotedName($this);
897 898 899
            }
        }

900 901 902 903
        foreach ($diff->addedColumns as $columnName => $column) {
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }
904

905
            $columns[strtolower($columnName)] = $column;
906 907
        }

908 909
        $sql = [];
        $tableSql = [];
910
        if ( ! $this->onSchemaAlterTable($diff, $tableSql)) {
911 912
            $dataTable = new Table('__temp__'.$table->getName());

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

916 917 918
            $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));
919
            $sql[] = $this->getDropTableSQL($fromTable);
920 921 922 923 924 925

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

            if ($diff->newName && $diff->newName != $diff->name) {
926
                $renamedTable = $diff->getNewName();
927 928 929
                $sql[] = 'ALTER TABLE '.$newTable->getQuotedName($this).' RENAME TO '.$renamedTable->getQuotedName($this);
            }

930 931
            $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff));
        }
932 933 934 935

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

Benjamin Morel's avatar
Benjamin Morel committed
936 937 938
    /**
     * @param \Doctrine\DBAL\Schema\TableDiff $diff
     *
939
     * @return string[]|false
Benjamin Morel's avatar
Benjamin Morel committed
940
     */
941 942
    private function getSimpleAlterTableSQL(TableDiff $diff)
    {
943 944 945 946 947
        // Suppress changes on integer type autoincrement columns.
        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
            if ( ! $columnDiff->fromColumn instanceof Column ||
                ! $columnDiff->column instanceof Column ||
                ! $columnDiff->column->getAutoincrement() ||
948
                ! $columnDiff->column->getType() instanceof Types\IntegerType
949 950 951 952 953 954 955 956 957 958
            ) {
                continue;
            }

            if ( ! $columnDiff->hasChanged('type') && $columnDiff->hasChanged('unsigned')) {
                unset($diff->changedColumns[$oldColumnName]);

                continue;
            }

959
            $fromColumnType = $columnDiff->fromColumn->getType();
960

961
            if ($fromColumnType instanceof Types\SmallIntType || $fromColumnType instanceof Types\BigIntType) {
962 963 964 965
                unset($diff->changedColumns[$oldColumnName]);
            }
        }

966 967 968
        if ( ! empty($diff->renamedColumns) || ! empty($diff->addedForeignKeys) || ! empty($diff->addedIndexes)
                || ! empty($diff->changedColumns) || ! empty($diff->changedForeignKeys) || ! empty($diff->changedIndexes)
                || ! empty($diff->removedColumns) || ! empty($diff->removedForeignKeys) || ! empty($diff->removedIndexes)
969
                || ! empty($diff->renamedIndexes)
970 971 972 973 974 975
        ) {
            return false;
        }

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

976 977 978
        $sql = [];
        $tableSql = [];
        $columnSql = [];
979

Benjamin Morel's avatar
Benjamin Morel committed
980
        foreach ($diff->addedColumns as $column) {
981 982 983 984
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
            }

985
            $field = array_merge(['unique' => null, 'autoincrement' => null, 'default' => null], $column->toArray());
986
            $type = $field['type'];
987 988
            switch (true) {
                case isset($field['columnDefinition']) || $field['autoincrement'] || $field['unique']:
989 990 991
                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():
992 993 994 995
                    return false;
            }

            $field['name'] = $column->getQuotedName($this);
996
            if ($type instanceof Types\StringType && $field['length'] === null) {
997 998 999 1000 1001 1002 1003 1004
                $field['length'] = 255;
            }

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

        if ( ! $this->onSchemaAlterTable($diff, $tableSql)) {
            if ($diff->newName !== false) {
1005
                $newTable = new Identifier($diff->newName);
1006 1007 1008
                $sql[] = 'ALTER TABLE '.$table->getQuotedName($this).' RENAME TO '.$newTable->getQuotedName($this);
            }
        }
1009 1010 1011 1012

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

Benjamin Morel's avatar
Benjamin Morel committed
1013 1014 1015 1016 1017
    /**
     * @param \Doctrine\DBAL\Schema\TableDiff $diff
     *
     * @return array
     */
1018
    private function getColumnNamesInAlteredTable(TableDiff $diff)
1019
    {
1020
        $columns = [];
1021

1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
        foreach ($diff->fromTable->getColumns() as $columnName => $column) {
            $columns[strtolower($columnName)] = $column->getName();
        }

        foreach ($diff->removedColumns as $columnName => $column) {
            $columnName = strtolower($columnName);
            if (isset($columns[$columnName])) {
                unset($columns[$columnName]);
            }
        }

        foreach ($diff->renamedColumns as $oldColumnName => $column) {
            $columnName = $column->getName();
            $columns[strtolower($oldColumnName)] = $columnName;
            $columns[strtolower($columnName)] = $columnName;
        }

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

        foreach ($diff->addedColumns as $columnName => $column) {
            $columns[strtolower($columnName)] = $columnName;
        }

        return $columns;
    }

Benjamin Morel's avatar
Benjamin Morel committed
1052 1053 1054 1055 1056
    /**
     * @param \Doctrine\DBAL\Schema\TableDiff $diff
     *
     * @return \Doctrine\DBAL\Schema\Index[]
     */
1057 1058 1059 1060 1061 1062
    private function getIndexesInAlteredTable(TableDiff $diff)
    {
        $indexes = $diff->fromTable->getIndexes();
        $columnNames = $this->getColumnNamesInAlteredTable($diff);

        foreach ($indexes as $key => $index) {
1063 1064 1065 1066 1067 1068
            foreach ($diff->renamedIndexes as $oldIndexName => $renamedIndex) {
                if (strtolower($key) === strtolower($oldIndexName)) {
                    unset($indexes[$key]);
                }
            }

1069
            $changed = false;
1070
            $indexColumns = [];
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085
            foreach ($index->getColumns() as $columnName) {
                $normalizedColumnName = strtolower($columnName);
                if ( ! isset($columnNames[$normalizedColumnName])) {
                    unset($indexes[$key]);
                    continue 2;
                } else {
                    $indexColumns[] = $columnNames[$normalizedColumnName];
                    if ($columnName !== $columnNames[$normalizedColumnName]) {
                        $changed = true;
                    }
                }
            }

            if ($changed) {
                $indexes[$key] = new Index($index->getName(), $indexColumns, $index->isUnique(), $index->isPrimary(), $index->getFlags());
1086 1087 1088 1089
            }
        }

        foreach ($diff->removedIndexes as $index) {
1090 1091 1092
            $indexName = strtolower($index->getName());
            if (strlen($indexName) && isset($indexes[$indexName])) {
                unset($indexes[$indexName]);
1093 1094 1095
            }
        }

1096
        foreach (array_merge($diff->changedIndexes, $diff->addedIndexes, $diff->renamedIndexes) as $index) {
1097 1098 1099 1100 1101
            $indexName = strtolower($index->getName());
            if (strlen($indexName)) {
                $indexes[$indexName] = $index;
            } else {
                $indexes[] = $index;
1102 1103 1104
            }
        }

1105 1106 1107
        return $indexes;
    }

Benjamin Morel's avatar
Benjamin Morel committed
1108 1109 1110 1111 1112
    /**
     * @param \Doctrine\DBAL\Schema\TableDiff $diff
     *
     * @return array
     */
1113 1114 1115 1116 1117 1118 1119
    private function getForeignKeysInAlteredTable(TableDiff $diff)
    {
        $foreignKeys = $diff->fromTable->getForeignKeys();
        $columnNames = $this->getColumnNamesInAlteredTable($diff);

        foreach ($foreignKeys as $key => $constraint) {
            $changed = false;
1120
            $localColumns = [];
1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157
            foreach ($constraint->getLocalColumns() as $columnName) {
                $normalizedColumnName = strtolower($columnName);
                if ( ! isset($columnNames[$normalizedColumnName])) {
                    unset($foreignKeys[$key]);
                    continue 2;
                } else {
                    $localColumns[] = $columnNames[$normalizedColumnName];
                    if ($columnName !== $columnNames[$normalizedColumnName]) {
                        $changed = true;
                    }
                }
            }

            if ($changed) {
                $foreignKeys[$key] = new ForeignKeyConstraint($localColumns, $constraint->getForeignTableName(), $constraint->getForeignColumns(), $constraint->getName(), $constraint->getOptions());
            }
        }

        foreach ($diff->removedForeignKeys as $constraint) {
            $constraintName = strtolower($constraint->getName());
            if (strlen($constraintName) && isset($foreignKeys[$constraintName])) {
                unset($foreignKeys[$constraintName]);
            }
        }

        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
1158 1159 1160 1161 1162
    /**
     * @param \Doctrine\DBAL\Schema\TableDiff $diff
     *
     * @return array
     */
1163 1164
    private function getPrimaryIndexInAlteredTable(TableDiff $diff)
    {
1165
        $primaryIndex = [];
1166 1167

        foreach ($this->getIndexesInAlteredTable($diff) as $index) {
1168
            if ($index->isPrimary()) {
1169
                $primaryIndex = [$index->getName() => $index];
1170 1171 1172 1173 1174
            }
        }

        return $primaryIndex;
    }
1175
}