Completed
Pull Request — develop (#3562)
by Sergei
72:46 queued 07:36
created

MySqlPlatform::quoteStringLiteral()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.2963

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
ccs 1
cts 3
cp 0.3333
cc 1
nc 1
nop 1
crap 1.2963
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Platforms;
6
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\BlobType;
14
use Doctrine\DBAL\Types\TextType;
15
use InvalidArgumentException;
16
use function array_diff_key;
17
use function array_merge;
18
use function array_unique;
19
use function array_values;
20
use function count;
21
use function implode;
22
use function in_array;
23
use function is_numeric;
24
use function is_string;
25
use function sprintf;
26
use function str_replace;
27
use function strtoupper;
28
use function trim;
29
30
/**
31
 * The MySqlPlatform provides the behavior, features and SQL dialect of the
32
 * MySQL database platform. This platform represents a MySQL 5.0 or greater platform that
33
 * uses the InnoDB storage engine.
34
 *
35
 * @todo   Rename: MySQLPlatform
36
 */
37
class MySqlPlatform extends AbstractPlatform
38
{
39
    public const LENGTH_LIMIT_TINYTEXT   = 255;
40
    public const LENGTH_LIMIT_TEXT       = 65535;
41
    public const LENGTH_LIMIT_MEDIUMTEXT = 16777215;
42
43
    public const LENGTH_LIMIT_TINYBLOB   = 255;
44
    public const LENGTH_LIMIT_BLOB       = 65535;
45
    public const LENGTH_LIMIT_MEDIUMBLOB = 16777215;
46
47
    /**
48
     * {@inheritDoc}
49
     */
50 11158
    protected function doModifyLimitQuery(string $query, ?int $limit, int $offset) : string
51
    {
52 11158
        if ($limit !== null) {
53 11096
            $query .= ' LIMIT ' . $limit;
54
55 11096
            if ($offset > 0) {
56 11096
                $query .= ' OFFSET ' . $offset;
57
            }
58 11146
        } elseif ($offset > 0) {
59
            // 2^64-1 is the maximum of unsigned BIGINT, the biggest limit possible
60 11140
            $query .= ' LIMIT 18446744073709551615 OFFSET ' . $offset;
61
        }
62
63 11158
        return $query;
64
    }
65
66
    /**
67
     * {@inheritDoc}
68
     */
69 12428
    public function getIdentifierQuoteCharacter() : string
70
    {
71 12428
        return '`';
72
    }
73
74
    /**
75
     * {@inheritDoc}
76
     */
77 7831
    public function getRegexpExpression() : string
78
    {
79 7831
        return 'RLIKE';
80
    }
81
82
    /**
83
     * {@inheritDoc}
84
     */
85 10039
    public function getLocateExpression(string $string, string $substring, ?string $start = null) : string
86
    {
87 10039
        if ($start === null) {
88 10039
            return sprintf('LOCATE(%s, %s)', $substring, $string);
89
        }
90
91 10039
        return sprintf('LOCATE(%s, %s, %s)', $substring, $string, $start);
92
    }
93
94
    /**
95
     * {@inheritDoc}
96
     */
97 7831
    public function getConcatExpression(string ...$string) : string
98
    {
99 7831
        return sprintf('CONCAT(%s)', implode(', ', $string));
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 10679
    protected function getDateArithmeticIntervalExpression(string $date, string $operator, string $interval, string $unit) : string
106
    {
107 10679
        $function = $operator === '+' ? 'DATE_ADD' : 'DATE_SUB';
108
109 10679
        return $function . '(' . $date . ', INTERVAL ' . $interval . ' ' . $unit . ')';
110
    }
111
112
    /**
113
     * {@inheritDoc}
114
     */
115 9259
    public function getDateDiffExpression(string $date1, string $date2) : string
116
    {
117 9259
        return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')';
118
    }
119
120
    /**
121
     * {@inheritDoc}
122
     */
123 9930
    public function getListDatabasesSQL() : string
124
    {
125 9930
        return 'SHOW DATABASES';
126
    }
127
128
    /**
129
     * {@inheritDoc}
130
     */
131
    public function getListTableConstraintsSQL(string $table) : string
132
    {
133
        return 'SHOW INDEX FROM ' . $table;
134
    }
135
136
    /**
137
     * {@inheritDoc}
138
     *
139
     * Two approaches to listing the table indexes. The information_schema is
140
     * preferred, because it doesn't cause problems with SQL keywords such as "order" or "table".
141
     */
142 10661
    public function getListTableIndexesSQL(string $table, ?string $currentDatabase = null) : string
143
    {
144 10661
        if ($currentDatabase) {
145 10661
            $currentDatabase = $this->quoteStringLiteral($currentDatabase);
146 10661
            $table           = $this->quoteStringLiteral($table);
147
148
            return 'SELECT NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, COLUMN_NAME AS Column_Name,' .
149
                   ' SUB_PART AS Sub_Part, INDEX_TYPE AS Index_Type' .
150 10661
                   ' FROM information_schema.STATISTICS WHERE TABLE_NAME = ' . $table .
151 10661
                   ' AND TABLE_SCHEMA = ' . $currentDatabase .
152 10661
                   ' ORDER BY SEQ_IN_INDEX ASC';
153
        }
154
155
        return 'SHOW INDEX FROM ' . $table;
156
    }
157
158
    /**
159
     * {@inheritDoc}
160
     */
161 9520
    public function getListViewsSQL(string $database) : string
162
    {
163 9520
        return 'SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ' . $this->quoteStringLiteral($database);
164
    }
165 9520
166
    /**
167
     * {@inheritDoc}
168
     */
169
    public function getListTableForeignKeysSQL(string $table, ?string $database = null) : string
170
    {
171 10657
        $table = $this->quoteStringLiteral($table);
172
173 10657
        $sql = 'SELECT DISTINCT k.`CONSTRAINT_NAME`, k.`COLUMN_NAME`, k.`REFERENCED_TABLE_NAME`, ' .
174
               'k.`REFERENCED_COLUMN_NAME` /*!50116 , c.update_rule, c.delete_rule */ ' .
175 10657
               'FROM information_schema.key_column_usage k /*!50116 ' .
176 10646
               'INNER JOIN information_schema.referential_constraints c ON ' .
177
               '  c.constraint_name = k.constraint_name AND ' .
178
               '  c.table_name = ' . $table . ' */ WHERE k.table_name = ' . $table;
179
180
        $databaseNameSql = $this->getDatabaseNameSql($database);
181
182
        $sql .= ' AND k.table_schema = ' . $databaseNameSql . ' /*!50116 AND c.constraint_schema = ' . $databaseNameSql . ' */';
183
        $sql .= ' AND k.`REFERENCED_COLUMN_NAME` is not NULL';
184 10657
185
        return $sql;
186 10657
    }
187
188 10657
    /**
189 10657
     * {@inheritDoc}
190
     */
191 10657
    public function getCreateViewSQL(string $name, string $sql) : string
192
    {
193
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
194
    }
195
196
    /**
197 8089
     * {@inheritDoc}
198
     */
199 8089
    public function getDropViewSQL(string $name) : string
200
    {
201
        return 'DROP VIEW ' . $name;
202
    }
203
204
    /**
205 8089
     * {@inheritDoc}
206
     */
207 8089
    protected function getVarcharTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
208
    {
209
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
210
                : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
211
    }
212
213 12360
    /**
214
     * {@inheritdoc}
215 12360
     */
216 12360
    protected function getBinaryTypeDeclarationSQLSnippet(int $length, bool $fixed) : string
217
    {
218
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
219
    }
220
221
    /**
222 9305
     * Gets the SQL snippet used to declare a CLOB column type.
223
     *     TINYTEXT   : 2 ^  8 - 1 = 255
224 9305
     *     TEXT       : 2 ^ 16 - 1 = 65535
225
     *     MEDIUMTEXT : 2 ^ 24 - 1 = 16777215
226
     *     LONGTEXT   : 2 ^ 32 - 1 = 4294967295
227
     *
228
     * {@inheritDoc}
229
     */
230
    public function getClobTypeDeclarationSQL(array $field) : string
231
    {
232
        if (! empty($field['length']) && is_numeric($field['length'])) {
233
            $length = $field['length'];
234
235
            if ($length <= static::LENGTH_LIMIT_TINYTEXT) {
236 12419
                return 'TINYTEXT';
237
            }
238 12419
239 10101
            if ($length <= static::LENGTH_LIMIT_TEXT) {
240
                return 'TEXT';
241 10101
            }
242 10099
243
            if ($length <= static::LENGTH_LIMIT_MEDIUMTEXT) {
244
                return 'MEDIUMTEXT';
245 10101
            }
246 10101
        }
247
248
        return 'LONGTEXT';
249 10099
    }
250 10099
251
    /**
252
     * {@inheritDoc}
253
     */
254 12417
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) : string
255
    {
256
        if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] === true) {
257
            return 'TIMESTAMP';
258
        }
259
260 12245
        return 'DATETIME';
261
    }
262 12245
263 7606
    /**
264
     * {@inheritDoc}
265
     */
266 12245
    public function getDateTypeDeclarationSQL(array $fieldDeclaration) : string
267
    {
268
        return 'DATE';
269
    }
270
271
    /**
272 8473
     * {@inheritDoc}
273
     */
274 8473
    public function getTimeTypeDeclarationSQL(array $fieldDeclaration) : string
275
    {
276
        return 'TIME';
277
    }
278
279
    /**
280 8341
     * {@inheritDoc}
281
     */
282 8341
    public function getBooleanTypeDeclarationSQL(array $columnDef) : string
283
    {
284
        return 'TINYINT(1)';
285
    }
286
287
    /**
288 10160
     * {@inheritDoc}
289
     *
290 10160
     * MySql prefers "autoincrement" identity columns since sequences can only
291
     * be emulated with a table.
292
     */
293
    public function prefersIdentityColumns() : bool
294
    {
295
        return true;
296
    }
297
298
    /**
299
     * {@inheritDoc}
300
     *
301
     * MySql supports this through AUTO_INCREMENT columns.
302
     */
303
    public function supportsIdentityColumns() : bool
304
    {
305
        return true;
306
    }
307
308
    /**
309
     * {@inheritDoc}
310
     */
311
    public function supportsInlineColumnComments() : bool
312
    {
313
        return true;
314
    }
315 8006
316
    /**
317 8006
     * {@inheritDoc}
318
     */
319
    public function supportsColumnCollation() : bool
320
    {
321
        return true;
322
    }
323
324
    /**
325 9610
     * {@inheritDoc}
326
     */
327 9610
    public function getListTablesSQL() : string
328
    {
329
        return "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'";
330
    }
331
332
    /**
333 12651
     * {@inheritDoc}
334
     */
335 12651
    public function getListTableColumnsSQL(string $table, ?string $database = null) : string
336
    {
337
        return 'SELECT COLUMN_NAME AS Field, COLUMN_TYPE AS Type, IS_NULLABLE AS `Null`, ' .
338
               'COLUMN_KEY AS `Key`, COLUMN_DEFAULT AS `Default`, EXTRA AS Extra, COLUMN_COMMENT AS Comment, ' .
339
               'CHARACTER_SET_NAME AS CharacterSet, COLLATION_NAME AS Collation ' .
340
               'FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ' . $this->getDatabaseNameSql($database) . ' ' .
341 10021
               'AND TABLE_NAME = ' . $this->quoteStringLiteral($table) . ' ORDER BY ORDINAL_POSITION';
342
    }
343 10021
344
    public function getListTableMetadataSQL(string $table, ?string $database = null) : string
345
    {
346
        return sprintf(
347
            <<<'SQL'
348
SELECT ENGINE, AUTO_INCREMENT, TABLE_COLLATION, TABLE_COMMENT, CREATE_OPTIONS
349 9409
FROM information_schema.TABLES
350
WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = %s AND TABLE_NAME = %s
351 9409
SQL
352
            ,
353
            $database ? $this->quoteStringLiteral($database) : 'DATABASE()',
354
            $this->quoteStringLiteral($table)
355
        );
356
    }
357 10641
358
    /**
359 10641
     * {@inheritDoc}
360
     */
361 10641
    public function getCreateDatabaseSQL(string $database) : string
362 10630
    {
363
        return 'CREATE DATABASE ' . $database;
364 7056
    }
365
366
    /**
367
     * {@inheritDoc}
368
     */
369
    public function getDropDatabaseSQL(string $database) : string
370 10641
    {
371 10641
        return 'DROP DATABASE ' . $database;
372
    }
373
374 9219
    /**
375
     * {@inheritDoc}
376 9219
     */
377
    protected function _getCreateTableSQL(string $tableName, array $columns, array $options = []) : array
378
    {
379
        $queryFields = $this->getColumnDeclarationListSQL($columns);
380
381
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
382
            foreach ($options['uniqueConstraints'] as $name => $definition) {
383 9219
                $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition);
384 9219
            }
385
        }
386
387
        // add all indexes
388
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
389
            foreach ($options['indexes'] as $index => $definition) {
390
                $queryFields .= ', ' . $this->getIndexDeclarationSQL($index, $definition);
391 12460
            }
392
        }
393 12460
394
        // attach all primary keys
395
        if (isset($options['primary']) && ! empty($options['primary'])) {
396
            $keyColumns   = array_unique(array_values($options['primary']));
397
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
398
        }
399 12460
400
        $query = 'CREATE ';
401 12460
402
        if (! empty($options['temporary'])) {
403
            $query .= 'TEMPORARY ';
404
        }
405
406
        $query .= 'TABLE ' . $tableName . ' (' . $queryFields . ') ';
407 12543
        $query .= $this->buildTableOptions($options);
408
        $query .= $this->buildPartitionOptions($options);
409 12543
410
        $sql    = [$query];
411 12543
        $engine = 'INNODB';
412
413
        if (isset($options['engine'])) {
414
            $engine = strtoupper(trim($options['engine']));
415
        }
416
417
        // Propagate foreign key constraints only for InnoDB.
418 12543
        if (isset($options['foreignKeys']) && $engine === 'INNODB') {
419 11529
            foreach ((array) $options['foreignKeys'] as $definition) {
420 11529
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
421
            }
422
        }
423
424
        return $sql;
425 12543
    }
426 12354
427 12354
    /**
428
     * {@inheritdoc}
429
     */
430 12543
    public function getDefaultValueDeclarationSQL(array $field) : string
431
    {
432 12543
        // Unset the default value if the given field definition does not allow default values.
433
        if ($field['type'] instanceof TextType || $field['type'] instanceof BlobType) {
434
            $field['default'] = null;
435
        }
436 12543
437 12543
        return parent::getDefaultValueDeclarationSQL($field);
438 12543
    }
439
440 12543
    /**
441 12543
     * Build SQL for table options
442
     *
443 12543
     * @param mixed[] $options
444 10317
     */
445
    private function buildTableOptions(array $options) : string
446
    {
447
        if (isset($options['table_options'])) {
448 12543
            return $options['table_options'];
449 12336
        }
450 11436
451
        $tableOptions = [];
452
453
        // Charset
454 12543
        if (! isset($options['charset'])) {
455
            $options['charset'] = 'utf8';
456
        }
457
458
        $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']);
459
460 12657
        // Collate
461
        if (! isset($options['collate'])) {
462
            $options['collate'] = $options['charset'] . '_unicode_ci';
463 12657
        }
464 12405
465
        $tableOptions[] = sprintf('COLLATE %s', $options['collate']);
466
467 12657
        // Engine
468
        if (! isset($options['engine'])) {
469
            $options['engine'] = 'InnoDB';
470
        }
471
472
        $tableOptions[] = sprintf('ENGINE = %s', $options['engine']);
473
474
        // Auto increment
475
        if (isset($options['auto_increment'])) {
476
            $tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']);
477 12543
        }
478
479 12543
        // Comment
480
        if (isset($options['comment'])) {
481
            $comment = trim($options['comment'], " '");
482
483 12543
            $tableOptions[] = sprintf('COMMENT = %s ', $this->quoteStringLiteral($comment));
484
        }
485
486 12543
        // Row format
487 12543
        if (isset($options['row_format'])) {
488
            $tableOptions[] = sprintf('ROW_FORMAT = %s', $options['row_format']);
489
        }
490 12543
491
        return implode(' ', $tableOptions);
492
    }
493 12543
494 12543
    /**
495
     * Build SQL for partition options.
496
     *
497 12543
     * @param mixed[] $options
498
     */
499
    private function buildPartitionOptions(array $options) : string
500 12543
    {
501 12525
        return isset($options['partition_options'])
502
            ? ' ' . $options['partition_options']
503
            : '';
504 12543
    }
505
506
    /**
507 12543
     * {@inheritDoc}
508
     */
509
    public function getAlterTableSQL(TableDiff $diff) : array
510
    {
511
        $columnSql  = [];
512 12543
        $queryParts = [];
513
        $newName    = $diff->getNewName();
514
515
        if ($newName !== false) {
516
            $queryParts[] = 'RENAME TO ' . $newName->getQuotedName($this);
517
        }
518
519 12543
        foreach ($diff->addedColumns as $column) {
520
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
521
                continue;
522
            }
523 12543
524
            $columnArray            = $column->toArray();
525
            $columnArray['comment'] = $this->getColumnComment($column);
526
            $queryParts[]           = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
527
        }
528
529
        foreach ($diff->removedColumns as $column) {
530
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
531
                continue;
532
            }
533 12543
534
            $queryParts[] =  'DROP ' . $column->getQuotedName($this);
535 12543
        }
536
537 12543
        foreach ($diff->changedColumns as $columnDiff) {
538
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
539
                continue;
540
            }
541
542
            $column      = $columnDiff->column;
543 10907
            $columnArray = $column->toArray();
544
545 10907
            // Don't propagate default value changes for unsupported column types.
546 10907
            if ($columnDiff->hasChanged('default') &&
547 10907
                count($columnDiff->changedProperties) === 1 &&
548
                ($columnArray['type'] instanceof TextType || $columnArray['type'] instanceof BlobType)
549 10907
            ) {
550 6537
                continue;
551
            }
552
553 10907
            $columnArray['comment'] = $this->getColumnComment($column);
554 10725
            $queryParts[]           =  'CHANGE ' . ($columnDiff->getOldColumnName()->getQuotedName($this)) . ' '
555
                    . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
556
        }
557
558 10725
        foreach ($diff->renamedColumns as $oldColumnName => $column) {
559 10725
            if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
560 10725
                continue;
561
            }
562
563 10907
            $oldColumnName          = new Identifier($oldColumnName);
564 9432
            $columnArray            = $column->toArray();
565
            $columnArray['comment'] = $this->getColumnComment($column);
566
            $queryParts[]           =  'CHANGE ' . $oldColumnName->getQuotedName($this) . ' '
567
                    . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
568 9432
        }
569
570
        if (isset($diff->addedIndexes['primary'])) {
571 10907
            $keyColumns   = array_unique(array_values($diff->addedIndexes['primary']->getColumns()));
572 10036
            $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')';
573
            unset($diff->addedIndexes['primary']);
574
        } elseif (isset($diff->changedIndexes['primary'])) {
575
            // Necessary in case the new primary key includes a new auto_increment column
576 10036
            foreach ($diff->changedIndexes['primary']->getColumns() as $columnName) {
577 10036
                if (isset($diff->addedColumns[$columnName]) && $diff->addedColumns[$columnName]->getAutoincrement()) {
578
                    $keyColumns   = array_unique(array_values($diff->changedIndexes['primary']->getColumns()));
579
                    $queryParts[] = 'DROP PRIMARY KEY';
580 10036
                    $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')';
581 10036
                    unset($diff->changedIndexes['primary']);
582 10036
                    break;
583
                }
584 8633
            }
585
        }
586
587 10000
        $sql      = [];
588 10000
        $tableSql = [];
589 10000
590
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
591
            if (count($queryParts) > 0) {
592 10907
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . implode(', ', $queryParts);
593 9348
            }
594
            $sql = array_merge(
595
                $this->getPreAlterTableIndexForeignKeySQL($diff),
596
                $sql,
597 9348
                $this->getPostAlterTableIndexForeignKeySQL($diff)
598 9348
            );
599 9348
        }
600 9348
601 9348
        return array_merge($sql, $tableSql, $columnSql);
602
    }
603
604 10907
    /**
605 10379
     * {@inheritDoc}
606 10379
     */
607 10379
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) : array
608 10872
    {
609
        $sql   = [];
610 10735
        $table = $diff->getName($this)->getQuotedName($this);
611 10735
612 9219
        foreach ($diff->changedIndexes as $changedIndex) {
613 9219
            $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $changedIndex));
614 9219
        }
615 9219
616 9245
        foreach ($diff->removedIndexes as $remKey => $remIndex) {
617
            $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $remIndex));
618
619
            foreach ($diff->addedIndexes as $addKey => $addIndex) {
620
                if ($remIndex->getColumns() === $addIndex->getColumns()) {
621 10907
                    $indexClause = 'INDEX ' . $addIndex->getName();
622 10907
623
                    if ($addIndex->isPrimary()) {
624 10907
                        $indexClause = 'PRIMARY KEY';
625 10907
                    } elseif ($addIndex->isUnique()) {
626 10829
                        $indexClause = 'UNIQUE INDEX ' . $addIndex->getName();
627
                    }
628 10907
629 10907
                    $query  = 'ALTER TABLE ' . $table . ' DROP INDEX ' . $remIndex->getName() . ', ';
630 10907
                    $query .= 'ADD ' . $indexClause;
631 10907
                    $query .= ' (' . $this->getIndexFieldDeclarationListSQL($addIndex) . ')';
632
633
                    $sql[] = $query;
634
635 10907
                    unset($diff->removedIndexes[$remKey], $diff->addedIndexes[$addKey]);
636
637
                    break;
638
                }
639
            }
640
        }
641 10907
642
        $engine = 'INNODB';
643 10907
644 10907
        if ($diff->fromTable instanceof Table && $diff->fromTable->hasOption('engine')) {
645
            $engine = strtoupper(trim($diff->fromTable->getOption('engine')));
646 10907
        }
647 9631
648
        // Suppress foreign key constraint propagation on non-supporting engines.
649
        if ($engine !== 'INNODB') {
650 10907
            $diff->addedForeignKeys   = [];
651 10262
            $diff->changedForeignKeys = [];
652
            $diff->removedForeignKeys = [];
653 10262
        }
654 7581
655 7581
        $sql = array_merge(
656
            $sql,
657 7581
            $this->getPreAlterTableAlterIndexForeignKeySQL($diff),
658
            parent::getPreAlterTableIndexForeignKeySQL($diff),
659 7581
            $this->getPreAlterTableRenameIndexForeignKeySQL($diff)
660 7581
        );
661
662
        return $sql;
663 7581
    }
664 7581
665 7581
    /**
666
     * @return string[]
667 7581
     */
668
    private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $index) : array
669 7581
    {
670
        $sql = [];
671 7593
672
        if (! $index->isPrimary() || ! $diff->fromTable instanceof Table) {
673
            return $sql;
674
        }
675
676 10907
        $tableName = $diff->getName($this)->getQuotedName($this);
677
678 10907
        // Dropping primary keys requires to unset autoincrement attribute on the particular column first.
679 10275
        foreach ($index->getColumns() as $columnName) {
680
            if (! $diff->fromTable->hasColumn($columnName)) {
681
                continue;
682
            }
683 10907
684 7206
            $column = $diff->fromTable->getColumn($columnName);
685 7206
686 7206
            if (! $column->getAutoincrement()) {
687
                continue;
688
            }
689 10907
690 10907
            $column->setAutoincrement(false);
691 10907
692 10907
            $sql[] = 'ALTER TABLE ' . $tableName . ' MODIFY ' .
693 10907
                $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
694
695
            // original autoincrement information might be needed later on by other parts of the table alteration
696 10907
            $column->setAutoincrement(true);
697
        }
698
699
        return $sql;
700
    }
701
702 10294
    /**
703
     * @param TableDiff $diff The table diff to gather the SQL for.
704 10294
     *
705
     * @return string[]
706 10294
     */
707 10262
    private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff) : array
708
    {
709
        $sql   = [];
710 10231
        $table = $diff->getName($this)->getQuotedName($this);
711
712
        foreach ($diff->changedIndexes as $changedIndex) {
713 10231
            // Changed primary key
714 10231
            if (! $changedIndex->isPrimary() || ! ($diff->fromTable instanceof Table)) {
715 7306
                continue;
716
            }
717
718 10231
            foreach ($diff->fromTable->getPrimaryKeyColumns() as $columnName) {
719
                $column = $diff->fromTable->getColumn($columnName);
720 10231
721 10225
                // Check if an autoincrement column was dropped from the primary key.
722
                if (! $column->getAutoincrement() || in_array($columnName, $changedIndex->getColumns())) {
723
                    continue;
724 10212
                }
725
726 10212
                // The autoincrement attribute needs to be removed from the dropped column
727 10212
                // before we can drop and recreate the primary key.
728
                $column->setAutoincrement(false);
729
730 10212
                $sql[] = 'ALTER TABLE ' . $table . ' MODIFY ' .
731
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());
732
733 10231
                // Restore the autoincrement attribute as it might be needed later on
734
                // by other parts of the table alteration.
735
                $column->setAutoincrement(true);
736
            }
737
        }
738
739
        return $sql;
740
    }
741 10907
742
    /**
743 10907
     * @param TableDiff $diff The table diff to gather the SQL for.
744 10907
     *
745
     * @return string[]
746 10907
     */
747
    protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff) : array
748 9631
    {
749 9580
        $sql       = [];
750
        $tableName = $diff->getName($this)->getQuotedName($this);
751
752 7476
        foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) {
753 7476
            if (in_array($foreignKey, $diff->changedForeignKeys, true)) {
754
                continue;
755
            }
756 7476
757 7420
            $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName);
758
        }
759
760
        return $sql;
761
    }
762 7456
763
    /**
764 7456
     * Returns the remaining foreign key constraints that require one of the renamed indexes.
765 7456
     *
766
     * "Remaining" here refers to the diff between the foreign keys currently defined in the associated
767
     * table and the foreign keys to be removed.
768
     *
769 7476
     * @param TableDiff $diff The table diff to evaluate.
770
     *
771
     * @return ForeignKeyConstraint[]
772
     */
773 10907
    private function getRemainingForeignKeyConstraintsRequiringRenamedIndexes(TableDiff $diff) : array
774
    {
775
        if (empty($diff->renamedIndexes) || ! $diff->fromTable instanceof Table) {
776
            return [];
777
        }
778
779
        $foreignKeys = [];
780
        /** @var ForeignKeyConstraint[] $remainingForeignKeys */
781 9597
        $remainingForeignKeys = array_diff_key(
782
            $diff->fromTable->getForeignKeys(),
783 9597
            $diff->removedForeignKeys
784 9597
        );
785
786 9597
        foreach ($remainingForeignKeys as $foreignKey) {
787 7744
            foreach ($diff->renamedIndexes as $index) {
788
                if ($foreignKey->intersectsIndexColumns($index)) {
789
                    $foreignKeys[] = $foreignKey;
790
791 7744
                    break;
792
                }
793
            }
794 9597
        }
795
796
        return $foreignKeys;
797
    }
798
799
    /**
800
     * {@inheritdoc}
801
     */
802
    protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) : array
803
    {
804
        return array_merge(
805
            parent::getPostAlterTableIndexForeignKeySQL($diff),
806
            $this->getPostAlterTableRenameIndexForeignKeySQL($diff)
807 9597
        );
808
    }
809 9597
810 9577
    /**
811
     * @param TableDiff $diff The table diff to gather the SQL for.
812
     *
813 8028
     * @return string[]
814
     */
815 8028
    protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff) : array
816 8028
    {
817 8028
        $sql     = [];
818
        $newName = $diff->getNewName();
819
820 8028
        if ($newName !== false) {
821 7744
            $tableName = $newName->getQuotedName($this);
822 7744
        } else {
823 7744
            $tableName = $diff->getName($this)->getQuotedName($this);
824
        }
825 7744
826
        foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) {
827
            if (in_array($foreignKey, $diff->changedForeignKeys, true)) {
828
                continue;
829
            }
830 8028
831
            $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName);
832
        }
833
834
        return $sql;
835
    }
836 10907
837
    /**
838 10907
     * {@inheritDoc}
839 10907
     */
840 10907
    protected function getCreateIndexSQLFlags(Index $index) : string
841
    {
842
        $type = '';
843
        if ($index->isUnique()) {
844
            $type .= 'UNIQUE ';
845
        } elseif ($index->hasFlag('fulltext')) {
846
            $type .= 'FULLTEXT ';
847
        } elseif ($index->hasFlag('spatial')) {
848
            $type .= 'SPATIAL ';
849 9597
        }
850
851 9597
        return $type;
852 9597
    }
853
854 9597
    /**
855 6533
     * {@inheritDoc}
856
     */
857 9589
    public function getIntegerTypeDeclarationSQL(array $columnDef) : string
858
    {
859
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
860 9597
    }
861 7744
862
    /**
863
     * {@inheritDoc}
864
     */
865 7744
    public function getBigIntTypeDeclarationSQL(array $columnDef) : string
866
    {
867
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
868 9597
    }
869
870
    /**
871
     * {@inheritDoc}
872
     */
873
    public function getSmallIntTypeDeclarationSQL(array $columnDef) : string
874 11599
    {
875
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef);
876 11599
    }
877 11599
878 11387
    /**
879 11561
     * {@inheritdoc}
880 10305
     */
881 11550
    public function getFloatDeclarationSQL(array $fieldDeclaration) : string
882 10280
    {
883
        return 'DOUBLE PRECISION' . $this->getUnsignedDeclaration($fieldDeclaration);
884
    }
885 11599
886
    /**
887
     * {@inheritdoc}
888
     */
889
    public function getDecimalTypeDeclarationSQL(array $columnDef) : string
890
    {
891 12563
        return parent::getDecimalTypeDeclarationSQL($columnDef) . $this->getUnsignedDeclaration($columnDef);
892
    }
893 12563
894
    /**
895
     * Get unsigned declaration for a column.
896
     *
897
     * @param mixed[] $columnDef
898
     */
899 6973
    private function getUnsignedDeclaration(array $columnDef) : string
900
    {
901 6973
        return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : '';
902
    }
903
904
    /**
905
     * {@inheritDoc}
906
     */
907 7929
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) : string
908
    {
909 7929
        $autoinc = '';
910
        if (! empty($columnDef['autoincrement'])) {
911
            $autoinc = ' AUTO_INCREMENT';
912
        }
913
914
        return $this->getUnsignedDeclaration($columnDef) . $autoinc;
915 9674
    }
916
917 9674
    /**
918
     * {@inheritDoc}
919
     */
920
    public function getColumnCharsetDeclarationSQL(string $charset) : string
921
    {
922
        return 'CHARACTER SET ' . $charset;
923 9719
    }
924
925 9719
    /**
926
     * {@inheritDoc}
927
     */
928
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) : string
929
    {
930
        $query = '';
931
        if ($foreignKey->hasOption('match')) {
932
            $query .= ' MATCH ' . $foreignKey->getOption('match');
933
        }
934
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
935 12635
936
        return $query;
937 12635
    }
938
939
    /**
940
     * {@inheritDoc}
941
     */
942
    public function getDropIndexSQL($index, $table = null) : string
943 12563
    {
944
        if ($index instanceof Index) {
945 12563
            $indexName = $index->getQuotedName($this);
946 12563
        } elseif (is_string($index)) {
947 11127
            $indexName = $index;
948
        } else {
949
            throw new InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
950 12563
        }
951
952
        if ($table instanceof Table) {
953
            $table = $table->getQuotedName($this);
954
        } elseif (! is_string($table)) {
955
            throw new InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.');
956 10074
        }
957
958 10074
        if ($index instanceof Index && $index->isPrimary()) {
959
            // mysql primary keys are always named "PRIMARY",
960
            // so we cannot use them in statements because of them being keyword.
961
            return $this->getDropPrimaryKeySQL($table);
962
        }
963
964 11466
        return 'DROP INDEX ' . $indexName . ' ON ' . $table;
965
    }
966 11466
967 11466
    protected function getDropPrimaryKeySQL(string $table) : string
968
    {
969
        return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY';
970 11466
    }
971
972 11466
    /**
973
     * {@inheritDoc}
974
     */
975
    public function getSetTransactionIsolationSQL(int $level) : string
976
    {
977
        return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
978 10288
    }
979
980 10288
    /**
981 10268
     * {@inheritDoc}
982 9424
     */
983 9424
    public function getName() : string
984
    {
985
        return 'mysql';
986
    }
987
988 10288
    /**
989 8189
     * {@inheritDoc}
990 10288
     */
991
    public function getReadLockSQL() : string
992
    {
993
        return 'LOCK IN SHARE MODE';
994 10288
    }
995
996
    /**
997 10237
     * {@inheritDoc}
998
     */
999
    protected function initializeDoctrineTypeMappings() : void
1000 10250
    {
1001
        $this->doctrineTypeMapping = [
1002
            'bigint'     => 'bigint',
1003
            'binary'     => 'binary',
1004
            'blob'       => 'blob',
1005
            'char'       => 'string',
1006
            'date'       => 'date',
1007
            'datetime'   => 'datetime',
1008 10237
            'decimal'    => 'decimal',
1009
            'double'     => 'float',
1010 10237
            'float'      => 'float',
1011
            'int'        => 'integer',
1012
            'integer'    => 'integer',
1013
            'longblob'   => 'blob',
1014
            'longtext'   => 'text',
1015
            'mediumblob' => 'blob',
1016 7806
            'mediumint'  => 'integer',
1017
            'mediumtext' => 'text',
1018 7806
            'numeric'    => 'decimal',
1019
            'real'       => 'float',
1020
            'set'        => 'simple_array',
1021
            'smallint'   => 'smallint',
1022
            'string'     => 'string',
1023
            'text'       => 'text',
1024 12146
            'time'       => 'time',
1025
            'timestamp'  => 'datetime',
1026 12146
            'tinyblob'   => 'blob',
1027
            'tinyint'    => 'boolean',
1028
            'tinytext'   => 'text',
1029
            'varbinary'  => 'binary',
1030
            'varchar'    => 'string',
1031
            'year'       => 'date',
1032
        ];
1033
    }
1034
1035
    /**
1036
     * {@inheritDoc}
1037
     */
1038
    public function getVarcharMaxLength() : int
1039
    {
1040 10833
        return 65535;
1041
    }
1042 10833
1043
    /**
1044
     * {@inheritdoc}
1045
     */
1046
    public function getBinaryMaxLength() : int
1047
    {
1048
        return 65535;
1049
    }
1050
1051
    /**
1052
     * {@inheritDoc}
1053
     */
1054
    protected function getReservedKeywordsClass() : string
1055
    {
1056
        return Keywords\MySQLKeywords::class;
1057
    }
1058
1059
    /**
1060
     * {@inheritDoc}
1061
     *
1062
     * MySQL commits a transaction implicitly when DROP TABLE is executed, however not
1063
     * if DROP TEMPORARY TABLE is executed.
1064
     */
1065
    public function getDropTemporaryTableSQL($table) : string
1066
    {
1067
        if ($table instanceof Table) {
1068
            $table = $table->getQuotedName($this);
1069
        } elseif (! is_string($table)) {
1070
            throw new InvalidArgumentException('getDropTemporaryTableSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.');
1071
        }
1072
1073
        return 'DROP TEMPORARY TABLE ' . $table;
1074 10833
    }
1075
1076
    /**
1077
     * Gets the SQL Snippet used to declare a BLOB column type.
1078
     *     TINYBLOB   : 2 ^  8 - 1 = 255
1079 12360
     *     BLOB       : 2 ^ 16 - 1 = 65535
1080
     *     MEDIUMBLOB : 2 ^ 24 - 1 = 16777215
1081 12360
     *     LONGBLOB   : 2 ^ 32 - 1 = 4294967295
1082
     *
1083
     * {@inheritDoc}
1084
     */
1085
    public function getBlobTypeDeclarationSQL(array $field) : string
1086
    {
1087 6081
        if (! empty($field['length']) && is_numeric($field['length'])) {
1088
            $length = $field['length'];
1089 6081
1090
            if ($length <= static::LENGTH_LIMIT_TINYBLOB) {
1091
                return 'TINYBLOB';
1092
            }
1093
1094
            if ($length <= static::LENGTH_LIMIT_BLOB) {
1095 6354
                return 'BLOB';
1096
            }
1097 6354
1098
            if ($length <= static::LENGTH_LIMIT_MEDIUMBLOB) {
1099
                return 'MEDIUMBLOB';
1100
            }
1101
        }
1102
1103
        return 'LONGBLOB';
1104
    }
1105
1106 7013
    /**
1107
     * {@inheritdoc}
1108 7013
     */
1109
    public function quoteStringLiteral(string $str) : string
1110 7013
    {
1111
        $str = str_replace('\\', '\\\\', $str); // MySQL requires backslashes to be escaped aswell.
1112
1113
        return parent::quoteStringLiteral($str);
1114 7013
    }
1115
1116
    /**
1117
     * {@inheritdoc}
1118
     */
1119
    public function getDefaultTransactionIsolationLevel() : int
1120
    {
1121
        return TransactionIsolationLevel::REPEATABLE_READ;
1122
    }
1123
1124
    /**
1125
     * {@inheritdoc}
1126 12384
     */
1127
    public function supportsColumnLengthIndexes() : bool
1128 12384
    {
1129 10094
        return true;
1130
    }
1131 10094
1132 10094
    /**
1133
     * Returns an SQL expression representing the given database name or current database name
1134
     *
1135 10094
     * @param string|null $database Database name
1136 10094
     */
1137
    private function getDatabaseNameSql(?string $database) : string
1138
    {
1139 10094
        if ($database === null) {
1140 10094
            return 'DATABASE()';
1141
        }
1142
1143
        return $this->quoteStringLiteral($database);
1144 12384
    }
1145
}
1146