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

20
namespace Doctrine\DBAL\Platforms;
21

22
use Doctrine\DBAL\Schema\Identifier;
Benjamin Morel's avatar
Benjamin Morel committed
23 24
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
25
use Doctrine\DBAL\Schema\TableDiff;
26
use Doctrine\DBAL\TransactionIsolationLevel;
27 28
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\TextType;
29

30 31
/**
 * The MySqlPlatform provides the behavior, features and SQL dialect of the
32 33
 * MySQL database platform. This platform represents a MySQL 5.0 or greater platform that
 * uses the InnoDB storage engine.
34
 *
Benjamin Morel's avatar
Benjamin Morel committed
35
 * @since  2.0
36
 * @author Roman Borschel <roman@code-factory.org>
37
 * @author Benjamin Eberlei <kontakt@beberlei.de>
Benjamin Morel's avatar
Benjamin Morel committed
38
 * @todo   Rename: MySQLPlatform
39
 */
40
class MySqlPlatform extends AbstractPlatform
41
{
42 43 44 45 46 47 48 49
    const LENGTH_LIMIT_TINYTEXT   = 255;
    const LENGTH_LIMIT_TEXT       = 65535;
    const LENGTH_LIMIT_MEDIUMTEXT = 16777215;

    const LENGTH_LIMIT_TINYBLOB   = 255;
    const LENGTH_LIMIT_BLOB       = 65535;
    const LENGTH_LIMIT_MEDIUMBLOB = 16777215;

50 51 52
    /**
     * Adds MySQL-specific LIMIT clause to the query
     * 18446744073709551615 is 2^64-1 maximum of unsigned BIGINT the biggest limit possible
53
     *
54 55 56
     * @param string $query
     * @param int    $limit
     * @param int    $offset
57 58
     *
     * @return string
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
     */
    protected function doModifyLimitQuery($query, $limit, $offset)
    {
        if ($limit !== null) {
            $query .= ' LIMIT ' . $limit;
            if ($offset !== null) {
                $query .= ' OFFSET ' . $offset;
            }
        } elseif ($offset !== null) {
            $query .= ' LIMIT 18446744073709551615 OFFSET ' . $offset;
        }

        return $query;
    }

romanb's avatar
romanb committed
74
    /**
75
     * {@inheritDoc}
romanb's avatar
romanb committed
76 77 78 79
     */
    public function getIdentifierQuoteCharacter()
    {
        return '`';
80
    }
81

82
    /**
83
     * {@inheritDoc}
84 85 86 87 88 89 90
     */
    public function getRegexpExpression()
    {
        return 'RLIKE';
    }

    /**
91
     * {@inheritDoc}
92 93 94 95 96 97
     */
    public function getGuidExpression()
    {
        return 'UUID()';
    }

98
    /**
99
     * {@inheritDoc}
100 101 102 103 104 105
     */
    public function getLocateExpression($str, $substr, $startPos = false)
    {
        if ($startPos == false) {
            return 'LOCATE(' . $substr . ', ' . $str . ')';
        }
106 107

        return 'LOCATE(' . $substr . ', ' . $str . ', '.$startPos.')';
108 109
    }

110
    /**
111
     * {@inheritDoc}
112 113 114 115
     */
    public function getConcatExpression()
    {
        $args = func_get_args();
116

117 118
        return 'CONCAT(' . join(', ', (array) $args) . ')';
    }
119

120
    /**
121
     * {@inheritdoc}
122
     */
123
    protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
124
    {
125
        $function = '+' === $operator ? 'DATE_ADD' : 'DATE_SUB';
126

127
        return $function . '(' . $date . ', INTERVAL ' . $interval . ' ' . $unit . ')';
128 129
    }

130 131 132
    /**
     * {@inheritDoc}
     */
133
    public function getDateDiffExpression($date1, $date2)
134
    {
135
        return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')';
136
    }
137

Benjamin Morel's avatar
Benjamin Morel committed
138 139 140
    /**
     * {@inheritDoc}
     */
141
    public function getListDatabasesSQL()
142 143 144 145
    {
        return 'SHOW DATABASES';
    }

Benjamin Morel's avatar
Benjamin Morel committed
146 147 148
    /**
     * {@inheritDoc}
     */
149
    public function getListTableConstraintsSQL($table)
150
    {
151
        return 'SHOW INDEX FROM ' . $table;
152 153
    }

154
    /**
155 156
     * {@inheritDoc}
     *
157
     * Two approaches to listing the table indexes. The information_schema is
Pascal Borreli's avatar
Pascal Borreli committed
158
     * preferred, because it doesn't cause problems with SQL keywords such as "order" or "table".
159 160
     */
    public function getListTableIndexesSQL($table, $currentDatabase = null)
161
    {
162
        if ($currentDatabase) {
163 164 165
            $currentDatabase = $this->quoteStringLiteral($currentDatabase);
            $table = $this->quoteStringLiteral($table);

166 167 168
            return "SELECT TABLE_NAME AS `Table`, NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, ".
                   "SEQ_IN_INDEX AS Seq_in_index, COLUMN_NAME AS Column_Name, COLLATION AS Collation, ".
                   "CARDINALITY AS Cardinality, SUB_PART AS Sub_Part, PACKED AS Packed, " .
169
                   "NULLABLE AS `Null`, INDEX_TYPE AS Index_Type, COMMENT AS Comment " .
170
                   "FROM information_schema.STATISTICS WHERE TABLE_NAME = " . $table . " AND TABLE_SCHEMA = " . $currentDatabase;
171
        }
172 173

        return 'SHOW INDEX FROM ' . $table;
174 175
    }

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

        return "SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = " . $database;
184 185
    }

Benjamin Morel's avatar
Benjamin Morel committed
186 187 188
    /**
     * {@inheritDoc}
     */
189
    public function getListTableForeignKeysSQL($table, $database = null)
190
    {
191 192 193 194 195 196
        $table = $this->quoteStringLiteral($table);

        if (null !== $database) {
            $database = $this->quoteStringLiteral($database);
        }

197 198 199
        $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 ".
200
               "INNER JOIN information_schema.referential_constraints c ON ".
201
               "  c.constraint_name = k.constraint_name AND ".
202
               "  c.table_name = $table */ WHERE k.table_name = $table";
203

Steve Müller's avatar
Steve Müller committed
204
        $databaseNameSql = null === $database ? 'DATABASE()' : $database;
205

206
        $sql .= " AND k.table_schema = $databaseNameSql /*!50116 AND c.constraint_schema = $databaseNameSql */";
207
        $sql .= " AND k.`REFERENCED_COLUMN_NAME` is not NULL";
208 209 210 211

        return $sql;
    }

Benjamin Morel's avatar
Benjamin Morel committed
212 213 214
    /**
     * {@inheritDoc}
     */
215
    public function getCreateViewSQL($name, $sql)
216 217 218 219
    {
        return 'CREATE VIEW ' . $name . ' AS ' . $sql;
    }

Benjamin Morel's avatar
Benjamin Morel committed
220 221 222
    /**
     * {@inheritDoc}
     */
223
    public function getDropViewSQL($name)
224 225 226 227
    {
        return 'DROP VIEW '. $name;
    }

228
    /**
229
     * {@inheritDoc}
230
     */
231
    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
232 233
    {
        return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)')
234
                : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)');
235
    }
236

Steve Müller's avatar
Steve Müller committed
237 238 239 240 241 242 243 244
    /**
     * {@inheritdoc}
     */
    protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
    {
        return $fixed ? 'BINARY(' . ($length ?: 255) . ')' : 'VARBINARY(' . ($length ?: 255) . ')';
    }

245
    /**
246 247 248 249 250 251 252 253 254
     * 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
     *
     * @param array $field
     *
     * @return string
255
     */
256
    public function getClobTypeDeclarationSQL(array $field)
257
    {
258
        if ( ! empty($field['length']) && is_numeric($field['length'])) {
259
            $length = $field['length'];
260

261
            if ($length <= static::LENGTH_LIMIT_TINYTEXT) {
262
                return 'TINYTEXT';
263 264
            }

265
            if ($length <= static::LENGTH_LIMIT_TEXT) {
266
                return 'TEXT';
267 268
            }

269
            if ($length <= static::LENGTH_LIMIT_MEDIUMTEXT) {
270 271 272
                return 'MEDIUMTEXT';
            }
        }
273

274 275
        return 'LONGTEXT';
    }
276

277
    /**
278
     * {@inheritDoc}
279
     */
280
    public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
281
    {
282
        if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] == true) {
283 284
            return 'TIMESTAMP';
        }
285 286

        return 'DATETIME';
287
    }
288

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

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

305
    /**
306
     * {@inheritDoc}
307
     */
308
    public function getBooleanTypeDeclarationSQL(array $field)
309 310 311 312
    {
        return 'TINYINT(1)';
    }

313 314 315 316
    /**
     * Obtain DBMS specific SQL code portion needed to set the COLLATION
     * of a field declaration to be used in statements like CREATE TABLE.
     *
317 318
     * @deprecated Deprecated since version 2.5, Use {@link self::getColumnCollationDeclarationSQL()} instead.
     *
319
     * @param string $collation name of the collation
320
     *
321 322 323 324 325
     * @return string  DBMS specific SQL code portion needed to set the COLLATION
     *                 of a field declaration.
     */
    public function getCollationFieldDeclaration($collation)
    {
326
        return $this->getColumnCollationDeclarationSQL($collation);
327
    }
328

329
    /**
330 331
     * {@inheritDoc}
     *
332 333 334 335 336 337 338
     * MySql prefers "autoincrement" identity columns since sequences can only
     * be emulated with a table.
     */
    public function prefersIdentityColumns()
    {
        return true;
    }
339

romanb's avatar
romanb committed
340
    /**
341
     * {@inheritDoc}
romanb's avatar
romanb committed
342
     *
343
     * MySql supports this through AUTO_INCREMENT columns.
romanb's avatar
romanb committed
344 345 346 347
     */
    public function supportsIdentityColumns()
    {
        return true;
348 349
    }

350 351 352
    /**
     * {@inheritDoc}
     */
353 354 355
    public function supportsInlineColumnComments()
    {
        return true;
romanb's avatar
romanb committed
356
    }
357

Benjamin Morel's avatar
Benjamin Morel committed
358 359 360
    /**
     * {@inheritDoc}
     */
361 362 363 364 365
    public function supportsColumnCollation()
    {
        return true;
    }

366 367 368
    /**
     * {@inheritDoc}
     */
369
    public function getListTablesSQL()
370
    {
371
        return "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'";
372
    }
373

Benjamin Morel's avatar
Benjamin Morel committed
374 375 376
    /**
     * {@inheritDoc}
     */
377
    public function getListTableColumnsSQL($table, $database = null)
378
    {
379 380
        $table = $this->quoteStringLiteral($table);

381
        if ($database) {
382
            $database = $this->quoteStringLiteral($database);
383 384
        } else {
            $database = 'DATABASE()';
385
        }
386

387 388 389
        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 ".
390
               "FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = " . $database . " AND TABLE_NAME = " . $table;
391 392
    }

393
    /**
394
     * {@inheritDoc}
395
     */
396
    public function getCreateDatabaseSQL($name)
397
    {
398
        return 'CREATE DATABASE ' . $name;
399
    }
400

401
    /**
402
     * {@inheritDoc}
403
     */
404
    public function getDropDatabaseSQL($name)
405
    {
406
        return 'DROP DATABASE ' . $name;
407
    }
408

409
    /**
410
     * {@inheritDoc}
411
     */
412
    protected function _getCreateTableSQL($tableName, array $columns, array $options = [])
413
    {
414
        $queryFields = $this->getColumnDeclarationListSQL($columns);
415

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

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

        // attach all primary keys
        if (isset($options['primary']) && ! empty($options['primary'])) {
431
            $keyColumns = array_unique(array_values($options['primary']));
432 433 434 435
            $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')';
        }

        $query = 'CREATE ';
436

437 438 439
        if (!empty($options['temporary'])) {
            $query .= 'TEMPORARY ';
        }
440

441
        $query .= 'TABLE ' . $tableName . ' (' . $queryFields . ') ';
442 443
        $query .= $this->buildTableOptions($options);
        $query .= $this->buildPartitionOptions($options);
444

445 446
        $sql[]  = $query;
        $engine = 'INNODB';
447

448 449 450 451 452 453
        if (isset($options['engine'])) {
            $engine = strtoupper(trim($options['engine']));
        }

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

459 460 461
        return $sql;
    }

462 463 464 465 466 467 468 469 470 471 472 473 474
    /**
     * {@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);
    }

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

488
        $tableOptions = [];
489 490

        // Charset
491 492
        if ( ! isset($options['charset'])) {
            $options['charset'] = 'utf8';
493 494
        }

495 496 497
        $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']);

        // Collate
498
        if ( ! isset($options['collate'])) {
499
            $options['collate'] = $options['charset'] . '_unicode_ci';
500
        }
501

502
        $tableOptions[] = sprintf('COLLATE %s', $options['collate']);
503

504
        // Engine
505 506
        if ( ! isset($options['engine'])) {
            $options['engine'] = 'InnoDB';
507
        }
508

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

511 512 513
        // Auto increment
        if (isset($options['auto_increment'])) {
            $tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']);
514
        }
515

516 517 518 519
        // Comment
        if (isset($options['comment'])) {
            $comment = trim($options['comment'], " '");

520
            $tableOptions[] = sprintf("COMMENT = %s ", $this->quoteStringLiteral($comment));
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540
        }

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

        return implode(' ', $tableOptions);
    }

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

545
    /**
546
     * {@inheritDoc}
547
     */
548
    public function getAlterTableSQL(TableDiff $diff)
549
    {
550 551
        $columnSql = [];
        $queryParts = [];
552
        if ($diff->newName !== false) {
553
            $queryParts[] = 'RENAME TO ' . $diff->getNewName()->getQuotedName($this);
554 555
        }

556
        foreach ($diff->addedColumns as $column) {
557 558
            if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
                continue;
559 560
            }

561 562 563
            $columnArray = $column->toArray();
            $columnArray['comment'] = $this->getColumnComment($column);
            $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray);
564 565
        }

566
        foreach ($diff->removedColumns as $column) {
567 568
            if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
                continue;
569 570
            }

571
            $queryParts[] =  'DROP ' . $column->getQuotedName($this);
572 573
        }

574
        foreach ($diff->changedColumns as $columnDiff) {
575 576
            if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
                continue;
577 578
            }

579
            /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */
580
            $column = $columnDiff->column;
581
            $columnArray = $column->toArray();
582 583 584 585 586 587 588 589 590

            // 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;
            }

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

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

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

608 609 610 611 612 613
        if (isset($diff->addedIndexes['primary'])) {
            $keyColumns = array_unique(array_values($diff->addedIndexes['primary']->getColumns()));
            $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')';
            unset($diff->addedIndexes['primary']);
        }

614 615
        $sql = [];
        $tableSql = [];
616

617
        if ( ! $this->onSchemaAlterTable($diff, $tableSql)) {
618
            if (count($queryParts) > 0) {
619
                $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . implode(", ", $queryParts);
620 621 622 623 624 625
            }
            $sql = array_merge(
                $this->getPreAlterTableIndexForeignKeySQL($diff),
                $sql,
                $this->getPostAlterTableIndexForeignKeySQL($diff)
            );
626
        }
627 628

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

631
    /**
632
     * {@inheritDoc}
633 634 635
     */
    protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff)
    {
636
        $sql = [];
637
        $table = $diff->getName($this)->getQuotedName($this);
638

639 640 641
        foreach ($diff->changedIndexes as $changedIndex) {
            $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $changedIndex));
        }
andig's avatar
andig committed
642

643 644
        foreach ($diff->removedIndexes as $remKey => $remIndex) {
            $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $remIndex));
645 646 647 648

            foreach ($diff->addedIndexes as $addKey => $addIndex) {
                if ($remIndex->getColumns() == $addIndex->getColumns()) {

649 650 651 652 653 654
                    $indexClause = 'INDEX ' . $addIndex->getName();

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

                    $query = 'ALTER TABLE ' . $table . ' DROP INDEX ' . $remIndex->getName() . ', ';
658
                    $query .= 'ADD ' . $indexClause;
659
                    $query .= ' (' . $this->getIndexFieldDeclarationListSQL($addIndex->getQuotedColumns($this)) . ')';
660 661 662

                    $sql[] = $query;

663
                    unset($diff->removedIndexes[$remKey], $diff->addedIndexes[$addKey]);
664 665 666 667 668 669

                    break;
                }
            }
        }

670 671 672 673 674 675 676 677
        $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.
        if ('INNODB' !== $engine) {
678 679 680
            $diff->addedForeignKeys   = [];
            $diff->changedForeignKeys = [];
            $diff->removedForeignKeys = [];
681 682
        }

683 684
        $sql = array_merge(
            $sql,
685
            $this->getPreAlterTableAlterIndexForeignKeySQL($diff),
686 687 688 689 690 691 692
            parent::getPreAlterTableIndexForeignKeySQL($diff),
            $this->getPreAlterTableRenameIndexForeignKeySQL($diff)
        );

        return $sql;
    }

693 694 695 696 697 698 699 700
    /**
     * @param TableDiff $diff
     * @param Index     $index
     *
     * @return string[]
     */
    private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $index)
    {
701
        $sql = [];
702 703 704 705 706 707 708 709 710

        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) {
711 712 713
            if (! $diff->fromTable->hasColumn($columnName)) {
                continue;
            }
714

715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
            $column = $diff->fromTable->getColumn($columnName);

            if ($column->getAutoincrement() === true) {
                $column->setAutoincrement(false);

                $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);
            }
        }

        return $sql;
    }

731 732 733 734 735 736 737
    /**
     * @param TableDiff $diff The table diff to gather the SQL for.
     *
     * @return array
     */
    private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff)
    {
738
        $sql = [];
739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
        $table = $diff->getName($this)->getQuotedName($this);

        foreach ($diff->changedIndexes as $changedIndex) {
            // Changed primary key
            if ($changedIndex->isPrimary() && $diff->fromTable instanceof Table) {
                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())) {
                        // 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);
                    }
                }
            }
        }

        return $sql;
    }

767 768 769 770 771 772 773
    /**
     * @param TableDiff $diff The table diff to gather the SQL for.
     *
     * @return array
     */
    protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
    {
774
        $sql = [];
775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798
        $tableName = $diff->getName($this)->getQuotedName($this);

        foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) {
            if (! in_array($foreignKey, $diff->changedForeignKeys, true)) {
                $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName);
            }
        }

        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.
     *
     * @return array
     */
    private function getRemainingForeignKeyConstraintsRequiringRenamedIndexes(TableDiff $diff)
    {
        if (empty($diff->renamedIndexes) || ! $diff->fromTable instanceof Table) {
799
            return [];
800 801
        }

802
        $foreignKeys = [];
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839
        /** @var \Doctrine\DBAL\Schema\ForeignKeyConstraint[] $remainingForeignKeys */
        $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.
     *
     * @return array
     */
    protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff)
    {
840
        $sql = [];
841 842 843 844 845 846 847 848 849
        $tableName = (false !== $diff->newName)
            ? $diff->getNewName()->getQuotedName($this)
            : $diff->getName($this)->getQuotedName($this);

        foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) {
            if (! in_array($foreignKey, $diff->changedForeignKeys, true)) {
                $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName);
            }
        }
850 851 852 853

        return $sql;
    }

854
    /**
855
     * {@inheritDoc}
856 857 858 859 860 861
     */
    protected function getCreateIndexSQLFlags(Index $index)
    {
        $type = '';
        if ($index->isUnique()) {
            $type .= 'UNIQUE ';
Steve Müller's avatar
Steve Müller committed
862
        } elseif ($index->hasFlag('fulltext')) {
863
            $type .= 'FULLTEXT ';
864 865
        } elseif ($index->hasFlag('spatial')) {
            $type .= 'SPATIAL ';
866 867 868 869 870
        }

        return $type;
    }

871
    /**
872
     * {@inheritDoc}
873
     */
874
    public function getIntegerTypeDeclarationSQL(array $field)
875
    {
876
        return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
877 878
    }

879 880 881
    /**
     * {@inheritDoc}
     */
882
    public function getBigIntTypeDeclarationSQL(array $field)
883
    {
884
        return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
885 886
    }

887 888 889
    /**
     * {@inheritDoc}
     */
890
    public function getSmallIntTypeDeclarationSQL(array $field)
891
    {
892
        return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field);
893 894
    }

895 896 897 898 899
    /**
     * {@inheritdoc}
     */
    public function getFloatDeclarationSQL(array $field)
    {
900
        return 'DOUBLE PRECISION' . $this->getUnsignedDeclaration($field);
901 902
    }

903 904 905 906 907
    /**
     * {@inheritdoc}
     */
    public function getDecimalTypeDeclarationSQL(array $columnDef)
    {
Steve Müller's avatar
Steve Müller committed
908
        return parent::getDecimalTypeDeclarationSQL($columnDef) . $this->getUnsignedDeclaration($columnDef);
909 910 911 912 913 914 915 916 917 918 919
    }

    /**
     * Get unsigned declaration for a column.
     *
     * @param array $columnDef
     *
     * @return string
     */
    private function getUnsignedDeclaration(array $columnDef)
    {
920
        return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : '';
921 922
    }

923 924 925
    /**
     * {@inheritDoc}
     */
926
    protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
927
    {
928
        $autoinc = '';
929
        if ( ! empty($columnDef['autoincrement'])) {
930 931 932
            $autoinc = ' AUTO_INCREMENT';
        }

933
        return $this->getUnsignedDeclaration($columnDef) . $autoinc;
934
    }
935

936
    /**
937
     * {@inheritDoc}
938
     */
939
    public function getAdvancedForeignKeyOptionsSQL(\Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey)
940 941
    {
        $query = '';
942 943
        if ($foreignKey->hasOption('match')) {
            $query .= ' MATCH ' . $foreignKey->getOption('match');
944
        }
945
        $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey);
946

947 948
        return $query;
    }
949

950
    /**
951
     * {@inheritDoc}
952
     */
953
    public function getDropIndexSQL($index, $table=null)
954
    {
955
        if ($index instanceof Index) {
956
            $indexName = $index->getQuotedName($this);
Steve Müller's avatar
Steve Müller committed
957
        } elseif (is_string($index)) {
958 959
            $indexName = $index;
        } else {
960
            throw new \InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.');
961
        }
962

963
        if ($table instanceof Table) {
964
            $table = $table->getQuotedName($this);
Steve Müller's avatar
Steve Müller committed
965
        } elseif (!is_string($table)) {
966
            throw new \InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.');
967
        }
968

969
        if ($index instanceof Index && $index->isPrimary()) {
970
            // mysql primary keys are always named "PRIMARY",
971 972 973
            // so we cannot use them in statements because of them being keyword.
            return $this->getDropPrimaryKeySQL($table);
        }
974

975 976
        return 'DROP INDEX ' . $indexName . ' ON ' . $table;
    }
977

978
    /**
979 980 981
     * @param string $table
     *
     * @return string
982 983 984 985
     */
    protected function getDropPrimaryKeySQL($table)
    {
        return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY';
986
    }
987

988 989 990
    /**
     * {@inheritDoc}
     */
991
    public function getSetTransactionIsolationSQL($level)
romanb's avatar
romanb committed
992
    {
993
        return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
romanb's avatar
romanb committed
994
    }
995 996

    /**
997
     * {@inheritDoc}
998 999 1000 1001 1002
     */
    public function getName()
    {
        return 'mysql';
    }
1003

1004 1005 1006
    /**
     * {@inheritDoc}
     */
1007 1008 1009 1010
    public function getReadLockSQL()
    {
        return 'LOCK IN SHARE MODE';
    }
1011

1012 1013 1014
    /**
     * {@inheritDoc}
     */
1015 1016
    protected function initializeDoctrineTypeMappings()
    {
1017
        $this->doctrineTypeMapping = [
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
            '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',
1035 1036 1037
            'float'         => 'float',
            'double'        => 'float',
            'real'          => 'float',
1038 1039 1040
            'decimal'       => 'decimal',
            'numeric'       => 'decimal',
            'year'          => 'date',
1041 1042 1043 1044
            'longblob'      => 'blob',
            'blob'          => 'blob',
            'mediumblob'    => 'blob',
            'tinyblob'      => 'blob',
Steve Müller's avatar
Steve Müller committed
1045 1046
            'binary'        => 'binary',
            'varbinary'     => 'binary',
1047
            'set'           => 'simple_array',
1048
        ];
1049
    }
1050

1051 1052 1053
    /**
     * {@inheritDoc}
     */
1054 1055 1056 1057
    public function getVarcharMaxLength()
    {
        return 65535;
    }
1058

Steve Müller's avatar
Steve Müller committed
1059 1060 1061 1062 1063 1064 1065 1066
    /**
     * {@inheritdoc}
     */
    public function getBinaryMaxLength()
    {
        return 65535;
    }

1067 1068 1069
    /**
     * {@inheritDoc}
     */
1070 1071
    protected function getReservedKeywordsClass()
    {
1072
        return Keywords\MySQLKeywords::class;
1073
    }
1074 1075

    /**
1076
     * {@inheritDoc}
1077 1078 1079 1080 1081 1082
     *
     * MySQL commits a transaction implicitly when DROP TABLE is executed, however not
     * if DROP TEMPORARY TABLE is executed.
     */
    public function getDropTemporaryTableSQL($table)
    {
1083
        if ($table instanceof Table) {
1084
            $table = $table->getQuotedName($this);
Steve Müller's avatar
Steve Müller committed
1085
        } elseif (!is_string($table)) {
Leo's avatar
Leo committed
1086
            throw new \InvalidArgumentException('getDropTemporaryTableSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.');
1087 1088 1089 1090
        }

        return 'DROP TEMPORARY TABLE ' . $table;
    }
1091 1092

    /**
1093 1094 1095 1096 1097 1098 1099 1100 1101
     * 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
     *
     * @param array $field
     *
     * @return string
1102 1103 1104
     */
    public function getBlobTypeDeclarationSQL(array $field)
    {
1105 1106 1107
        if ( ! empty($field['length']) && is_numeric($field['length'])) {
            $length = $field['length'];

1108
            if ($length <= static::LENGTH_LIMIT_TINYBLOB) {
1109 1110 1111
                return 'TINYBLOB';
            }

1112
            if ($length <= static::LENGTH_LIMIT_BLOB) {
1113 1114 1115
                return 'BLOB';
            }

1116
            if ($length <= static::LENGTH_LIMIT_MEDIUMBLOB) {
1117 1118 1119 1120
                return 'MEDIUMBLOB';
            }
        }

1121 1122
        return 'LONGBLOB';
    }
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132

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

        return parent::quoteStringLiteral($str);
    }
1133 1134 1135 1136 1137 1138

    /**
     * {@inheritdoc}
     */
    public function getDefaultTransactionIsolationLevel()
    {
1139
        return TransactionIsolationLevel::REPEATABLE_READ;
1140
    }
1141
}