Failed Conditions
Pull Request — 3.0.x (#3987)
by Grégoire
33:07 queued 11s
created

supportsCreateDropForeignKeyConstraints()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Doctrine\DBAL\Platforms;
4
5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\Schema\Constraint;
7
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
8
use Doctrine\DBAL\Schema\Identifier;
9
use Doctrine\DBAL\Schema\Index;
10
use Doctrine\DBAL\Schema\Table;
11
use Doctrine\DBAL\Schema\TableDiff;
12
use Doctrine\DBAL\TransactionIsolationLevel;
13
use Doctrine\DBAL\Types;
14
use function array_merge;
15
use function array_unique;
16
use function array_values;
17
use function implode;
18
use function is_numeric;
19
use function sprintf;
20
use function sqrt;
21
use function str_replace;
22
use function strlen;
23
use function strpos;
24
use function strtolower;
25
use function trim;
26
27
/**
28
 * The SqlitePlatform class describes the specifics and dialects of the SQLite
29
 * database platform.
30
 *
31
 * @todo   Rename: SQLitePlatform
32
 */
33
class SqlitePlatform extends AbstractPlatform
34
{
35
    /**
36
     * {@inheritDoc}
37
     */
38 22
    public function getRegexpExpression()
39
    {
40 22
        return 'REGEXP';
41
    }
42
43
    /**
44
     * {@inheritDoc}
45
     *
46
     * @deprecated Use application-generated UUIDs instead
47
     */
48 2
    public function getGuidExpression()
49
    {
50
        return "HEX(RANDOMBLOB(4)) || '-' || HEX(RANDOMBLOB(2)) || '-4' || "
51
            . "SUBSTR(HEX(RANDOMBLOB(2)), 2) || '-' || "
52
            . "SUBSTR('89AB', 1 + (ABS(RANDOM()) % 4), 1) || "
53 2
            . "SUBSTR(HEX(RANDOMBLOB(2)), 2) || '-' || HEX(RANDOMBLOB(6))";
54
    }
55
56
    /**
57
     * @param string $type
58
     *
59
     * @return string
60
     */
61
    public function getNowExpression($type = 'timestamp')
62
    {
63
        switch ($type) {
64
            case 'time':
65
                return 'time(\'now\')';
66
67
            case 'date':
68
                return 'date(\'now\')';
69
70
            case 'timestamp':
71
            default:
72
                return 'datetime(\'now\')';
73
        }
74
    }
75
76
    /**
77
     * {@inheritDoc}
78
     */
79 36
    public function getTrimExpression($str, $pos = TrimMode::UNSPECIFIED, $char = false)
80
    {
81 36
        $trimChar = $char !== false ? ', ' . $char : '';
82
83 36
        switch ($pos) {
84
            case TrimMode::LEADING:
85 9
                $trimFn = 'LTRIM';
86 9
                break;
87
88
            case TrimMode::TRAILING:
89 9
                $trimFn = 'RTRIM';
90 9
                break;
91
92
            default:
93 18
                $trimFn = 'TRIM';
94
        }
95
96 36
        return $trimFn . '(' . $str . $trimChar . ')';
97
    }
98
99
    /**
100
     * {@inheritDoc}
101
     *
102
     * SQLite only supports the 2 parameter variant of this function
103
     */
104 22
    public function getSubstringExpression($value, $position, $length = null)
105
    {
106 22
        if ($length !== null) {
107 22
            return 'SUBSTR(' . $value . ', ' . $position . ', ' . $length . ')';
108
        }
109
110 22
        return 'SUBSTR(' . $value . ', ' . $position . ', LENGTH(' . $value . '))';
111
    }
112
113
    /**
114
     * {@inheritDoc}
115
     */
116 1
    public function getLocateExpression($str, $substr, $startPos = false)
117
    {
118 1
        if ($startPos === false) {
119 1
            return 'LOCATE(' . $str . ', ' . $substr . ')';
120
        }
121
122 1
        return 'LOCATE(' . $str . ', ' . $substr . ', ' . $startPos . ')';
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 46
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
129
    {
130 46
        switch ($unit) {
131
            case DateIntervalUnit::SECOND:
132
            case DateIntervalUnit::MINUTE:
133
            case DateIntervalUnit::HOUR:
134 1
                return 'DATETIME(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')";
135
        }
136
137 46
        switch ($unit) {
138
            case DateIntervalUnit::WEEK:
139 1
                $interval *= 7;
140 1
                $unit      = DateIntervalUnit::DAY;
141 1
                break;
142
143
            case DateIntervalUnit::QUARTER:
144 1
                $interval *= 3;
145 1
                $unit      = DateIntervalUnit::MONTH;
146 1
                break;
147
        }
148
149 46
        if (! is_numeric($interval)) {
150 23
            $interval = "' || " . $interval . " || '";
151
        }
152
153 46
        return 'DATE(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')";
154
    }
155
156
    /**
157
     * {@inheritDoc}
158
     */
159 3
    public function getDateDiffExpression($date1, $date2)
160
    {
161 3
        return sprintf("JULIANDAY(%s, 'start of day') - JULIANDAY(%s, 'start of day')", $date1, $date2);
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167 22
    protected function _getTransactionIsolationLevelSQL($level)
168
    {
169 1
        switch ($level) {
170 21
            case TransactionIsolationLevel::READ_UNCOMMITTED:
171 22
                return '0';
172
173 21
            case TransactionIsolationLevel::READ_COMMITTED:
174 21
            case TransactionIsolationLevel::REPEATABLE_READ:
175 21
            case TransactionIsolationLevel::SERIALIZABLE:
176 22
                return '1';
177
178
            default:
179
                return parent::_getTransactionIsolationLevelSQL($level);
180
        }
181
    }
182
183
    /**
184
     * {@inheritDoc}
185
     */
186 22
    public function getSetTransactionIsolationSQL($level)
187
    {
188 22
        return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level);
189
    }
190
191
    /**
192
     * {@inheritDoc}
193
     */
194 23
    public function prefersIdentityColumns()
195
    {
196 23
        return true;
197
    }
198
199
    /**
200
     * {@inheritDoc}
201
     */
202 38
    public function getBooleanTypeDeclarationSQL(array $field)
203
    {
204 38
        return 'BOOLEAN';
205
    }
206
207
    /**
208
     * {@inheritDoc}
209
     */
210 649
    public function getIntegerTypeDeclarationSQL(array $field)
211
    {
212 649
        return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($field);
213
    }
214
215
    /**
216
     * {@inheritDoc}
217
     */
218 39
    public function getBigIntTypeDeclarationSQL(array $field)
219
    {
220
        //  SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT fields.
221 39
        if (! empty($field['autoincrement'])) {
222 24
            return $this->getIntegerTypeDeclarationSQL($field);
223
        }
224
225 37
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
226
    }
227
228
    /**
229
     * @param array<string, mixed> $field
230
     *
231
     * @return string
232
     */
233 44
    public function getTinyIntTypeDeclarationSQL(array $field)
234
    {
235
        //  SQLite autoincrement is implicit for INTEGER PKs, but not for TINYINT fields.
236 44
        if (! empty($field['autoincrement'])) {
237 44
            return $this->getIntegerTypeDeclarationSQL($field);
238
        }
239
240 22
        return 'TINYINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
241
    }
242
243
    /**
244
     * {@inheritDoc}
245
     */
246 25
    public function getSmallIntTypeDeclarationSQL(array $field)
247
    {
248
        //  SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT fields.
249 25
        if (! empty($field['autoincrement'])) {
250 24
            return $this->getIntegerTypeDeclarationSQL($field);
251
        }
252
253 23
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
254
    }
255
256
    /**
257
     * @param array<string, mixed> $field
258
     *
259
     * @return string
260
     */
261 22
    public function getMediumIntTypeDeclarationSQL(array $field)
262
    {
263
        //  SQLite autoincrement is implicit for INTEGER PKs, but not for MEDIUMINT fields.
264 22
        if (! empty($field['autoincrement'])) {
265 22
            return $this->getIntegerTypeDeclarationSQL($field);
266
        }
267
268 22
        return 'MEDIUMINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
269
    }
270
271
    /**
272
     * {@inheritDoc}
273
     */
274 23
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
275
    {
276 23
        return 'DATETIME';
277
    }
278
279
    /**
280
     * {@inheritDoc}
281
     */
282 20
    public function getDateTypeDeclarationSQL(array $fieldDeclaration)
283
    {
284 20
        return 'DATE';
285
    }
286
287
    /**
288
     * {@inheritDoc}
289
     */
290 19
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration)
291
    {
292 19
        return 'TIME';
293
    }
294
295
    /**
296
     * {@inheritDoc}
297
     */
298 649
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
299
    {
300
        // sqlite autoincrement is only possible for the primary key
301 649
        if (! empty($columnDef['autoincrement'])) {
302 232
            return ' PRIMARY KEY AUTOINCREMENT';
303
        }
304
305 572
        return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : '';
306
    }
307
308
    /**
309
     * {@inheritDoc}
310
     */
311 122
    public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey)
312
    {
313 122
        return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint(
314 122
            $foreignKey->getQuotedLocalColumns($this),
315 122
            str_replace('.', '__', $foreignKey->getQuotedForeignTableName($this)),
316 122
            $foreignKey->getQuotedForeignColumns($this),
317 122
            $foreignKey->getName(),
318 122
            $foreignKey->getOptions()
319
        ));
320
    }
321
322
    /**
323
     * {@inheritDoc}
324
     */
325 727
    protected function _getCreateTableSQL($name, array $columns, array $options = [])
326
    {
327 727
        $name        = str_replace('.', '__', $name);
328 727
        $queryFields = $this->getColumnDeclarationListSQL($columns);
329
330 727
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
331
            foreach ($options['uniqueConstraints'] as $constraintName => $definition) {
332
                $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition);
333
            }
334
        }
335
336 727
        $queryFields .= $this->getNonAutoincrementPrimaryKeyDefinition($columns, $options);
337
338 727
        if (isset($options['foreignKeys'])) {
339 727
            foreach ($options['foreignKeys'] as $foreignKey) {
340 122
                $queryFields .= ', ' . $this->getForeignKeyDeclarationSQL($foreignKey);
341
            }
342
        }
343
344 727
        $tableComment = '';
345 727
        if (isset($options['comment'])) {
346 1
            $comment = trim($options['comment'], " '");
347
348 1
            $tableComment = $this->getInlineTableCommentSQL($comment);
349
        }
350
351 727
        $query = ['CREATE TABLE ' . $name . ' ' . $tableComment . '(' . $queryFields . ')'];
352
353 727
        if (isset($options['alter']) && $options['alter'] === true) {
354 281
            return $query;
355
        }
356
357 463
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
358 101
            foreach ($options['indexes'] as $indexDef) {
359 101
                $query[] = $this->getCreateIndexSQL($indexDef, $name);
360
            }
361
        }
362
363 463
        if (isset($options['unique']) && ! empty($options['unique'])) {
364
            foreach ($options['unique'] as $indexDef) {
365
                $query[] = $this->getCreateIndexSQL($indexDef, $name);
366
            }
367
        }
368
369 463
        return $query;
370
    }
371
372
    /**
373
     * Generate a PRIMARY KEY definition if no autoincrement value is used
374
     *
375
     * @param mixed[][] $columns
376
     * @param mixed[]   $options
377
     */
378 727
    private function getNonAutoincrementPrimaryKeyDefinition(array $columns, array $options) : string
379
    {
380 727
        if (empty($options['primary'])) {
381 360
            return '';
382
        }
383
384 369
        $keyColumns = array_unique(array_values($options['primary']));
385
386 369
        foreach ($keyColumns as $keyColumn) {
387 369
            if (! empty($columns[$keyColumn]['autoincrement'])) {
388 100
                return '';
389
            }
390
        }
391
392 273
        return ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
393
    }
394
395
    /**
396
     * {@inheritDoc}
397
     */
398 425
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
399
    {
400 425
        return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)')
401 425
            : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'TEXT');
402
    }
403
404
    /**
405
     * {@inheritdoc}
406
     */
407 23
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
408
    {
409 23
        return 'BLOB';
410
    }
411
412
    /**
413
     * {@inheritdoc}
414
     */
415 46
    public function getBinaryMaxLength()
416
    {
417 46
        return 0;
418
    }
419
420
    /**
421
     * {@inheritdoc}
422
     */
423 45
    public function getBinaryDefaultLength()
424
    {
425 45
        return 0;
426
    }
427
428
    /**
429
     * {@inheritDoc}
430
     */
431 71
    public function getClobTypeDeclarationSQL(array $field)
432
    {
433 71
        return 'CLOB';
434
    }
435
436
    /**
437
     * {@inheritDoc}
438
     */
439 22
    public function getListTableConstraintsSQL($table)
440
    {
441 22
        $table = str_replace('.', '__', $table);
442
443 22
        return sprintf(
444 1
            "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = %s AND sql NOT NULL ORDER BY name",
445 22
            $this->quoteStringLiteral($table)
446
        );
447
    }
448
449
    /**
450
     * {@inheritDoc}
451
     */
452 131
    public function getListTableColumnsSQL($table, $currentDatabase = null)
453
    {
454 131
        $table = str_replace('.', '__', $table);
455
456 131
        return sprintf('PRAGMA table_info(%s)', $this->quoteStringLiteral($table));
457
    }
458
459
    /**
460
     * {@inheritDoc}
461
     */
462 125
    public function getListTableIndexesSQL($table, $currentDatabase = null)
463
    {
464 125
        $table = str_replace('.', '__', $table);
465
466 125
        return sprintf('PRAGMA index_list(%s)', $this->quoteStringLiteral($table));
467
    }
468
469
    /**
470
     * {@inheritDoc}
471
     */
472 138
    public function getListTablesSQL()
473
    {
474
        return "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND name != 'geometry_columns' AND name != 'spatial_ref_sys' "
475
             . 'UNION ALL SELECT name FROM sqlite_temp_master '
476 138
             . "WHERE type = 'table' ORDER BY name";
477
    }
478
479
    /**
480
     * {@inheritDoc}
481
     */
482 1
    public function getListViewsSQL($database)
483
    {
484 1
        return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL";
485
    }
486
487
    /**
488
     * {@inheritDoc}
489
     */
490 1
    public function getCreateViewSQL($name, $sql)
491
    {
492 1
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
493
    }
494
495
    /**
496
     * {@inheritDoc}
497
     */
498 1
    public function getDropViewSQL($name)
499
    {
500 1
        return 'DROP VIEW ' . $name;
501
    }
502
503
    /**
504
     * {@inheritDoc}
505
     */
506 122
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
507
    {
508 122
        $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
509
510 122
        $query .= ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false ? ' ' : ' NOT ') . 'DEFERRABLE';
511 122
        $query .= ' INITIALLY ' . ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false ? 'DEFERRED' : 'IMMEDIATE');
512
513 122
        return $query;
514
    }
515
516
    /**
517
     * {@inheritDoc}
518
     */
519 3
    public function supportsIdentityColumns()
520
    {
521 3
        return true;
522
    }
523
524
    /**
525
     * {@inheritDoc}
526
     */
527 69
    public function supportsColumnCollation()
528
    {
529 69
        return true;
530
    }
531
532
    /**
533
     * {@inheritDoc}
534
     */
535 815
    public function supportsInlineColumnComments()
536
    {
537 815
        return true;
538
    }
539
540
    /**
541
     * {@inheritDoc}
542
     */
543 121
    public function getName()
544
    {
545 121
        return 'sqlite';
546
    }
547
548
    /**
549
     * {@inheritDoc}
550
     */
551 31
    public function getTruncateTableSQL($tableName, $cascade = false)
552
    {
553 31
        $tableIdentifier = new Identifier($tableName);
554 31
        $tableName       = str_replace('.', '__', $tableIdentifier->getQuotedName($this));
555
556 31
        return 'DELETE FROM ' . $tableName;
557
    }
558
559
    /**
560
     * User-defined function for Sqlite that is used with PDO::sqliteCreateFunction().
561
     *
562
     * @param int|float $value
563
     *
564
     * @return float
565
     */
566
    public static function udfSqrt($value)
567
    {
568
        return sqrt($value);
569
    }
570
571
    /**
572
     * User-defined function for Sqlite that implements MOD(a, b).
573
     *
574
     * @param int $a
575
     * @param int $b
576
     *
577
     * @return int
578
     */
579
    public static function udfMod($a, $b)
580
    {
581
        return $a % $b;
582
    }
583
584
    /**
585
     * @param string $str
586
     * @param string $substr
587
     * @param int    $offset
588
     *
589
     * @return int
590
     */
591 1
    public static function udfLocate($str, $substr, $offset = 0)
592
    {
593
        // SQL's LOCATE function works on 1-based positions, while PHP's strpos works on 0-based positions.
594
        // So we have to make them compatible if an offset is given.
595 1
        if ($offset > 0) {
596 1
            $offset -= 1;
597
        }
598
599 1
        $pos = strpos($str, $substr, $offset);
600
601 1
        if ($pos !== false) {
602 1
            return $pos + 1;
603
        }
604
605 1
        return 0;
606
    }
607
608
    /**
609
     * {@inheritDoc}
610
     */
611
    public function getForUpdateSQL()
612
    {
613
        return '';
614
    }
615
616
    /**
617
     * {@inheritDoc}
618
     */
619 166
    public function getInlineColumnCommentSQL($comment)
620
    {
621 166
        return '--' . str_replace("\n", "\n--", $comment) . "\n";
622
    }
623
624 1
    private function getInlineTableCommentSQL(string $comment) : string
625
    {
626 1
        return $this->getInlineColumnCommentSQL($comment);
627
    }
628
629
    /**
630
     * {@inheritDoc}
631
     */
632 133
    protected function initializeDoctrineTypeMappings()
633
    {
634 133
        $this->doctrineTypeMapping = [
635
            'bigint'           => 'bigint',
636
            'bigserial'        => 'bigint',
637
            'blob'             => 'blob',
638
            'boolean'          => 'boolean',
639
            'char'             => 'string',
640
            'clob'             => 'text',
641
            'date'             => 'date',
642
            'datetime'         => 'datetime',
643
            'decimal'          => 'decimal',
644
            'double'           => 'float',
645
            'double precision' => 'float',
646
            'float'            => 'float',
647
            'image'            => 'string',
648
            'int'              => 'integer',
649
            'integer'          => 'integer',
650
            'longtext'         => 'text',
651
            'longvarchar'      => 'string',
652
            'mediumint'        => 'integer',
653
            'mediumtext'       => 'text',
654
            'ntext'            => 'string',
655
            'numeric'          => 'decimal',
656
            'nvarchar'         => 'string',
657
            'real'             => 'float',
658
            'serial'           => 'integer',
659
            'smallint'         => 'smallint',
660
            'text'             => 'text',
661
            'time'             => 'time',
662
            'timestamp'        => 'datetime',
663
            'tinyint'          => 'boolean',
664
            'tinytext'         => 'text',
665
            'varchar'          => 'string',
666
            'varchar2'         => 'string',
667
        ];
668 133
    }
669
670
    /**
671
     * {@inheritDoc}
672
     */
673 890
    protected function getReservedKeywordsClass()
674
    {
675 890
        return Keywords\SQLiteKeywords::class;
676
    }
677
678
    /**
679
     * {@inheritDoc}
680
     */
681 281
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff)
682
    {
683 281
        if (! $diff->fromTable instanceof Table) {
684
            throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema');
685
        }
686
687 281
        $sql = [];
688 281
        foreach ($diff->fromTable->getIndexes() as $index) {
689 139
            if ($index->isPrimary()) {
690 94
                continue;
691
            }
692
693 69
            $sql[] = $this->getDropIndexSQL($index, $diff->name);
694
        }
695
696 281
        return $sql;
697
    }
698
699
    /**
700
     * {@inheritDoc}
701
     */
702 281
    protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff)
703
    {
704 281
        if (! $diff->fromTable instanceof Table) {
705
            throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema');
706
        }
707
708 281
        $sql       = [];
709 281
        $tableName = $diff->getNewName();
710
711 281
        if ($tableName === false) {
712 215
            $tableName = $diff->getName($this);
713
        }
714
715 281
        foreach ($this->getIndexesInAlteredTable($diff) as $index) {
716 139
            if ($index->isPrimary()) {
717 94
                continue;
718
            }
719
720 113
            $sql[] = $this->getCreateIndexSQL($index, $tableName->getQuotedName($this));
721
        }
722
723 281
        return $sql;
724
    }
725
726
    /**
727
     * {@inheritDoc}
728
     */
729 96
    protected function doModifyLimitQuery($query, $limit, $offset)
730
    {
731 96
        if ($limit === null && $offset > 0) {
732 23
            return $query . ' LIMIT -1 OFFSET ' . $offset;
733
        }
734
735 74
        return parent::doModifyLimitQuery($query, $limit, $offset);
736
    }
737
738
    /**
739
     * {@inheritDoc}
740
     */
741 31
    public function getBlobTypeDeclarationSQL(array $field)
742
    {
743 31
        return 'BLOB';
744
    }
745
746
    /**
747
     * {@inheritDoc}
748
     */
749 2
    public function getTemporaryTableName($tableName)
750
    {
751 2
        $tableName = str_replace('.', '__', $tableName);
752
753 2
        return $tableName;
754
    }
755
756
    /**
757
     * {@inheritDoc}
758
     *
759
     * Sqlite Platform emulates schema by underscoring each dot and generating tables
760
     * into the default database.
761
     *
762
     * This hack is implemented to be able to use SQLite as testdriver when
763
     * using schema supporting databases.
764
     */
765
    public function canEmulateSchemas()
766
    {
767
        return true;
768
    }
769
770
    /**
771
     * {@inheritDoc}
772
     */
773 235
    public function supportsForeignKeyConstraints()
774
    {
775 235
        return true;
776
    }
777
778 23
    public function supportsCreateDropForeignKeyConstraints() : bool
779
    {
780 23
        return false;
781
    }
782
783
    /**
784
     * {@inheritDoc}
785
     */
786
    public function getCreatePrimaryKeySQL(Index $index, $table)
787
    {
788
        throw new DBALException('Sqlite platform does not support alter primary key.');
789
    }
790
791
    /**
792
     * {@inheritdoc}
793
     */
794 22
    public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table)
795
    {
796 22
        throw new DBALException('Sqlite platform does not support alter foreign key, the table must be fully recreated using getAlterTableSQL.');
797
    }
798
799
    /**
800
     * {@inheritdoc}
801
     */
802
    public function getDropForeignKeySQL($foreignKey, $table)
803
    {
804
        throw new DBALException('Sqlite platform does not support alter foreign key, the table must be fully recreated using getAlterTableSQL.');
805
    }
806
807
    /**
808
     * {@inheritDoc}
809
     */
810
    public function getCreateConstraintSQL(Constraint $constraint, $table)
811
    {
812
        throw new DBALException('Sqlite platform does not support alter constraint.');
813
    }
814
815
    /**
816
     * {@inheritDoc}
817
     *
818
     * @param int|null $createFlags
819
     */
820 749
    public function getCreateTableSQL(Table $table, $createFlags = null)
821
    {
822 749
        $createFlags = $createFlags ?? self::CREATE_INDEXES | self::CREATE_FOREIGNKEYS;
823
824 749
        return parent::getCreateTableSQL($table, $createFlags);
825
    }
826
827
    /**
828
     * @param string      $table
829
     * @param string|null $database
830
     *
831
     * @return string
832
     */
833 121
    public function getListTableForeignKeysSQL($table, $database = null)
0 ignored issues
show
Unused Code introduced by
The parameter $database is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

833
    public function getListTableForeignKeysSQL($table, /** @scrutinizer ignore-unused */ $database = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
834
    {
835 121
        $table = str_replace('.', '__', $table);
836
837 121
        return sprintf('PRAGMA foreign_key_list(%s)', $this->quoteStringLiteral($table));
838
    }
839
840
    /**
841
     * {@inheritDoc}
842
     */
843 353
    public function getAlterTableSQL(TableDiff $diff)
844
    {
845 353
        $sql = $this->getSimpleAlterTableSQL($diff);
846 353
        if ($sql !== false) {
0 ignored issues
show
introduced by
The condition $sql !== false is always false.
Loading history...
847 28
            return $sql;
848
        }
849
850 325
        $fromTable = $diff->fromTable;
851 325
        if (! $fromTable instanceof Table) {
852 44
            throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema');
853
        }
854
855 281
        $table = clone $fromTable;
856
857 281
        $columns        = [];
858 281
        $oldColumnNames = [];
859 281
        $newColumnNames = [];
860 281
        $columnSql      = [];
861
862 281
        foreach ($table->getColumns() as $columnName => $column) {
863 259
            $columnName                  = strtolower($columnName);
864 259
            $columns[$columnName]        = $column;
865 259
            $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this);
866
        }
867
868 281
        foreach ($diff->removedColumns as $columnName => $column) {
869 89
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
870
                continue;
871
            }
872
873 89
            $columnName = strtolower($columnName);
874 89
            if (! isset($columns[$columnName])) {
875
                continue;
876
            }
877
878
            unset(
879 89
                $columns[$columnName],
880 89
                $oldColumnNames[$columnName],
881 89
                $newColumnNames[$columnName]
882
            );
883
        }
884
885 281
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
886 111
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
887
                continue;
888
            }
889
890 111
            $oldColumnName = strtolower($oldColumnName);
891 111
            if (isset($columns[$oldColumnName])) {
892 111
                unset($columns[$oldColumnName]);
893
            }
894
895 111
            $columns[strtolower($column->getName())] = $column;
896
897 111
            if (! isset($newColumnNames[$oldColumnName])) {
898
                continue;
899
            }
900
901 111
            $newColumnNames[$oldColumnName] = $column->getQuotedName($this);
902
        }
903
904 281
        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
905 144
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
906
                continue;
907
            }
908
909 144
            if (isset($columns[$oldColumnName])) {
910 122
                unset($columns[$oldColumnName]);
911
            }
912
913 144
            $columns[strtolower($columnDiff->column->getName())] = $columnDiff->column;
914
915 144
            if (! isset($newColumnNames[$oldColumnName])) {
916 22
                continue;
917
            }
918
919 122
            $newColumnNames[$oldColumnName] = $columnDiff->column->getQuotedName($this);
920
        }
921
922 281
        foreach ($diff->addedColumns as $columnName => $column) {
923 67
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
924
                continue;
925
            }
926
927 67
            $columns[strtolower($columnName)] = $column;
928
        }
929
930 281
        $sql      = [];
931 281
        $tableSql = [];
932 281
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
933 281
            $dataTable = new Table('__temp__' . $table->getName());
934
935 281
            $newTable = new Table($table->getQuotedName($this), $columns, $this->getPrimaryIndexInAlteredTable($diff), $this->getForeignKeysInAlteredTable($diff), 0, $table->getOptions());
936 281
            $newTable->addOption('alter', true);
937
938 281
            $sql = $this->getPreAlterTableIndexForeignKeySQL($diff);
939
            //$sql = array_merge($sql, $this->getCreateTableSQL($dataTable, 0));
940 281
            $sql[] = sprintf('CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', $dataTable->getQuotedName($this), implode(', ', $oldColumnNames), $table->getQuotedName($this));
941 281
            $sql[] = $this->getDropTableSQL($fromTable);
942
943 281
            $sql   = array_merge($sql, $this->getCreateTableSQL($newTable));
944 281
            $sql[] = sprintf('INSERT INTO %s (%s) SELECT %s FROM %s', $newTable->getQuotedName($this), implode(', ', $newColumnNames), implode(', ', $oldColumnNames), $dataTable->getQuotedName($this));
945 281
            $sql[] = $this->getDropTableSQL($dataTable);
946
947 281
            $newName = $diff->getNewName();
948
949 281
            if ($newName !== false) {
950 66
                $sql[] = sprintf(
951 3
                    'ALTER TABLE %s RENAME TO %s',
952 66
                    $newTable->getQuotedName($this),
953 66
                    $newName->getQuotedName($this)
954
                );
955
            }
956
957 281
            $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff));
958
        }
959
960 281
        return array_merge($sql, $tableSql, $columnSql);
961
    }
962
963
    /**
964
     * @return string[]|false
965
     */
966 353
    private function getSimpleAlterTableSQL(TableDiff $diff)
967
    {
968
        // Suppress changes on integer type autoincrement columns.
969 353
        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
970 149
            if ($columnDiff->fromColumn === null ||
971 145
                ! $columnDiff->column->getAutoincrement() ||
972 149
                ! $columnDiff->column->getType() instanceof Types\IntegerType
973
            ) {
974 144
                continue;
975
            }
976
977 5
            if (! $columnDiff->hasChanged('type') && $columnDiff->hasChanged('unsigned')) {
978 1
                unset($diff->changedColumns[$oldColumnName]);
979
980 1
                continue;
981
            }
982
983 4
            $fromColumnType = $columnDiff->fromColumn->getType();
984
985 4
            if (! ($fromColumnType instanceof Types\SmallIntType) && ! ($fromColumnType instanceof Types\BigIntType)) {
986
                continue;
987
            }
988
989 4
            unset($diff->changedColumns[$oldColumnName]);
990
        }
991
992 353
        if (! empty($diff->renamedColumns) || ! empty($diff->addedForeignKeys) || ! empty($diff->addedIndexes)
993 348
                || ! empty($diff->changedColumns) || ! empty($diff->changedForeignKeys) || ! empty($diff->changedIndexes)
994 344
                || ! empty($diff->removedColumns) || ! empty($diff->removedForeignKeys) || ! empty($diff->removedIndexes)
995 353
                || ! empty($diff->renamedIndexes)
996
        ) {
997 281
            return false;
998
        }
999
1000 72
        $table = new Table($diff->name);
1001
1002 72
        $sql       = [];
1003 72
        $tableSql  = [];
1004 72
        $columnSql = [];
1005
1006 72
        foreach ($diff->addedColumns as $column) {
1007 66
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
1008
                continue;
1009
            }
1010
1011 66
            $field = array_merge(['unique' => null, 'autoincrement' => null, 'default' => null], $column->toArray());
1012 66
            $type  = $field['type'];
1013
            switch (true) {
1014 66
                case isset($field['columnDefinition']) || $field['autoincrement'] || $field['unique']:
1015 44
                case $type instanceof Types\DateTimeType && $field['default'] === $this->getCurrentTimestampSQL():
1016 44
                case $type instanceof Types\DateType && $field['default'] === $this->getCurrentDateSQL():
1017 22
                case $type instanceof Types\TimeType && $field['default'] === $this->getCurrentTimeSQL():
1018 44
                    return false;
1019
            }
1020
1021 22
            $field['name'] = $column->getQuotedName($this);
1022 22
            if ($type instanceof Types\StringType && $field['length'] === null) {
1023 22
                $field['length'] = 255;
1024
            }
1025
1026 22
            $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN ' . $this->getColumnDeclarationSQL($field['name'], $field);
1027
        }
1028
1029 28
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
1030 28
            if ($diff->newName !== false) {
1031 1
                $newTable = new Identifier($diff->newName);
0 ignored issues
show
Bug introduced by
It seems like $diff->newName can also be of type true; however, parameter $identifier of Doctrine\DBAL\Schema\Identifier::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1031
                $newTable = new Identifier(/** @scrutinizer ignore-type */ $diff->newName);
Loading history...
1032 1
                $sql[]    = 'ALTER TABLE ' . $table->getQuotedName($this) . ' RENAME TO ' . $newTable->getQuotedName($this);
1033
            }
1034
        }
1035
1036 28
        return array_merge($sql, $tableSql, $columnSql);
1037
    }
1038
1039
    /**
1040
     * @return string[]
1041
     */
1042 281
    private function getColumnNamesInAlteredTable(TableDiff $diff)
1043
    {
1044 281
        $columns = [];
1045
1046 281
        foreach ($diff->fromTable->getColumns() as $columnName => $column) {
0 ignored issues
show
Bug introduced by
The method getColumns() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1046
        foreach ($diff->fromTable->/** @scrutinizer ignore-call */ getColumns() as $columnName => $column) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1047 259
            $columns[strtolower($columnName)] = $column->getName();
1048
        }
1049
1050 281
        foreach ($diff->removedColumns as $columnName => $column) {
1051 89
            $columnName = strtolower($columnName);
1052 89
            if (! isset($columns[$columnName])) {
1053
                continue;
1054
            }
1055
1056 89
            unset($columns[$columnName]);
1057
        }
1058
1059 281
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
1060 111
            $columnName                          = $column->getName();
1061 111
            $columns[strtolower($oldColumnName)] = $columnName;
1062 111
            $columns[strtolower($columnName)]    = $columnName;
1063
        }
1064
1065 281
        foreach ($diff->changedColumns as $oldColumnName => $columnDiff) {
1066 144
            $columnName                          = $columnDiff->column->getName();
1067 144
            $columns[strtolower($oldColumnName)] = $columnName;
1068 144
            $columns[strtolower($columnName)]    = $columnName;
1069
        }
1070
1071 281
        foreach ($diff->addedColumns as $column) {
1072 67
            $columnName                       = $column->getName();
1073 67
            $columns[strtolower($columnName)] = $columnName;
1074
        }
1075
1076 281
        return $columns;
1077
    }
1078
1079
    /**
1080
     * @return Index[]
1081
     */
1082 281
    private function getIndexesInAlteredTable(TableDiff $diff)
1083
    {
1084 281
        $indexes     = $diff->fromTable->getIndexes();
1085 281
        $columnNames = $this->getColumnNamesInAlteredTable($diff);
1086
1087 281
        foreach ($indexes as $key => $index) {
1088 139
            foreach ($diff->renamedIndexes as $oldIndexName => $renamedIndex) {
1089 68
                if (strtolower($key) !== strtolower($oldIndexName)) {
1090 67
                    continue;
1091
                }
1092
1093 24
                unset($indexes[$key]);
1094
            }
1095
1096 139
            $changed      = false;
1097 139
            $indexColumns = [];
1098 139
            foreach ($index->getColumns() as $columnName) {
1099 139
                $normalizedColumnName = strtolower($columnName);
1100 139
                if (! isset($columnNames[$normalizedColumnName])) {
1101 22
                    unset($indexes[$key]);
1102 22
                    continue 2;
1103
                }
1104
1105 139
                $indexColumns[] = $columnNames[$normalizedColumnName];
1106 139
                if ($columnName === $columnNames[$normalizedColumnName]) {
1107 139
                    continue;
1108
                }
1109
1110 23
                $changed = true;
1111
            }
1112
1113 139
            if (! $changed) {
1114 139
                continue;
1115
            }
1116
1117 23
            $indexes[$key] = new Index($index->getName(), $indexColumns, $index->isUnique(), $index->isPrimary(), $index->getFlags());
1118
        }
1119
1120 281
        foreach ($diff->removedIndexes as $index) {
1121 23
            $indexName = strtolower($index->getName());
1122 23
            if (strlen($indexName) === 0 || ! isset($indexes[$indexName])) {
1123
                continue;
1124
            }
1125
1126 23
            unset($indexes[$indexName]);
1127
        }
1128
1129 281
        foreach (array_merge($diff->changedIndexes, $diff->addedIndexes, $diff->renamedIndexes) as $index) {
1130 69
            $indexName = strtolower($index->getName());
1131 69
            if (strlen($indexName) > 0) {
1132 69
                $indexes[$indexName] = $index;
1133
            } else {
1134
                $indexes[] = $index;
1135
            }
1136
        }
1137
1138 281
        return $indexes;
1139
    }
1140
1141
    /**
1142
     * @return ForeignKeyConstraint[]
1143
     */
1144 281
    private function getForeignKeysInAlteredTable(TableDiff $diff)
1145
    {
1146 281
        $foreignKeys = $diff->fromTable->getForeignKeys();
1147 281
        $columnNames = $this->getColumnNamesInAlteredTable($diff);
1148
1149 281
        foreach ($foreignKeys as $key => $constraint) {
1150 68
            $changed      = false;
1151 68
            $localColumns = [];
1152 68
            foreach ($constraint->getLocalColumns() as $columnName) {
1153 68
                $normalizedColumnName = strtolower($columnName);
1154 68
                if (! isset($columnNames[$normalizedColumnName])) {
1155 22
                    unset($foreignKeys[$key]);
1156 22
                    continue 2;
1157
                }
1158
1159 68
                $localColumns[] = $columnNames[$normalizedColumnName];
1160 68
                if ($columnName === $columnNames[$normalizedColumnName]) {
1161 67
                    continue;
1162
                }
1163
1164 23
                $changed = true;
1165
            }
1166
1167 68
            if (! $changed) {
1168 67
                continue;
1169
            }
1170
1171 23
            $foreignKeys[$key] = new ForeignKeyConstraint($localColumns, $constraint->getForeignTableName(), $constraint->getForeignColumns(), $constraint->getName(), $constraint->getOptions());
1172
        }
1173
1174 281
        foreach ($diff->removedForeignKeys as $constraint) {
1175 23
            if (! $constraint instanceof ForeignKeyConstraint) {
1176
                $constraint = new Identifier($constraint);
1177
            }
1178
1179 23
            $constraintName = strtolower($constraint->getName());
1180 23
            if (strlen($constraintName) === 0 || ! isset($foreignKeys[$constraintName])) {
1181
                continue;
1182
            }
1183
1184 23
            unset($foreignKeys[$constraintName]);
1185
        }
1186
1187 281
        foreach (array_merge($diff->changedForeignKeys, $diff->addedForeignKeys) as $constraint) {
1188 26
            $constraintName = strtolower($constraint->getName());
1189 26
            if (strlen($constraintName) > 0) {
1190 25
                $foreignKeys[$constraintName] = $constraint;
1191
            } else {
1192 1
                $foreignKeys[] = $constraint;
1193
            }
1194
        }
1195
1196 281
        return $foreignKeys;
1197
    }
1198
1199
    /**
1200
     * @return Index[]
1201
     */
1202 281
    private function getPrimaryIndexInAlteredTable(TableDiff $diff)
1203
    {
1204 281
        $primaryIndex = [];
1205
1206 281
        foreach ($this->getIndexesInAlteredTable($diff) as $index) {
1207 139
            if (! $index->isPrimary()) {
1208 113
                continue;
1209
            }
1210
1211 94
            $primaryIndex = [$index->getName() => $index];
1212
        }
1213
1214 281
        return $primaryIndex;
1215
    }
1216
}
1217