MySqlPlatform.php 34.3 KB
Newer Older
1 2
<?php

3
namespace Doctrine\DBAL\Platforms;
4

5
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
6
use Doctrine\DBAL\Schema\Identifier;
Benjamin Morel's avatar
Benjamin Morel committed
7 8
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
9
use Doctrine\DBAL\Schema\TableDiff;
10
use Doctrine\DBAL\TransactionIsolationLevel;
11 12
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\TextType;
13
use InvalidArgumentException;
14 15 16 17 18 19 20 21 22 23 24 25 26 27
use function array_diff_key;
use function array_merge;
use function array_unique;
use function array_values;
use function count;
use function func_get_args;
use function implode;
use function in_array;
use function is_numeric;
use function is_string;
use function sprintf;
use function str_replace;
use function strtoupper;
use function trim;
28

29 30
/**
 * The MySqlPlatform provides the behavior, features and SQL dialect of the
31 32
 * MySQL database platform. This platform represents a MySQL 5.0 or greater platform that
 * uses the InnoDB storage engine.
33
 *
Benjamin Morel's avatar
Benjamin Morel committed
34
 * @todo   Rename: MySQLPlatform
35
 */
36
class MySqlPlatform extends AbstractPlatform
37
{
38 39 40
    public const LENGTH_LIMIT_TINYTEXT   = 255;
    public const LENGTH_LIMIT_TEXT       = 65535;
    public const LENGTH_LIMIT_MEDIUMTEXT = 16777215;
41

42 43 44
    public const LENGTH_LIMIT_TINYBLOB   = 255;
    public const LENGTH_LIMIT_BLOB       = 65535;
    public const LENGTH_LIMIT_MEDIUMBLOB = 16777215;
45

46
    /**
47
     * {@inheritDoc}
48 49 50 51 52
     */
    protected function doModifyLimitQuery($query, $limit, $offset)
    {
        if ($limit !== null) {
            $query .= ' LIMIT ' . $limit;
53 54

            if ($offset > 0) {
55 56
                $query .= ' OFFSET ' . $offset;
            }
57 58
        } elseif ($offset > 0) {
            // 2^64-1 is the maximum of unsigned BIGINT, the biggest limit possible
59 60 61 62 63 64
            $query .= ' LIMIT 18446744073709551615 OFFSET ' . $offset;
        }

        return $query;
    }

romanb's avatar
romanb committed
65
    /**
66
     * {@inheritDoc}
romanb's avatar
romanb committed
67 68 69 70
     */
    public function getIdentifierQuoteCharacter()
    {
        return '`';
71
    }
72

73
    /**
74
     * {@inheritDoc}
75 76 77 78 79 80 81
     */
    public function getRegexpExpression()
    {
        return 'RLIKE';
    }

    /**
82
     * {@inheritDoc}
83 84
     *
     * @deprecated Use application-generated UUIDs instead
85 86 87 88 89 90
     */
    public function getGuidExpression()
    {
        return 'UUID()';
    }

91
    /**
92
     * {@inheritDoc}
93 94 95
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
96
        if ($startPos === false) {
97 98
            return 'LOCATE(' . $substr . ', ' . $str . ')';
        }
99

100
        return 'LOCATE(' . $substr . ', ' . $str . ', ' . $startPos . ')';
101 102
    }

103
    /**
104
     * {@inheritDoc}
105 106 107
     */
    public function getConcatExpression()
    {
108
        return sprintf('CONCAT(%s)', implode(', ', func_get_args()));
109
    }
110

111
    /**
112
     * {@inheritdoc}
113
     */
114
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
115
    {
116
        $function = $operator === '+' ? 'DATE_ADD' : 'DATE_SUB';
117

118
        return $function . '(' . $date . ', INTERVAL ' . $interval . ' ' . $unit . ')';
119 120
    }

121 122 123
    /**
     * {@inheritDoc}
     */
124
    public function getDateDiffExpression($date1, $date2)
125
    {
126
        return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')';
127
    }
128

Benjamin Morel's avatar
Benjamin Morel committed
129 130 131
    /**
     * {@inheritDoc}
     */
132
    public function getListDatabasesSQL()
133 134 135 136
    {
        return 'SHOW DATABASES';
    }

Benjamin Morel's avatar
Benjamin Morel committed
137 138 139
    /**
     * {@inheritDoc}
     */
140
    public function getListTableConstraintsSQL($table)
141
    {
142
        return 'SHOW INDEX FROM ' . $table;
143 144
    }

145
    /**
146 147
     * {@inheritDoc}
     *
148
     * Two approaches to listing the table indexes. The information_schema is
Pascal Borreli's avatar
Pascal Borreli committed
149
     * preferred, because it doesn't cause problems with SQL keywords such as "order" or "table".
150 151
     */
    public function getListTableIndexesSQL($table, $currentDatabase = null)
152
    {
153
        if ($currentDatabase) {
154
            $currentDatabase = $this->quoteStringLiteral($currentDatabase);
155
            $table           = $this->quoteStringLiteral($table);
156

157 158 159 160
            return 'SELECT NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, COLUMN_NAME AS Column_Name,' .
                   ' SUB_PART AS Sub_Part, INDEX_TYPE AS Index_Type' .
                   ' FROM information_schema.STATISTICS WHERE TABLE_NAME = ' . $table .
                   ' AND TABLE_SCHEMA = ' . $currentDatabase .
161
                   ' ORDER BY SEQ_IN_INDEX ASC';
162
        }
163 164

        return 'SHOW INDEX FROM ' . $table;
165 166
    }

Benjamin Morel's avatar
Benjamin Morel committed
167 168 169
    /**
     * {@inheritDoc}
     */
170
    public function getListViewsSQL($database)
171
    {
172 173
        $database = $this->quoteStringLiteral($database);

174
        return 'SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ' . $database;
175 176
    }

Benjamin Morel's avatar
Benjamin Morel committed
177 178 179
    /**
     * {@inheritDoc}
     */
180
    public function getListTableForeignKeysSQL($table, $database = null)
181
    {
182 183
        $table = $this->quoteStringLiteral($table);

184
        if ($database !== null) {
185 186 187
            $database = $this->quoteStringLiteral($database);
        }

188 189 190 191 192
        $sql = 'SELECT DISTINCT k.`CONSTRAINT_NAME`, k.`COLUMN_NAME`, k.`REFERENCED_TABLE_NAME`, ' .
               'k.`REFERENCED_COLUMN_NAME` /*!50116 , c.update_rule, c.delete_rule */ ' .
               'FROM information_schema.key_column_usage k /*!50116 ' .
               'INNER JOIN information_schema.referential_constraints c ON ' .
               '  c.constraint_name = k.constraint_name AND ' .
193
               '  c.table_name = ' . $table . ' */ WHERE k.table_name = ' . $table;
194

195
        $databaseNameSql = $database ?? 'DATABASE()';
196

197
        $sql .= ' AND k.table_schema = ' . $databaseNameSql . ' /*!50116 AND c.constraint_schema = ' . $databaseNameSql . ' */';
198
        $sql .= ' AND k.`REFERENCED_COLUMN_NAME` is not NULL';
199 200 201 202

        return $sql;
    }

Benjamin Morel's avatar
Benjamin Morel committed
203 204 205
    /**
     * {@inheritDoc}
     */
206
    public function getCreateViewSQL($name, $sql)
207 208 209 210
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

Benjamin Morel's avatar
Benjamin Morel committed
211 212 213
    /**
     * {@inheritDoc}
     */
214
    public function getDropViewSQL($name)
215
    {
216
        return 'DROP VIEW ' . $name;
217 218
    }

219
    /**
220
     * {@inheritDoc}
221
     */
222
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
223 224
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
225
                : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
226
    }
227

Steve Müller's avatar
Steve Müller committed
228 229 230 231 232 233 234 235
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
    }

236
    /**
237 238 239 240 241 242
     * Gets the SQL snippet used to declare a CLOB column type.
     *     TINYTEXT   : 2 ^  8 - 1 = 255
     *     TEXT       : 2 ^ 16 - 1 = 65535
     *     MEDIUMTEXT : 2 ^ 24 - 1 = 16777215
     *     LONGTEXT   : 2 ^ 32 - 1 = 4294967295
     *
243
     * {@inheritDoc}
244
     */
245
    public function getClobTypeDeclarationSQL(array $field)
246
    {
247
        if (! empty($field['length']) && is_numeric($field['length'])) {
248
            $length = $field['length'];
249

250
            if ($length <= static::LENGTH_LIMIT_TINYTEXT) {
251
                return 'TINYTEXT';
252 253
            }

254
            if ($length <= static::LENGTH_LIMIT_TEXT) {
255
                return 'TEXT';
256 257
            }

258
            if ($length <= static::LENGTH_LIMIT_MEDIUMTEXT) {
259 260 261
                return 'MEDIUMTEXT';
            }
        }
262

263 264
        return 'LONGTEXT';
    }
265

266
    /**
267
     * {@inheritDoc}
268
     */
269
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
270
    {
271
        if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] === true) {
272 273
            return 'TIMESTAMP';
        }
274 275

        return 'DATETIME';
276
    }
277

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

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

294
    /**
295
     * {@inheritDoc}
296
     */
297
    public function getBooleanTypeDeclarationSQL(array $field)
298 299 300 301
    {
        return 'TINYINT(1)';
    }

302 303 304 305
    /**
     * Obtain DBMS specific SQL code portion needed to set the COLLATION
     * of a field declaration to be used in statements like CREATE TABLE.
     *
306 307
     * @deprecated Deprecated since version 2.5, Use {@link self::getColumnCollationDeclarationSQL()} instead.
     *
308
     * @param string $collation name of the collation
309
     *
310 311 312 313 314
     * @return string  DBMS specific SQL code portion needed to set the COLLATION
     *                 of a field declaration.
     */
    public function getCollationFieldDeclaration($collation)
    {
315
        return $this->getColumnCollationDeclarationSQL($collation);
316
    }
317

318
    /**
319 320
     * {@inheritDoc}
     *
321 322 323 324 325 326 327
     * MySql prefers "autoincrement" identity columns since sequences can only
     * be emulated with a table.
     */
    public function prefersIdentityColumns()
    {
        return true;
    }
328

romanb's avatar
romanb committed
329
    /**
330
     * {@inheritDoc}
romanb's avatar
romanb committed
331
     *
332
     * MySql supports this through AUTO_INCREMENT columns.
romanb's avatar
romanb committed
333 334 335 336
     */
    public function supportsIdentityColumns()
    {
        return true;
337 338
    }

339 340 341
    /**
     * {@inheritDoc}
     */
342 343 344
    public function supportsInlineColumnComments()
    {
        return true;
romanb's avatar
romanb committed
345
    }
346

Benjamin Morel's avatar
Benjamin Morel committed
347 348 349
    /**
     * {@inheritDoc}
     */
350 351 352 353 354
    public function supportsColumnCollation()
    {
        return true;
    }

355 356 357
    /**
     * {@inheritDoc}
     */
358
    public function getListTablesSQL()
359
    {
360
        return "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'";
361
    }
362

Benjamin Morel's avatar
Benjamin Morel committed
363 364 365
    /**
     * {@inheritDoc}
     */
366
    public function getListTableColumnsSQL($table, $database = null)
367
    {
368 369
        $table = $this->quoteStringLiteral($table);

370
        if ($database) {
371
            $database = $this->quoteStringLiteral($database);
372 373
        } else {
            $database = 'DATABASE()';
374
        }
375

376 377 378
        return 'SELECT COLUMN_NAME AS Field, COLUMN_TYPE AS Type, IS_NULLABLE AS `Null`, ' .
               'COLUMN_KEY AS `Key`, COLUMN_DEFAULT AS `Default`, EXTRA AS Extra, COLUMN_COMMENT AS Comment, ' .
               'CHARACTER_SET_NAME AS CharacterSet, COLLATION_NAME AS Collation ' .
379 380
               'FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ' . $database . ' AND TABLE_NAME = ' . $table .
               ' ORDER BY ORDINAL_POSITION ASC';
381 382
    }

383 384 385 386 387 388 389 390 391 392 393 394 395 396
    public function getListTableMetadataSQL(string $table, ?string $database = null) : string
    {
        return sprintf(
            <<<'SQL'
SELECT ENGINE, AUTO_INCREMENT, TABLE_COLLATION, TABLE_COMMENT, CREATE_OPTIONS
FROM information_schema.TABLES
WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = %s AND TABLE_NAME = %s
SQL
            ,
            $database ? $this->quoteStringLiteral($database) : 'DATABASE()',
            $this->quoteStringLiteral($table)
        );
    }

397
    /**
398
     * {@inheritDoc}
399
     */
400
    public function getCreateDatabaseSQL($name)
401
    {
402
        return 'CREATE DATABASE ' . $name;
403
    }
404

405
    /**
406
     * {@inheritDoc}
407
     */
408
    public function getDropDatabaseSQL($name)
409
    {
410
        return 'DROP DATABASE ' . $name;
411
    }
412

413
    /**
414
     * {@inheritDoc}
415
     */
416
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
417
    {
418
        $queryFields = $this->getColumnDeclarationListSQL($columns);
419

420
        if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
421
            foreach ($options['uniqueConstraints'] as $index => $definition) {
422
                $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($index, $definition);
423
            }
424 425 426 427
        }

        // add all indexes
        if (isset($options['indexes']) && ! empty($options['indexes'])) {
Steve Müller's avatar
Steve Müller committed
428
            foreach ($options['indexes'] as $index => $definition) {
429
                $queryFields .= ', ' . $this->getIndexDeclarationSQL($index, $definition);
430 431 432 433 434
            }
        }

        // attach all primary keys
        if (isset($options['primary']) && ! empty($options['primary'])) {
435
            $keyColumns   = array_unique(array_values($options['primary']));
436 437 438 439
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
        }

        $query = 'CREATE ';
440

441
        if (! empty($options['temporary'])) {
442 443
            $query .= 'TEMPORARY ';
        }
444

445
        $query .= 'TABLE ' . $tableName . ' (' . $queryFields . ') ';
446 447
        $query .= $this->buildTableOptions($options);
        $query .= $this->buildPartitionOptions($options);
448

449
        $sql    = [$query];
450
        $engine = 'INNODB';
451

452 453 454 455 456 457
        if (isset($options['engine'])) {
            $engine = strtoupper(trim($options['engine']));
        }

        // Propagate foreign key constraints only for InnoDB.
        if (isset($options['foreignKeys']) && $engine === 'INNODB') {
458 459 460 461
            foreach ((array) $options['foreignKeys'] as $definition) {
                $sql[] = $this->getCreateForeignKeySQL($definition, $tableName);
            }
        }
462

463 464 465
        return $sql;
    }

466 467 468 469 470 471 472 473 474 475 476 477 478
    /**
     * {@inheritdoc}
     */
    public function getDefaultValueDeclarationSQL($field)
    {
        // Unset the default value if the given field definition does not allow default values.
        if ($field['type'] instanceof TextType || $field['type'] instanceof BlobType) {
            $field['default'] = null;
        }

        return parent::getDefaultValueDeclarationSQL($field);
    }

479 480 481
    /**
     * Build SQL for table options
     *
482
     * @param mixed[] $options
483 484 485 486 487 488 489
     *
     * @return string
     */
    private function buildTableOptions(array $options)
    {
        if (isset($options['table_options'])) {
            return $options['table_options'];
490
        }
491

492
        $tableOptions = [];
493 494

        // Charset
495
        if (! isset($options['charset'])) {
496
            $options['charset'] = 'utf8';
497 498
        }

499 500 501
        $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']);

        // Collate
502
        if (! isset($options['collate'])) {
503
            $options['collate'] = $options['charset'] . '_unicode_ci';
504
        }
505

506
        $tableOptions[] = $this->getColumnCollationDeclarationSQL($options['collate']);
507

508
        // Engine
509
        if (! isset($options['engine'])) {
510
            $options['engine'] = 'InnoDB';
511
        }
512

513
        $tableOptions[] = sprintf('ENGINE = %s', $options['engine']);
514

515 516 517
        // Auto increment
        if (isset($options['auto_increment'])) {
            $tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']);
518
        }
519

520 521
        // Comment
        if (isset($options['comment'])) {
522
            $tableOptions[] = sprintf('COMMENT = %s ', $this->quoteStringLiteral($options['comment']));
523 524 525 526 527 528 529 530 531 532 533 534 535
        }

        // Row format
        if (isset($options['row_format'])) {
            $tableOptions[] = sprintf('ROW_FORMAT = %s', $options['row_format']);
        }

        return implode(' ', $tableOptions);
    }

    /**
     * Build SQL for partition options.
     *
536
     * @param mixed[] $options
537 538 539 540 541
     *
     * @return string
     */
    private function buildPartitionOptions(array $options)
    {
542
        return isset($options['partition_options'])
543
            ? ' ' . $options['partition_options']
544
            : '';
545
    }
546

547
    /**
548
     * {@inheritDoc}
549
     */
550
    public function getAlterTableSQL(TableDiff $diff)
551
    {
552
        $columnSql  = [];
553
        $queryParts = [];
Sergei Morozov's avatar
Sergei Morozov committed
554 555 556 557
        $newName    = $diff->getNewName();

        if ($newName !== false) {
            $queryParts[] = 'RENAME TO ' . $newName->getQuotedName($this);
558 559
        }

560
        foreach ($diff->addedColumns as $column) {
561 562
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
563 564
            }

565
            $columnArray            = $column->toArray();
566
            $columnArray['comment'] = $this->getColumnComment($column);
567
            $queryParts[]           = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
568 569
        }

570
        foreach ($diff->removedColumns as $column) {
571 572
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
573 574
            }

575
            $queryParts[] =  'DROP ' . $column->getQuotedName($this);
576 577
        }

578
        foreach ($diff->changedColumns as $columnDiff) {
579 580
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
581 582
            }

583
            $column      = $columnDiff->column;
584
            $columnArray = $column->toArray();
585 586 587 588 589 590 591 592 593

            // Don't propagate default value changes for unsupported column types.
            if ($columnDiff->hasChanged('default') &&
                count($columnDiff->changedProperties) === 1 &&
                ($columnArray['type'] instanceof TextType || $columnArray['type'] instanceof BlobType)
            ) {
                continue;
            }

594
            $columnArray['comment'] = $this->getColumnComment($column);
595
            $queryParts[]           =  'CHANGE ' . ($columnDiff->getOldColumnName()->getQuotedName($this)) . ' '
596
                    . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
597 598
        }

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

604 605
            $oldColumnName          = new Identifier($oldColumnName);
            $columnArray            = $column->toArray();
606
            $columnArray['comment'] = $this->getColumnComment($column);
607
            $queryParts[]           =  'CHANGE ' . $oldColumnName->getQuotedName($this) . ' '
608
                    . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
609 610
        }

611
        if (isset($diff->addedIndexes['primary'])) {
612
            $keyColumns   = array_unique(array_values($diff->addedIndexes['primary']->getColumns()));
613 614
            $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')';
            unset($diff->addedIndexes['primary']);
615 616 617 618 619 620 621 622 623 624 625
        } elseif (isset($diff->changedIndexes['primary'])) {
            // Necessary in case the new primary key includes a new auto_increment column
            foreach ($diff->changedIndexes['primary']->getColumns() as $columnName) {
                if (isset($diff->addedColumns[$columnName]) && $diff->addedColumns[$columnName]->getAutoincrement()) {
                    $keyColumns   = array_unique(array_values($diff->changedIndexes['primary']->getColumns()));
                    $queryParts[] = 'DROP PRIMARY KEY';
                    $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')';
                    unset($diff->changedIndexes['primary']);
                    break;
                }
            }
626 627
        }

628
        $sql      = [];
629
        $tableSql = [];
630

631
        if (! $this->onSchemaAlterTable($diff, $tableSql)) {
632
            if (count($queryParts) > 0) {
633
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . implode(', ', $queryParts);
634 635 636 637 638 639
            }
            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
640
        }
641 642

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

645
    /**
646
     * {@inheritDoc}
647 648 649
     */
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
650
        $sql   = [];
651
        $table = $diff->getName($this)->getQuotedName($this);
652

653 654 655
        foreach ($diff->changedIndexes as $changedIndex) {
            $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $changedIndex));
        }
andig's avatar
andig committed
656

657 658
        foreach ($diff->removedIndexes as $remKey => $remIndex) {
            $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $remIndex));
659 660

            foreach ($diff->addedIndexes as $addKey => $addIndex) {
661
                if ($remIndex->getColumns() === $addIndex->getColumns()) {
662 663 664 665 666 667
                    $indexClause = 'INDEX ' . $addIndex->getName();

                    if ($addIndex->isPrimary()) {
                        $indexClause = 'PRIMARY KEY';
                    } elseif ($addIndex->isUnique()) {
                        $indexClause = 'UNIQUE INDEX ' . $addIndex->getName();
668 669
                    }

670
                    $query  = 'ALTER TABLE ' . $table . ' DROP INDEX ' . $remIndex->getName() . ', ';
671
                    $query .= 'ADD ' . $indexClause;
672
                    $query .= ' (' . $this->getIndexFieldDeclarationListSQL($addIndex) . ')';
673 674 675

                    $sql[] = $query;

676
                    unset($diff->removedIndexes[$remKey], $diff->addedIndexes[$addKey]);
677 678 679 680 681 682

                    break;
                }
            }
        }

683 684 685 686 687 688 689
        $engine = 'INNODB';

        if ($diff->fromTable instanceof Table && $diff->fromTable->hasOption('engine')) {
            $engine = strtoupper(trim($diff->fromTable->getOption('engine')));
        }

        // Suppress foreign key constraint propagation on non-supporting engines.
690
        if ($engine !== 'INNODB') {
691 692 693
            $diff->addedForeignKeys   = [];
            $diff->changedForeignKeys = [];
            $diff->removedForeignKeys = [];
694 695
        }

696 697
        $sql = array_merge(
            $sql,
698
            $this->getPreAlterTableAlterIndexForeignKeySQL($diff),
699 700 701 702 703 704 705
            parent::getPreAlterTableIndexForeignKeySQL($diff),
            $this->getPreAlterTableRenameIndexForeignKeySQL($diff)
        );

        return $sql;
    }

706 707 708 709 710
    /**
     * @return string[]
     */
    private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $index)
    {
711
        $sql = [];
712 713 714 715 716 717 718 719 720

        if (! $index->isPrimary() || ! $diff->fromTable instanceof Table) {
            return $sql;
        }

        $tableName = $diff->getName($this)->getQuotedName($this);

        // Dropping primary keys requires to unset autoincrement attribute on the particular column first.
        foreach ($index->getColumns() as $columnName) {
721 722 723
            if (! $diff->fromTable->hasColumn($columnName)) {
                continue;
            }
724

725 726
            $column = $diff->fromTable->getColumn($columnName);

727 728 729
            if ($column->getAutoincrement() !== true) {
                continue;
            }
730

731
            $column->setAutoincrement(false);
732

733 734 735 736 737
            $sql[] = 'ALTER TABLE ' . $tableName . ' MODIFY ' .
                $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());

            // original autoincrement information might be needed later on by other parts of the table alteration
            $column->setAutoincrement(true);
738 739 740 741 742
        }

        return $sql;
    }

743 744 745
    /**
     * @param TableDiff $diff The table diff to gather the SQL for.
     *
746
     * @return string[]
747 748 749
     */
    private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff)
    {
750
        $sql   = [];
751 752 753 754
        $table = $diff->getName($this)->getQuotedName($this);

        foreach ($diff->changedIndexes as $changedIndex) {
            // Changed primary key
755 756 757 758 759 760 761 762 763 764
            if (! $changedIndex->isPrimary() || ! ($diff->fromTable instanceof Table)) {
                continue;
            }

            foreach ($diff->fromTable->getPrimaryKeyColumns() as $columnName) {
                $column = $diff->fromTable->getColumn($columnName);

                // Check if an autoincrement column was dropped from the primary key.
                if (! $column->getAutoincrement() || in_array($columnName, $changedIndex->getColumns())) {
                    continue;
765
                }
766 767 768 769 770 771 772 773 774 775 776

                // The autoincrement attribute needs to be removed from the dropped column
                // before we can drop and recreate the primary key.
                $column->setAutoincrement(false);

                $sql[] = 'ALTER TABLE ' . $table . ' MODIFY ' .
                    $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray());

                // Restore the autoincrement attribute as it might be needed later on
                // by other parts of the table alteration.
                $column->setAutoincrement(true);
777 778 779 780 781 782
            }
        }

        return $sql;
    }

783 784 785
    /**
     * @param TableDiff $diff The table diff to gather the SQL for.
     *
786
     * @return string[]
787 788 789
     */
    protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
    {
790
        $sql       = [];
791 792 793
        $tableName = $diff->getName($this)->getQuotedName($this);

        foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) {
794 795
            if (in_array($foreignKey, $diff->changedForeignKeys, true)) {
                continue;
796
            }
797 798

            $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName);
799 800 801 802 803 804 805 806 807 808 809 810 811
        }

        return $sql;
    }

    /**
     * Returns the remaining foreign key constraints that require one of the renamed indexes.
     *
     * "Remaining" here refers to the diff between the foreign keys currently defined in the associated
     * table and the foreign keys to be removed.
     *
     * @param TableDiff $diff The table diff to evaluate.
     *
812
     * @return ForeignKeyConstraint[]
813 814 815 816
     */
    private function getRemainingForeignKeyConstraintsRequiringRenamedIndexes(TableDiff $diff)
    {
        if (empty($diff->renamedIndexes) || ! $diff->fromTable instanceof Table) {
817
            return [];
818 819
        }

820
        $foreignKeys = [];
821
        /** @var ForeignKeyConstraint[] $remainingForeignKeys */
822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853
        $remainingForeignKeys = array_diff_key(
            $diff->fromTable->getForeignKeys(),
            $diff->removedForeignKeys
        );

        foreach ($remainingForeignKeys as $foreignKey) {
            foreach ($diff->renamedIndexes as $index) {
                if ($foreignKey->intersectsIndexColumns($index)) {
                    $foreignKeys[] = $foreignKey;

                    break;
                }
            }
        }

        return $foreignKeys;
    }

    /**
     * {@inheritdoc}
     */
    protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
        return array_merge(
            parent::getPostAlterTableIndexForeignKeySQL($diff),
            $this->getPostAlterTableRenameIndexForeignKeySQL($diff)
        );
    }

    /**
     * @param TableDiff $diff The table diff to gather the SQL for.
     *
854
     * @return string[]
855 856 857
     */
    protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
    {
Sergei Morozov's avatar
Sergei Morozov committed
858 859 860 861 862 863 864 865
        $sql     = [];
        $newName = $diff->getNewName();

        if ($newName !== false) {
            $tableName = $newName->getQuotedName($this);
        } else {
            $tableName = $diff->getName($this)->getQuotedName($this);
        }
866 867

        foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) {
868 869
            if (in_array($foreignKey, $diff->changedForeignKeys, true)) {
                continue;
870
            }
871 872

            $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName);
873
        }
874 875 876 877

        return $sql;
    }

878
    /**
879
     * {@inheritDoc}
880 881 882 883 884 885
     */
    protected function getCreateIndexSQLFlags(Index $index)
    {
        $type = '';
        if ($index->isUnique()) {
            $type .= 'UNIQUE ';
Steve Müller's avatar
Steve Müller committed
886
        } elseif ($index->hasFlag('fulltext')) {
887
            $type .= 'FULLTEXT ';
888 889
        } elseif ($index->hasFlag('spatial')) {
            $type .= 'SPATIAL ';
890 891 892 893 894
        }

        return $type;
    }

895
    /**
896
     * {@inheritDoc}
897
     */
898
    public function getIntegerTypeDeclarationSQL(array $field)
899
    {
900
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
901 902
    }

903 904 905
    /**
     * {@inheritDoc}
     */
906
    public function getBigIntTypeDeclarationSQL(array $field)
907
    {
908
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
909 910
    }

911 912 913
    /**
     * {@inheritDoc}
     */
914
    public function getSmallIntTypeDeclarationSQL(array $field)
915
    {
916
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
917 918
    }

919 920 921 922 923
    /**
     * {@inheritdoc}
     */
    public function getFloatDeclarationSQL(array $field)
    {
924
        return 'DOUBLE PRECISION' . $this->getUnsignedDeclaration($field);
925 926
    }

927 928 929 930 931
    /**
     * {@inheritdoc}
     */
    public function getDecimalTypeDeclarationSQL(array $columnDef)
    {
Steve Müller's avatar
Steve Müller committed
932
        return parent::getDecimalTypeDeclarationSQL($columnDef) . $this->getUnsignedDeclaration($columnDef);
933 934 935 936 937
    }

    /**
     * Get unsigned declaration for a column.
     *
938
     * @param mixed[] $columnDef
939 940 941 942 943
     *
     * @return string
     */
    private function getUnsignedDeclaration(array $columnDef)
    {
944
        return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : '';
945 946
    }

947 948 949
    /**
     * {@inheritDoc}
     */
950
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
951
    {
952
        $autoinc = '';
953
        if (! empty($columnDef['autoincrement'])) {
954 955 956
            $autoinc = ' AUTO_INCREMENT';
        }

957
        return $this->getUnsignedDeclaration($columnDef) . $autoinc;
958
    }
959

960 961 962 963 964 965 966 967
    /**
     * {@inheritDoc}
     */
    public function getColumnCharsetDeclarationSQL($charset)
    {
        return 'CHARACTER SET ' . $charset;
    }

968 969 970 971 972 973 974 975
    /**
     * {@inheritDoc}
     */
    public function getColumnCollationDeclarationSQL($collation)
    {
        return 'COLLATE ' . $this->quoteSingleIdentifier($collation);
    }

976
    /**
977
     * {@inheritDoc}
978
     */
979
    public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey)
980 981
    {
        $query = '';
982 983
        if ($foreignKey->hasOption('match')) {
            $query .= ' MATCH ' . $foreignKey->getOption('match');
984
        }
985
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
986

987 988
        return $query;
    }
989

990
    /**
991
     * {@inheritDoc}
992
     */
993
    public function getDropIndexSQL($index, $table = null)
994
    {
995
        if ($index instanceof Index) {
996
            $indexName = $index->getQuotedName($this);
Steve Müller's avatar
Steve Müller committed
997
        } elseif (is_string($index)) {
998 999
            $indexName = $index;
        } else {
1000
            throw new InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
1001
        }
1002

1003
        if ($table instanceof Table) {
1004
            $table = $table->getQuotedName($this);
1005 1006
        } elseif (! is_string($table)) {
            throw new InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.');
1007
        }
1008

1009
        if ($index instanceof Index && $index->isPrimary()) {
1010
            // mysql primary keys are always named "PRIMARY",
1011 1012 1013
            // so we cannot use them in statements because of them being keyword.
            return $this->getDropPrimaryKeySQL($table);
        }
1014

1015 1016
        return 'DROP INDEX ' . $indexName . ' ON ' . $table;
    }
1017

1018
    /**
1019 1020 1021
     * @param string $table
     *
     * @return string
1022 1023 1024 1025
     */
    protected function getDropPrimaryKeySQL($table)
    {
        return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY';
1026
    }
1027

1028 1029 1030
    /**
     * {@inheritDoc}
     */
1031
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
1032
    {
1033
        return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
1034
    }
1035 1036

    /**
1037
     * {@inheritDoc}
1038 1039 1040 1041 1042
     */
    public function getName()
    {
        return 'mysql';
    }
1043

1044 1045 1046
    /**
     * {@inheritDoc}
     */
1047 1048 1049 1050
    public function getReadLockSQL()
    {
        return 'LOCK IN SHARE MODE';
    }
1051

1052 1053 1054
    /**
     * {@inheritDoc}
     */
1055 1056
    protected function initializeDoctrineTypeMappings()
    {
1057
        $this->doctrineTypeMapping = [
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074
            'tinyint'       => 'boolean',
            'smallint'      => 'smallint',
            'mediumint'     => 'integer',
            'int'           => 'integer',
            'integer'       => 'integer',
            'bigint'        => 'bigint',
            'tinytext'      => 'text',
            'mediumtext'    => 'text',
            'longtext'      => 'text',
            'text'          => 'text',
            'varchar'       => 'string',
            'string'        => 'string',
            'char'          => 'string',
            'date'          => 'date',
            'datetime'      => 'datetime',
            'timestamp'     => 'datetime',
            'time'          => 'time',
1075 1076 1077
            'float'         => 'float',
            'double'        => 'float',
            'real'          => 'float',
1078 1079 1080
            'decimal'       => 'decimal',
            'numeric'       => 'decimal',
            'year'          => 'date',
1081 1082 1083 1084
            'longblob'      => 'blob',
            'blob'          => 'blob',
            'mediumblob'    => 'blob',
            'tinyblob'      => 'blob',
Steve Müller's avatar
Steve Müller committed
1085 1086
            'binary'        => 'binary',
            'varbinary'     => 'binary',
1087
            'set'           => 'simple_array',
1088
        ];
1089
    }
1090

1091 1092 1093
    /**
     * {@inheritDoc}
     */
1094 1095 1096 1097
    public function getVarcharMaxLength()
    {
        return 65535;
    }
1098

Steve Müller's avatar
Steve Müller committed
1099 1100 1101 1102 1103 1104 1105 1106
    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 65535;
    }

1107 1108 1109
    /**
     * {@inheritDoc}
     */
1110 1111
    protected function getReservedKeywordsClass()
    {
1112
        return Keywords\MySQLKeywords::class;
1113
    }
1114 1115

    /**
1116
     * {@inheritDoc}
1117 1118 1119 1120 1121 1122
     *
     * MySQL commits a transaction implicitly when DROP TABLE is executed, however not
     * if DROP TEMPORARY TABLE is executed.
     */
    public function getDropTemporaryTableSQL($table)
    {
1123
        if ($table instanceof Table) {
1124
            $table = $table->getQuotedName($this);
1125 1126
        } elseif (! is_string($table)) {
            throw new InvalidArgumentException('getDropTemporaryTableSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.');
1127 1128 1129 1130
        }

        return 'DROP TEMPORARY TABLE ' . $table;
    }
1131 1132

    /**
1133 1134 1135 1136 1137 1138
     * Gets the SQL Snippet used to declare a BLOB column type.
     *     TINYBLOB   : 2 ^  8 - 1 = 255
     *     BLOB       : 2 ^ 16 - 1 = 65535
     *     MEDIUMBLOB : 2 ^ 24 - 1 = 16777215
     *     LONGBLOB   : 2 ^ 32 - 1 = 4294967295
     *
1139
     * {@inheritDoc}
1140 1141 1142
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
1143
        if (! empty($field['length']) && is_numeric($field['length'])) {
1144 1145
            $length = $field['length'];

1146
            if ($length <= static::LENGTH_LIMIT_TINYBLOB) {
1147 1148 1149
                return 'TINYBLOB';
            }

1150
            if ($length <= static::LENGTH_LIMIT_BLOB) {
1151 1152 1153
                return 'BLOB';
            }

1154
            if ($length <= static::LENGTH_LIMIT_MEDIUMBLOB) {
1155 1156 1157 1158
                return 'MEDIUMBLOB';
            }
        }

1159 1160
        return 'LONGBLOB';
    }
1161 1162 1163 1164 1165 1166 1167 1168 1169 1170

    /**
     * {@inheritdoc}
     */
    public function quoteStringLiteral($str)
    {
        $str = str_replace('\\', '\\\\', $str); // MySQL requires backslashes to be escaped aswell.

        return parent::quoteStringLiteral($str);
    }
1171 1172 1173 1174 1175 1176

    /**
     * {@inheritdoc}
     */
    public function getDefaultTransactionIsolationLevel()
    {
1177
        return TransactionIsolationLevel::REPEATABLE_READ;
1178
    }
1179 1180 1181 1182 1183 1184 1185 1186

    /**
     * {@inheritdoc}
     */
    public function supportsColumnLengthIndexes() : bool
    {
        return true;
    }
1187
}