Passed
Pull Request — master (#1928)
by Corey
03:52 queued 01:10
created

MysqlAdapter::hasForeignKey()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 21
c 0
b 0
f 0
ccs 15
cts 15
cp 1
rs 9.2222
cc 6
nc 10
nop 3
crap 6
1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Db\Adapter;
9
10
use Cake\Database\Connection;
11
use Cake\Database\Driver\Mysql as MysqlDriver;
12
use InvalidArgumentException;
13
use PDO;
14
use Phinx\Db\Table\Column;
15
use Phinx\Db\Table\ForeignKey;
16
use Phinx\Db\Table\Index;
17
use Phinx\Db\Table\Table;
18
use Phinx\Db\Util\AlterInstructions;
19
use Phinx\Util\Literal;
20
use RuntimeException;
21
22
/**
23
 * Phinx MySQL Adapter.
24
 *
25
 * @author Rob Morgan <[email protected]>
26
 */
27
class MysqlAdapter extends PdoAdapter
28
{
29
    /**
30
     * @var string[]
31
     */
32
    protected static $specificColumnTypes = [
33
        self::PHINX_TYPE_ENUM,
34
        self::PHINX_TYPE_SET,
35
        self::PHINX_TYPE_YEAR,
36
        self::PHINX_TYPE_JSON,
37
        self::PHINX_TYPE_BINARYUUID,
38
        self::PHINX_TYPE_TINYBLOB,
39
        self::PHINX_TYPE_MEDIUMBLOB,
40
        self::PHINX_TYPE_LONGBLOB,
41
    ];
42
43
    /**
44
     * @var bool[]
45
     */
46
    protected $signedColumnTypes = [
47
        self::PHINX_TYPE_INTEGER => true,
48
        self::PHINX_TYPE_TINY_INTEGER => true,
49
        self::PHINX_TYPE_SMALL_INTEGER => true,
50
        self::PHINX_TYPE_BIG_INTEGER => true,
51
        self::PHINX_TYPE_FLOAT => true,
52
        self::PHINX_TYPE_DECIMAL => true,
53
        self::PHINX_TYPE_DOUBLE => true,
54
        self::PHINX_TYPE_BOOLEAN => true,
55
    ];
56
57
    public const TEXT_TINY = 255;
58
    public const TEXT_SMALL = 255; /* deprecated, alias of TEXT_TINY */
59
    public const TEXT_REGULAR = 65535;
60
    public const TEXT_MEDIUM = 16777215;
61
    public const TEXT_LONG = 4294967295;
62
63
    // According to https://dev.mysql.com/doc/refman/5.0/en/blob.html BLOB sizes are the same as TEXT
64
    public const BLOB_TINY = 255;
65
    public const BLOB_SMALL = 255; /* deprecated, alias of BLOB_TINY */
66
    public const BLOB_REGULAR = 65535;
67
    public const BLOB_MEDIUM = 16777215;
68
    public const BLOB_LONG = 4294967295;
69
70 80
    public const INT_TINY = 255;
71
    public const INT_SMALL = 65535;
72 80
    public const INT_MEDIUM = 16777215;
73 80
    public const INT_REGULAR = 4294967295;
74
    public const INT_BIG = 18446744073709551615;
75
76
    public const BIT = 64;
77
78
    public const TYPE_YEAR = 'year';
79 80
80 80
    public const FIRST = 'FIRST';
81
82 80
    /**
83
     * {@inheritDoc}
84 80
     *
85
     * @throws \RuntimeException
86
     * @throws \InvalidArgumentException
87
     *
88
     * @return void
89 80
     */
90 80
    public function connect()
91 80
    {
92 80
        if ($this->connection === null) {
93
            if (!class_exists('PDO') || !in_array('mysql', PDO::getAvailableDrivers(), true)) {
94
                // @codeCoverageIgnoreStart
95 80
                throw new RuntimeException('You need to enable the PDO_Mysql extension for Phinx to run properly.');
96
                // @codeCoverageIgnoreEnd
97
            }
98 80
99
            $options = $this->getOptions();
100
101
            $dsn = 'mysql:';
102 80
103
            if (!empty($options['unix_socket'])) {
104
                // use socket connection
105
                $dsn .= 'unix_socket=' . $options['unix_socket'];
106 80
            } else {
107 80
                // use network connection
108
                $dsn .= 'host=' . $options['host'];
109
                if (!empty($options['port'])) {
110 80
                    $dsn .= ';port=' . $options['port'];
111
                }
112
            }
113 80
114 80
            $dsn .= ';dbname=' . $options['name'];
115 1
116 1
            // charset support
117 1
            if (!empty($options['charset'])) {
118 1
                $dsn .= ';charset=' . $options['charset'];
119
            }
120
121 80
            $driverOptions = [];
122 80
123 80
            // use custom data fetch mode
124
            if (!empty($options['fetch_mode'])) {
125
                $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
126
            }
127
128 81
            // support arbitrary \PDO::MYSQL_ATTR_* driver options and pass them to PDO
129
            // http://php.net/manual/en/ref.pdo-mysql.php#pdo-mysql.constants
130 81
            foreach ($options as $key => $option) {
131 81
                if (strpos($key, 'mysql_attr_') === 0) {
132
                    $pdoConstant = '\PDO::' . strtoupper($key);
133
                    if (!defined($pdoConstant)) {
134
                        throw new \UnexpectedValueException('Invalid PDO attribute: ' . $key . ' (' . $pdoConstant . ')');
135
                    }
136 6
                    $driverOptions[constant($pdoConstant)] = $option;
137
                }
138 6
            }
139
140
            $db = $this->createPdoConnection($dsn, $options['user'] ?? null, $options['pass'] ?? null, $driverOptions);
141
142
            $this->setConnection($db);
143
        }
144 6
    }
145
146 6
    /**
147 6
     * @inheritDoc
148
     */
149
    public function disconnect()
150
    {
151
        $this->connection = null;
152 6
    }
153
154 6
    /**
155 6
     * @inheritDoc
156
     */
157
    public function hasTransactions()
158
    {
159
        return true;
160 1
    }
161
162 1
    /**
163 1
     * @inheritDoc
164
     */
165
    public function beginTransaction()
166
    {
167
        $this->execute('START TRANSACTION');
168 112
    }
169
170 112
    /**
171
     * @inheritDoc
172
     */
173
    public function commitTransaction()
174
    {
175
        $this->execute('COMMIT');
176 112
    }
177
178 112
    /**
179
     * @inheritDoc
180
     */
181
    public function rollbackTransaction()
182
    {
183
        $this->execute('ROLLBACK');
184 82
    }
185
186 82
    /**
187
     * @inheritDoc
188 82
     */
189
    public function quoteTableName($tableName)
190
    {
191 82
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
192 82
    }
193
194 82
    /**
195
     * @inheritDoc
196 82
     */
197
    public function quoteColumnName($columnName)
198
    {
199
        return '`' . str_replace('`', '``', $columnName) . '`';
200
    }
201
202 82
    /**
203
     * @inheritDoc
204
     */
205
    public function hasTable($tableName)
206 82
    {
207
        if ($this->hasCreatedTable($tableName)) {
208 82
            return true;
209 82
        }
210
211
        if (strpos($tableName, '.') !== false) {
212 82
            [$schema, $table] = explode('.', $tableName);
213 82
            $exists = $this->hasTableWithSchema($schema, $table);
214 68
            // Only break here on success, because it is possible for table names to contain a dot.
215 68
            if ($exists) {
216 68
                return true;
217 68
            }
218 68
        }
219
220 68
        $options = $this->getOptions();
221 68
222 82
        return $this->hasTableWithSchema($options['name'], $tableName);
223
    }
224 2
225 2
    /**
226 2
     * @param string $schema The table schema
227 2
     * @param string $tableName The table name
228
     *
229 2
     * @return bool
230 2
     */
231 2
    protected function hasTableWithSchema($schema, $tableName)
232
    {
233
        $result = $this->fetchRow(sprintf(
234
            "SELECT TABLE_NAME
235
            FROM INFORMATION_SCHEMA.TABLES
236 82
            WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'",
237 82
            $schema,
238 82
            $tableName
239 82
        ));
240
241
        return !empty($result);
242 82
    }
243 82
244 82
    /**
245 82
     * @inheritDoc
246 82
     */
247
    public function createTable(Table $table, array $columns = [], array $indexes = [])
248
    {
249 82
        // This method is based on the MySQL docs here: http://dev.mysql.com/doc/refman/5.1/en/create-index.html
250 2
        $defaultOptions = [
251 2
            'engine' => 'InnoDB',
252
            'collation' => 'utf8_general_ci',
253 82
        ];
254 82
255 82
        $options = array_merge(
256 82
            $defaultOptions,
257 82
            array_intersect_key($this->getOptions(), $defaultOptions),
258
            $table->getOptions()
259
        );
260 82
261 82
        // Add the default primary key
262 82
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
263 82
            $options['id'] = 'id';
264 81
        }
265 82
266
        if (isset($options['id']) && is_string($options['id'])) {
267
            // Handle id => "field_name" to support AUTO_INCREMENT
268 2
            $column = new Column();
269 2
            $column->setName($options['id'])
270 2
                   ->setType('integer')
271 2
                   ->setSigned(isset($options['signed']) ? $options['signed'] : true)
272 2
                   ->setIdentity(true);
273 2
274 2
            array_unshift($columns, $column);
275 2
            if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) {
276 2
                throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key');
277 2
            }
278 82
            $options['primary_key'] = $options['id'];
279 82
        }
280 1
281
        // open: process table options like collation etc
282
283
        // process table engine (default to InnoDB)
284 82
        $optionsStr = 'ENGINE = InnoDB';
285 82
        if (isset($options['engine'])) {
286 10
            $optionsStr = sprintf('ENGINE = %s', $options['engine']);
287 82
        }
288
289
        // process table collation
290 82
        if (isset($options['collation'])) {
291 82
            $charset = explode('_', $options['collation']);
292 2
            $optionsStr .= sprintf(' CHARACTER SET %s', $charset[0]);
293 82
            $optionsStr .= sprintf(' COLLATE %s', $options['collation']);
294
        }
295 82
296 82
        // set the table comment
297
        if (isset($options['comment'])) {
298
            $optionsStr .= sprintf(' COMMENT=%s ', $this->getConnection()->quote($options['comment']));
299 82
        }
300 82
301
        // set the table row format
302
        if (isset($options['row_format'])) {
303
            $optionsStr .= sprintf(' ROW_FORMAT=%s ', $options['row_format']);
304
        }
305 5
306
        $sql = 'CREATE TABLE ';
307 5
        $sql .= $this->quoteTableName($table->getName()) . ' (';
308 5
        foreach ($columns as $column) {
309
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
310
        }
311
312
        // set the primary key(s)
313 5
        if (isset($options['primary_key'])) {
314
            $sql = rtrim($sql);
315 5
            $sql .= ' PRIMARY KEY (';
316 5
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
317
                $sql .= $this->quoteColumnName($options['primary_key']);
318
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
319
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
320
            }
321 1
            $sql .= ')';
322
        } else {
323 1
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
324 1
        }
325 1
326 1
        // set the indexes
327
        foreach ($indexes as $index) {
328 1
            $sql .= ', ' . $this->getIndexSqlDefinition($index);
329 1
        }
330
331
        $sql .= ') ' . $optionsStr;
332
        $sql = rtrim($sql);
333
334 12
        // execute the sql
335
        $this->execute($sql);
336 12
337 12
        $this->addCreatedTable($table->getName());
338 12
    }
339 12
340
    /**
341 12
     * {@inheritDoc}
342 12
     *
343 12
     * @throws \InvalidArgumentException
344 12
     */
345 12
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
346 12
    {
347
        $instructions = new AlterInstructions();
348 12
349 12
        // Drop the existing primary key
350 12
        $primaryKey = $this->getPrimaryKey($table->getName());
351
        if (!empty($primaryKey['columns'])) {
352 12
            $instructions->addAlter('DROP PRIMARY KEY');
353 3
        }
354 3
355
        // Add the primary key(s)
356 12
        if (!empty($newColumns)) {
357 12
            $sql = 'ADD PRIMARY KEY (';
358
            if (is_string($newColumns)) { // handle primary_key => 'id'
359 12
                $sql .= $this->quoteColumnName($newColumns);
360
            } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
361
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
362
            } else {
363
                throw new InvalidArgumentException(sprintf(
364
                    'Invalid value for primary key: %s',
365 79
                    json_encode($newColumns)
366
                ));
367 79
            }
368 79
            $sql .= ')';
369 79
            $instructions->addAlter($sql);
370 77
        }
371
372 77
        return $instructions;
373
    }
374 21
375
    /**
376
     * @inheritDoc
377
     */
378
    protected function getChangeCommentInstructions(Table $table, $newComment)
379
    {
380
        $instructions = new AlterInstructions();
381
382
        // passing 'null' is to remove table comment
383 95
        $newComment = ($newComment !== null)
384
            ? $newComment
385 95
            : '';
386 10
        $sql = sprintf(' COMMENT=%s ', $this->getConnection()->quote($newComment));
387 95
        $instructions->addAlter($sql);
388 79
389 79
        return $instructions;
390 95
    }
391
392
    /**
393
     * @inheritDoc
394
     */
395
    protected function getRenameTableInstructions($tableName, $newTableName)
396 18
    {
397
        $this->updateCreatedTableName($tableName, $newTableName);
398 18
        $sql = sprintf(
399 18
            'RENAME TABLE %s TO %s',
400 18
            $this->quoteTableName($tableName),
401 18
            $this->quoteTableName($newTableName)
402 18
        );
403 18
404
        return new AlterInstructions([], [$sql]);
405 18
    }
406 2
407 2
    /**
408
     * @inheritDoc
409 18
     */
410 18
    protected function getDropTableInstructions($tableName)
411
    {
412
        $this->removeCreatedTable($tableName);
413
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
414
415 7
        return new AlterInstructions([], [$sql]);
416
    }
417 7
418 7
    /**
419 7
     * @inheritDoc
420 5
     */
421 5
    public function truncateTable($tableName)
422 5
    {
423 1
        $sql = sprintf(
424 1
            'TRUNCATE TABLE %s',
425 5
            $this->quoteTableName($tableName)
426
        );
427 5
428 5
        $this->execute($sql);
429 5
    }
430 5
431 5
    /**
432 5
     * @inheritDoc
433
     */
434 5
    public function getColumns($tableName)
435 5
    {
436 5
        $columns = [];
437
        $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName)));
438 6
        foreach ($rows as $columnInfo) {
439
            $phinxType = $this->getPhinxType($columnInfo['Type']);
440 2
441
            $column = new Column();
442
            $column->setName($columnInfo['Field'])
443 2
                   ->setNull($columnInfo['Null'] !== 'NO')
444
                   ->setDefault($columnInfo['Default'])
445
                   ->setType($phinxType['name'])
446
                   ->setSigned(strpos($columnInfo['Type'], 'unsigned') === false)
447
                   ->setLimit($phinxType['limit'])
448
                   ->setScale($phinxType['scale']);
449 5
450
            if ($columnInfo['Extra'] === 'auto_increment') {
451 5
                $column->setIdentity(true);
452 5
            }
453 5
454 5
            if (isset($phinxType['values'])) {
455 5
                $column->setValues($phinxType['values']);
456 5
            }
457 5
458 5
            $columns[] = $column;
459
        }
460 5
461 5
        return $columns;
462 5
    }
463
464
    /**
465
     * @inheritDoc
466
     */
467 5
    public function hasColumn($tableName, $columnName)
468
    {
469 5
        $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName)));
470 5
        foreach ($rows as $column) {
471 5
            if (strcasecmp($column['Field'], $columnName) === 0) {
472 5
                return true;
473 5
            }
474 5
        }
475 5
476 5
        return false;
477
    }
478
479
    /**
480
     * @inheritDoc
481
     */
482
    protected function getAddColumnInstructions(Table $table, Column $column)
483
    {
484 19
        $alter = sprintf(
485
            'ADD %s %s',
486 19
            $this->quoteColumnName($column->getName()),
487 19
            $this->getColumnSqlDefinition($column)
488 19
        );
489 18
490 18
        $alter .= $this->afterClause($column);
491 18
492 18
        return new AlterInstructions([$alter]);
493 19
    }
494 19
495
    /**
496
     * Exposes the MySQL syntax to arrange a column `FIRST`.
497
     *
498
     * @param Column $column The column being altered.
499
     *
500 14
     * @return string The appropriate SQL fragment.
501
     */
502 14
    protected function afterClause(Column $column)
503 6
    {
504 6
        $after = $column->getAfter();
505
        if (empty($after)) {
506 14
            return '';
507 14
        }
508
509 14
        if ($after === self::FIRST) {
510 14
            return ' FIRST';
511 12
        }
512
513 13
        return ' AFTER ' . $this->quoteColumnName($after);
514
    }
515 11
516
    /**
517
     * {@inheritDoc}
518
     *
519
     * @throws \InvalidArgumentException
520
     */
521 1
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
522
    {
523 1
        $rows = $this->fetchAll(sprintf('SHOW FULL COLUMNS FROM %s', $this->quoteTableName($tableName)));
524
525 1
        foreach ($rows as $row) {
526 1
            if (strcasecmp($row['Field'], $columnName) === 0) {
527 1
                $null = ($row['Null'] === 'NO') ? 'NOT NULL' : 'NULL';
528
                $comment = isset($row['Comment']) ? ' COMMENT ' . '\'' . addslashes($row['Comment']) . '\'' : '';
529 1
                $extra = ' ' . strtoupper($row['Extra']);
530
                if (($row['Default'] !== null)) {
531
                    $extra .= $this->getDefaultValueDefinition($row['Default']);
532
                }
533
                $definition = $row['Type'] . ' ' . $null . $extra . $comment;
534
535
                $alter = sprintf(
536
                    'CHANGE COLUMN %s %s %s',
537 4
                    $this->quoteColumnName($columnName),
538
                    $this->quoteColumnName($newColumnName),
539 4
                    $definition
540 4
                );
541 4
542 4
                return new AlterInstructions([$alter]);
543 4
            }
544 4
        }
545 4
546 4
        throw new InvalidArgumentException(sprintf(
547
            "The specified column doesn't exist: " .
548
            $columnName
549
        ));
550
    }
551 3
552
    /**
553 3
     * @inheritDoc
554 2
     */
555 2
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
556
    {
557 3
        $alter = sprintf(
558 3
            'CHANGE %s %s %s%s',
559
            $this->quoteColumnName($columnName),
560 3
            $this->quoteColumnName($newColumn->getName()),
561 3
            $this->getColumnSqlDefinition($newColumn),
562 3
            $this->afterClause($newColumn)
563 3
        );
564 3
565 3
        return new AlterInstructions([$alter]);
566 3
    }
567 3
568 3
    /**
569 3
     * @inheritDoc
570
     */
571 3
    protected function getDropColumnInstructions($tableName, $columnName)
572 1
    {
573
        $alter = sprintf('DROP COLUMN %s', $this->quoteColumnName($columnName));
574
575
        return new AlterInstructions([$alter]);
576
    }
577 2
578
    /**
579 2
     * Get an array of indexes from a particular table.
580
     *
581 2
     * @param string $tableName Table name
582
     *
583 2
     * @return array
584 2
     */
585 2
    protected function getIndexes($tableName)
586 2
    {
587 2
        $indexes = [];
588 2
        $rows = $this->fetchAll(sprintf('SHOW INDEXES FROM %s', $this->quoteTableName($tableName)));
589 2
        foreach ($rows as $row) {
590 2
            if (!isset($indexes[$row['Key_name']])) {
591 2
                $indexes[$row['Key_name']] = ['columns' => []];
592
            }
593 2
            $indexes[$row['Key_name']]['columns'][] = strtolower($row['Column_name']);
594
        }
595
596
        return $indexes;
597
    }
598
599 21
    /**
600
     * @inheritDoc
601 21
     */
602 5
    public function hasIndex($tableName, $columns)
603 5
    {
604 21
        if (is_string($columns)) {
605 21
            $columns = [$columns]; // str to array
606 6
        }
607 4
608
        $columns = array_map('strtolower', $columns);
609 4
        $indexes = $this->getIndexes($tableName);
610
611 15
        foreach ($indexes as $index) {
612 12
            if ($columns == $index['columns']) {
613 10
                return true;
614
            }
615 11
        }
616 11
617
        return false;
618
    }
619
620
    /**
621
     * @inheritDoc
622
     */
623
    public function hasIndexByName($tableName, $indexName)
624
    {
625
        $indexes = $this->getIndexes($tableName);
626 22
627
        foreach ($indexes as $name => $index) {
628 22
            if ($name === $indexName) {
629 22
                return true;
630
            }
631
        }
632
633
        return false;
634
    }
635
636
    /**
637
     * @inheritDoc
638
     */
639
    protected function getAddIndexInstructions(Table $table, Index $index)
640 22
    {
641
        $instructions = new AlterInstructions();
642 22
643 22
        if ($index->getType() === Index::FULLTEXT) {
644 19
            // Must be executed separately
645 19
            // SQLSTATE[HY000]: General error: 1795 InnoDB presently supports one FULLTEXT index creation at a time
646 19
            $alter = sprintf(
647 19
                'ALTER TABLE %s ADD %s',
648 22
                $this->quoteTableName($table->getName()),
649 22
                $this->getIndexSqlDefinition($index)
650
            );
651
652
            $instructions->addPostStep($alter);
653
        } else {
654
            $alter = sprintf(
655 15
                'ADD %s',
656
                $this->getIndexSqlDefinition($index)
657 15
            );
658 15
659 15
            $instructions->addAlter($alter);
660 15
        }
661 15
662 15
        return $instructions;
663 15
    }
664 15
665
    /**
666
     * {@inheritDoc}
667
     *
668
     * @throws \InvalidArgumentException
669 8
     */
670
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
671 8
    {
672 3
        if (is_string($columns)) {
673 3
            $columns = [$columns]; // str to array
674
        }
675
676 8
        $indexes = $this->getIndexes($tableName);
677 8
        $columns = array_map('strtolower', $columns);
678 8
679 8
        foreach ($indexes as $indexName => $index) {
680 8
            if ($columns == $index['columns']) {
681
                return new AlterInstructions([sprintf(
682 8
                    'DROP INDEX %s',
683 8
                    $this->quoteColumnName($indexName)
684 8
                )]);
685
            }
686 7
        }
687 7
688
        throw new InvalidArgumentException(sprintf(
689
            "The specified index on columns '%s' does not exist",
690
            implode(',', $columns)
691
        ));
692
    }
693
694
    /**
695 7
     * {@inheritDoc}
696 7
     *
697
     * @throws \InvalidArgumentException
698 7
     */
699 7
    protected function getDropIndexByNameInstructions($tableName, $indexName)
700 7
    {
701 7
        $indexes = $this->getIndexes($tableName);
702 7
703
        foreach ($indexes as $name => $index) {
704 7
            if ($name === $indexName) {
705
                return new AlterInstructions([sprintf(
706
                    'DROP INDEX %s',
707
                    $this->quoteColumnName($indexName)
708
                )]);
709 96
            }
710
        }
711
712 96
        throw new InvalidArgumentException(sprintf(
713 87
            "The specified index name '%s' does not exist",
714
            $indexName
715 96
        ));
716 4
    }
717
718 96
    /**
719 9
     * @inheritDoc
720
     */
721
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
722 6
    {
723 6
        $primaryKey = $this->getPrimaryKey($tableName);
724 6
725 6
        if (empty($primaryKey['constraint'])) {
726 6
            return false;
727 6
        }
728 6
729 6
        if ($constraint) {
730
            return ($primaryKey['constraint'] === $constraint);
731 5
        } else {
732
            if (is_string($columns)) {
733 5
                $columns = [$columns]; // str to array
734
            }
735 95
            $missingColumns = array_diff($columns, $primaryKey['columns']);
736 5
737
            return empty($missingColumns);
738 95
        }
739 3
    }
740
741 95
    /**
742 1
     * Get the primary key from a particular table.
743
     *
744
     * @param string $tableName Table name
745 1
     *
746 1
     * @return array
747 1
     */
748 1
    public function getPrimaryKey($tableName)
749 1
    {
750 1
        $options = $this->getOptions();
751 1
        $rows = $this->fetchAll(sprintf(
752 1
            "SELECT
753
                k.CONSTRAINT_NAME,
754 1
                k.COLUMN_NAME
755
            FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS t
756 1
            JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
757
                USING(CONSTRAINT_NAME,TABLE_NAME)
758 95
            WHERE t.CONSTRAINT_TYPE='PRIMARY KEY'
759 82
                AND t.TABLE_SCHEMA='%s'
760
                AND t.TABLE_NAME='%s'",
761
            $options['name'],
762 6
            $tableName
763 6
        ));
764 6
765 6
        $primaryKey = [
766 6
            'columns' => [],
767 6
        ];
768
        foreach ($rows as $row) {
769 6
            $primaryKey['constraint'] = $row['CONSTRAINT_NAME'];
770 6
            $primaryKey['columns'][] = $row['COLUMN_NAME'];
771 6
        }
772 6
773 6
        return $primaryKey;
774 6
    }
775 6
776 2
    /**
777 2
     * @inheritDoc
778 6
     */
779
    public function hasForeignKey($tableName, $columns, $constraint = null)
780 5
    {
781 82
        if (is_string($columns)) {
782 76
            $columns = [$columns]; // str to array
783 76
        }
784 82
        $foreignKeys = $this->getForeignKeys($tableName);
785
        if ($constraint) {
786 86
            if (isset($foreignKeys[$constraint])) {
787 82
                return !empty($foreignKeys[$constraint]);
788
            }
789 86
790 7
            return false;
791
        }
792 84
793 5
        foreach ($foreignKeys as $key) {
794
            if ($columns == $key['columns']) {
795 83
                return true;
796 7
            }
797
        }
798 83
799 80
        return false;
800
    }
801 83
802 4
    /**
803
     * Get an array of foreign keys from a particular table.
804 83
     *
805 4
     * @param string $tableName Table name
806
     *
807 83
     * @return array
808 80
     */
809
    protected function getForeignKeys($tableName)
810 10
    {
811 2
        if (strpos($tableName, '.') !== false) {
812
            [$schema, $tableName] = explode('.', $tableName);
813 10
        }
814 10
815 10
        $foreignKeys = [];
816 10
        $rows = $this->fetchAll(sprintf(
817 5
            "SELECT
818 8
              CONSTRAINT_NAME,
819 5
              CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS TABLE_NAME,
820
              COLUMN_NAME,
821 6
              CONCAT(REFERENCED_TABLE_SCHEMA, '.', REFERENCED_TABLE_NAME) AS REFERENCED_TABLE_NAME,
822 4
              REFERENCED_COLUMN_NAME
823
            FROM information_schema.KEY_COLUMN_USAGE
824 2
            WHERE REFERENCED_TABLE_NAME IS NOT NULL
825
              AND TABLE_SCHEMA = %s
826
              AND TABLE_NAME = '%s'
827
            ORDER BY POSITION_IN_UNIQUE_CONSTRAINT",
828
            empty($schema) ? 'DATABASE()' : "'$schema'",
829
            $tableName
830 2
        ));
831
        foreach ($rows as $row) {
832
            $foreignKeys[$row['CONSTRAINT_NAME']]['table'] = $row['TABLE_NAME'];
833 2
            $foreignKeys[$row['CONSTRAINT_NAME']]['columns'][] = $row['COLUMN_NAME'];
834 2
            $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_table'] = $row['REFERENCED_TABLE_NAME'];
835 2
            $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME'];
836
        }
837
838
        return $foreignKeys;
839
    }
840
841
    /**
842
     * @inheritDoc
843
     */
844
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
845
    {
846 17
        $alter = sprintf(
847
            'ADD %s',
848 17
            $this->getForeignKeySqlDefinition($foreignKey)
849 17
        );
850 1
851
        return new AlterInstructions([$alter]);
852 16
    }
853 16
854 16
    /**
855 16
     * @inheritDoc
856 14
     */
857 14
    protected function getDropForeignKeyInstructions($tableName, $constraint)
858 16
    {
859 4
        $alter = sprintf(
860 4
            'DROP FOREIGN KEY %s',
861 16
            $constraint
862 3
        );
863 3
864 3
        return new AlterInstructions([$alter]);
865
    }
866 16
867 6
    /**
868 6
     * {@inheritDoc}
869 3
     *
870 3
     * @throws \InvalidArgumentException
871 6
     */
872 16
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
873 5
    {
874 5
        $instructions = new AlterInstructions();
875 1
876 1
        foreach ($columns as $column) {
877 5
            $rows = $this->fetchAll(sprintf(
878 2
                "SELECT
879 2
                    CONSTRAINT_NAME
880 5
                  FROM information_schema.KEY_COLUMN_USAGE
881 16
                  WHERE REFERENCED_TABLE_SCHEMA = DATABASE()
882 2
                    AND REFERENCED_TABLE_NAME IS NOT NULL
883 2
                    AND TABLE_NAME = '%s'
884 2
                    AND COLUMN_NAME = '%s'
885 16
                  ORDER BY POSITION_IN_UNIQUE_CONSTRAINT",
886 2
                $tableName,
887 2
                $column
888 2
            ));
889 16
890 2
            foreach ($rows as $row) {
891 2
                $instructions->merge($this->getDropForeignKeyInstructions($tableName, $row['CONSTRAINT_NAME']));
892 2
            }
893 16
        }
894 15
895 15
        if (empty($instructions->getAlterParts())) {
896 12
            throw new InvalidArgumentException(sprintf(
897 12
                "Not foreign key on columns '%s' exist",
898 15
                implode(',', $columns)
899 11
            ));
900 6
        }
901 4
902 4
        return $instructions;
903 6
    }
904 6
905 10
    /**
906 2
     * {@inheritDoc}
907 2
     *
908 10
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
909 1
     */
910 1
    public function getSqlType($type, $limit = null)
911 1
    {
912 10
        switch ($type) {
913 1
            case static::PHINX_TYPE_FLOAT:
914 1
            case static::PHINX_TYPE_DOUBLE:
915 1
            case static::PHINX_TYPE_DECIMAL:
916 10
            case static::PHINX_TYPE_DATE:
917 1
            case static::PHINX_TYPE_ENUM:
918 1
            case static::PHINX_TYPE_SET:
919 1
            case static::PHINX_TYPE_JSON:
920 10
            // Geospatial database types
921 2
            case static::PHINX_TYPE_GEOMETRY:
922 2
            case static::PHINX_TYPE_POINT:
923 2
            case static::PHINX_TYPE_LINESTRING:
924 9
            case static::PHINX_TYPE_POLYGON:
925 2
                return ['name' => $type];
926 2
            case static::PHINX_TYPE_DATETIME:
927 2
            case static::PHINX_TYPE_TIMESTAMP:
928 8
            case static::PHINX_TYPE_TIME:
929 2
                return ['name' => $type, 'limit' => $limit];
930 2
            case static::PHINX_TYPE_STRING:
931 2
                return ['name' => 'varchar', 'limit' => $limit ?: 255];
932
            case static::PHINX_TYPE_CHAR:
933
                return ['name' => 'char', 'limit' => $limit ?: 255];
934
            case static::PHINX_TYPE_TEXT:
935 16
                if ($limit) {
936
                    $sizes = [
937
                        // Order matters! Size must always be tested from longest to shortest!
938 15
                        'longtext' => static::TEXT_LONG,
939 15
                        'mediumtext' => static::TEXT_MEDIUM,
940
                        'text' => static::TEXT_REGULAR,
941 15
                        'tinytext' => static::TEXT_SMALL,
942
                    ];
943 15
                    foreach ($sizes as $name => $length) {
944 3
                        if ($limit >= $length) {
945 3
                            return ['name' => $name];
946
                        }
947 15
                    }
948
                }
949
950
                return ['name' => 'text'];
951
            case static::PHINX_TYPE_BINARY:
952
                if ($limit === null) {
953
                    $limit = 255;
954 83
                }
955
956 83
                if ($limit > 255) {
957
                    return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit);
958 83
                }
959 1
960 1
                return ['name' => 'binary', 'limit' => $limit];
961 82
            case static::PHINX_TYPE_BINARYUUID:
962
                return ['name' => 'binary', 'limit' => 16];
963 83
            case static::PHINX_TYPE_VARBINARY:
964
                if ($limit === null) {
965
                    $limit = 255;
966
                }
967
968 4
                if ($limit > 255) {
969
                    return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit);
970 4
                }
971 4
972 4
                return ['name' => 'varbinary', 'limit' => $limit];
973
            case static::PHINX_TYPE_BLOB:
974 4
                if ($limit !== null) {
975 4
                    // Rework this part as the choosen types were always UNDER the required length
976
                    $sizes = [
977 4
                        'tinyblob' => static::BLOB_SMALL,
978 3
                        'blob' => static::BLOB_REGULAR,
979 3
                        'mediumblob' => static::BLOB_MEDIUM,
980
                    ];
981 3
982
                    foreach ($sizes as $name => $length) {
983 3
                        if ($limit <= $length) {
984
                            return ['name' => $name];
985
                        }
986
                    }
987
988
                    // For more length requirement, the longblob is used
989 81
                    return ['name' => 'longblob'];
990
                }
991 81
992 81
                // If not limit is provided, fallback on blob
993
                return ['name' => 'blob'];
994
            case static::PHINX_TYPE_TINYBLOB:
995
                // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit
996
                return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_TINY);
997
            case static::PHINX_TYPE_MEDIUMBLOB:
998
                // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit
999
                return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_MEDIUM);
1000 89
            case static::PHINX_TYPE_LONGBLOB:
1001
                // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit
1002 89
                return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_LONG);
1003
            case static::PHINX_TYPE_BIT:
1004 89
                return ['name' => 'bit', 'limit' => $limit ?: 64];
1005 89
            case static::PHINX_TYPE_SMALL_INTEGER:
1006 89
                return ['name' => 'smallint', 'limit' => $limit ?: 6];
1007 2
            case static::PHINX_TYPE_TINY_INTEGER:
1008 89
                return ['name' => 'tinyint', 'limit' => $limit ?: 4];
1009 86
            case static::PHINX_TYPE_INTEGER:
1010 86
                if ($limit && $limit >= static::INT_TINY) {
1011 89
                    $sizes = [
1012 5
                        // Order matters! Size must always be tested from longest to shortest!
1013 5
                        'bigint' => static::INT_BIG,
1014 89
                        'int' => static::INT_REGULAR,
1015 89
                        'mediumint' => static::INT_MEDIUM,
1016 89
                        'smallint' => static::INT_SMALL,
1017 89
                        'tinyint' => static::INT_TINY,
1018 89
                    ];
1019 89
                    $limits = [
1020
                        'tinyint' => 4,
1021 89
                        'smallint' => 6,
1022 2
                        'int' => 11,
1023 2
                        'bigint' => 20,
1024
                    ];
1025 89
                    foreach ($sizes as $name => $length) {
1026 1
                        if ($limit >= $length) {
1027 1
                            $def = ['name' => $name];
1028
                            if (isset($limits[$name])) {
1029 89
                                $def['limit'] = $limits[$name];
1030
                            }
1031
1032
                            return $def;
1033
                        }
1034
                    }
1035
                } elseif (!$limit) {
1036
                    $limit = 11;
1037
                }
1038 16
1039
                return ['name' => 'int', 'limit' => $limit];
1040 16
            case static::PHINX_TYPE_BIG_INTEGER:
1041 16
                return ['name' => 'bigint', 'limit' => $limit ?: 20];
1042 16
            case static::PHINX_TYPE_BOOLEAN:
1043 2
                return ['name' => 'tinyint', 'limit' => 1];
1044 2
            case static::PHINX_TYPE_UUID:
1045
                return ['name' => 'char', 'limit' => 36];
1046 16
            case static::PHINX_TYPE_YEAR:
1047 5
                if (!$limit || in_array($limit, [2, 4])) {
1048 5
                    $limit = 4;
1049
                }
1050 16
1051 1
                return ['name' => 'year', 'limit' => $limit];
1052 1
            default:
1053
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by MySQL.');
1054 16
        }
1055
    }
1056 16
1057 5
    /**
1058 5
     * Returns Phinx type by SQL type
1059
     *
1060 16
     * @internal param string $sqlType SQL type
1061
     *
1062 16
     * @param string $sqlTypeDef SQL Type definition
1063
     *
1064
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1065
     *
1066
     * @return array Phinx type
1067
     */
1068
    public function getPhinxType($sqlTypeDef)
1069
    {
1070
        $matches = [];
1071 17
        if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*(.+)*$/', $sqlTypeDef, $matches)) {
1072
            throw new UnsupportedColumnTypeException('Column type "' . $sqlTypeDef . '" is not supported by MySQL.');
1073 17
        }
1074 17
1075 5
        $limit = null;
1076 5
        $scale = null;
1077 17
        $type = $matches[1];
1078 17
        if (count($matches) > 2) {
1079 17
            $limit = $matches[3] ? (int)$matches[3] : null;
1080 17
        }
1081 17
        if (count($matches) > 4) {
1082 17
            $scale = (int)$matches[5];
1083 17
        }
1084 17
        if ($type === 'tinyint' && $limit === 1) {
1085 17
            $type = static::PHINX_TYPE_BOOLEAN;
1086 17
            $limit = null;
1087 17
        }
1088 2
        switch ($type) {
1089 2
            case 'varchar':
1090 17
                $type = static::PHINX_TYPE_STRING;
1091 2
                if ($limit === 255) {
1092 2
                    $limit = null;
1093 17
                }
1094
                break;
1095
            case 'char':
1096
                $type = static::PHINX_TYPE_CHAR;
1097
                if ($limit === 255) {
1098
                    $limit = null;
1099
                }
1100
                if ($limit === 36) {
1101
                    $type = static::PHINX_TYPE_UUID;
1102 2
                }
1103
                break;
1104 2
            case 'tinyint':
1105
                $type = static::PHINX_TYPE_TINY_INTEGER;
1106
                $limit = static::INT_TINY;
1107 2
                break;
1108
            case 'smallint':
1109
                $type = static::PHINX_TYPE_SMALL_INTEGER;
1110
                $limit = static::INT_SMALL;
1111 2
                break;
1112 2
            case 'mediumint':
1113
                $type = static::PHINX_TYPE_INTEGER;
1114 2
                $limit = static::INT_MEDIUM;
1115
                break;
1116 2
            case 'int':
1117
                $type = static::PHINX_TYPE_INTEGER;
1118
                if ($limit === 11) {
1119
                    $limit = null;
1120
                }
1121
                break;
1122
            case 'bigint':
1123 85
                if ($limit === 20) {
1124
                    $limit = null;
1125 85
                }
1126
                $type = static::PHINX_TYPE_BIG_INTEGER;
1127
                break;
1128
            case 'bit':
1129
                $type = static::PHINX_TYPE_BIT;
1130
                if ($limit === 64) {
1131
                    $limit = null;
1132
                }
1133
                break;
1134
            case 'blob':
1135
                $type = static::PHINX_TYPE_BLOB;
1136
                $limit = static::BLOB_REGULAR;
1137
                break;
1138
            case 'tinyblob':
1139
                $type = static::PHINX_TYPE_TINYBLOB;
1140
                $limit = static::BLOB_TINY;
1141
                break;
1142
            case 'mediumblob':
1143
                $type = static::PHINX_TYPE_MEDIUMBLOB;
1144
                $limit = static::BLOB_MEDIUM;
1145
                break;
1146
            case 'longblob':
1147
                $type = static::PHINX_TYPE_LONGBLOB;
1148
                $limit = static::BLOB_LONG;
1149
                break;
1150
            case 'tinytext':
1151
                $type = static::PHINX_TYPE_TEXT;
1152
                $limit = static::TEXT_TINY;
1153
                break;
1154
            case 'mediumtext':
1155
                $type = static::PHINX_TYPE_TEXT;
1156
                $limit = static::TEXT_MEDIUM;
1157
                break;
1158
            case 'longtext':
1159
                $type = static::PHINX_TYPE_TEXT;
1160
                $limit = static::TEXT_LONG;
1161
                break;
1162
            case 'binary':
1163
                if ($limit === null) {
1164
                    $limit = 255;
1165
                }
1166
1167
                if ($limit > 255) {
1168
                    $type = static::PHINX_TYPE_BLOB;
1169
                    break;
1170
                }
1171
1172
                if ($limit === 16) {
1173
                    $type = static::PHINX_TYPE_BINARYUUID;
1174
                }
1175
                break;
1176
        }
1177
1178
        try {
1179
            // Call this to check if parsed type is supported.
1180
            $this->getSqlType($type, $limit);
1181
        } catch (UnsupportedColumnTypeException $e) {
1182
            $type = Literal::from($type);
1183
        }
1184
1185
        $phinxType = [
1186
            'name' => $type,
1187
            'limit' => $limit,
1188
            'scale' => $scale,
1189
        ];
1190
1191
        if ($type === static::PHINX_TYPE_ENUM || $type === static::PHINX_TYPE_SET) {
1192
            $values = trim($matches[6], "()");
1193
            $phinxType['values'] = [];
1194
            $opened = false;
1195
            $escaped = false;
1196
            $wasEscaped = false;
1197
            $value = '';
1198
            $valuesLength = strlen($values);
1199
            for ($i = 0; $i < $valuesLength; $i++) {
1200
                $char = $values[$i];
1201
                if ($char === "'" && !$opened) {
1202
                    $opened = true;
1203
                } elseif (
1204
                    !$escaped
1205
                    && ($i + 1) < $valuesLength
1206
                    && (
1207
                        $char === "'" && $values[$i + 1] === "'"
1208
                        || $char === "\\" && $values[$i + 1] === "\\"
1209
                    )
1210
                ) {
1211
                    $escaped = true;
1212
                } elseif ($char === "'" && $opened && !$escaped) {
1213
                    $phinxType['values'][] = $value;
1214
                    $value = '';
1215
                    $opened = false;
1216
                } elseif (($char === "'" || $char === "\\") && $opened && $escaped) {
1217
                    $value .= $char;
1218
                    $escaped = false;
1219
                    $wasEscaped = true;
1220
                } elseif ($opened) {
1221
                    if ($values[$i - 1] === "\\" && !$wasEscaped) {
1222
                        if ($char === 'n') {
1223
                            $char = "\n";
1224
                        } elseif ($char === 'r') {
1225
                            $char = "\r";
1226
                        } elseif ($char === 't') {
1227
                            $char = "\t";
1228
                        }
1229
                        if ($values[$i] !== $char) {
1230
                            $value = substr($value, 0, strlen($value) - 1);
1231
                        }
1232
                    }
1233
                    $value .= $char;
1234
                    $wasEscaped = false;
1235
                }
1236
            }
1237
        }
1238
1239
        return $phinxType;
1240
    }
1241
1242
    /**
1243
     * @inheritDoc
1244
     */
1245
    public function createDatabase($name, $options = [])
1246
    {
1247
        $charset = $options['charset'] ?? 'utf8';
1248
1249
        if (isset($options['collation'])) {
1250
            $this->execute(sprintf(
1251
                'CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s` COLLATE `%s`',
1252
                $name,
1253
                $charset,
1254
                $options['collation']
1255
            ));
1256
        } else {
1257
            $this->execute(sprintf('CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s`', $name, $charset));
1258
        }
1259
    }
1260
1261
    /**
1262
     * @inheritDoc
1263
     */
1264
    public function hasDatabase($name)
1265
    {
1266
        $rows = $this->fetchAll(
1267
            sprintf(
1268
                'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = \'%s\'',
1269
                $name
1270
            )
1271
        );
1272
1273
        foreach ($rows as $row) {
1274
            if (!empty($row)) {
1275
                return true;
1276
            }
1277
        }
1278
1279
        return false;
1280
    }
1281
1282
    /**
1283
     * @inheritDoc
1284
     */
1285
    public function dropDatabase($name)
1286
    {
1287
        $this->execute(sprintf('DROP DATABASE IF EXISTS `%s`', $name));
1288
        $this->createdTables = [];
1289
    }
1290
1291
    /**
1292
     * Gets the MySQL Column Definition for a Column object.
1293
     *
1294
     * @param \Phinx\Db\Table\Column $column Column
1295
     *
1296
     * @return string
1297
     */
1298
    protected function getColumnSqlDefinition(Column $column)
1299
    {
1300
        if ($column->getType() instanceof Literal) {
1301
            $def = (string)$column->getType();
1302
        } else {
1303
            $sqlType = $this->getSqlType($column->getType(), $column->getLimit());
1304
            $def = strtoupper($sqlType['name']);
1305
        }
1306
        if ($column->getPrecision() && $column->getScale()) {
1307
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1308
        } elseif (isset($sqlType['limit'])) {
1309
            $def .= '(' . $sqlType['limit'] . ')';
1310
        }
1311
        if (($values = $column->getValues()) && is_array($values)) {
1312
            $def .= "(" . implode(", ", array_map(function ($value) {
1313
                // we special case NULL as it's not actually allowed an enum value,
1314
                // and we want MySQL to issue an error on the create statement, but
1315
                // quote coerces it to an empty string, which will not error
1316
                return $value === null ? 'NULL' : $this->getConnection()->quote($value);
1317
            }, $values)) . ")";
1318
        }
1319
1320
        $def .= $column->getEncoding() ? ' CHARACTER SET ' . $column->getEncoding() : '';
1321
        $def .= $column->getCollation() ? ' COLLATE ' . $column->getCollation() : '';
1322
        $def .= !$column->isSigned() && isset($this->signedColumnTypes[$column->getType()]) ? ' unsigned' : '';
1323
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1324
1325
        if (
1326
            version_compare($this->getAttribute(\PDO::ATTR_SERVER_VERSION), '8') > -1
1327
            && in_array($column->getType(), [
1328
                static::PHINX_TYPE_GEOMETRY,
1329
                static::PHINX_TYPE_POINT,
1330
                static::PHINX_TYPE_LINESTRING,
1331
                static::PHINX_TYPE_POLYGON,
1332
            ])
1333
            && !is_null($column->getSrid())
1334
        ) {
1335
            $def .= " SRID {$column->getSrid()}";
1336
        }
1337
1338
        $def .= $column->isIdentity() ? ' AUTO_INCREMENT' : '';
1339
        $def .= $this->getDefaultValueDefinition($column->getDefault(), $column->getType());
1340
1341
        if ($column->getComment()) {
1342
            $def .= ' COMMENT ' . $this->getConnection()->quote($column->getComment());
1343
        }
1344
1345
        if ($column->getUpdate()) {
1346
            $def .= ' ON UPDATE ' . $column->getUpdate();
1347
        }
1348
1349
        return $def;
1350
    }
1351
1352
    /**
1353
     * Gets the MySQL Index Definition for an Index object.
1354
     *
1355
     * @param \Phinx\Db\Table\Index $index Index
1356
     *
1357
     * @return string
1358
     */
1359
    protected function getIndexSqlDefinition(Index $index)
1360
    {
1361
        $def = '';
1362
        $limit = '';
1363
1364
        if ($index->getType() === Index::UNIQUE) {
1365
            $def .= ' UNIQUE';
1366
        }
1367
1368
        if ($index->getType() === Index::FULLTEXT) {
1369
            $def .= ' FULLTEXT';
1370
        }
1371
1372
        $def .= ' KEY';
1373
1374
        if (is_string($index->getName())) {
1375
            $def .= ' `' . $index->getName() . '`';
1376
        }
1377
1378
        $columnNames = $index->getColumns();
1379
        $order = $index->getOrder() ?? [];
1380
        $columnNames = array_map(function ($columnName) use ($order) {
1381
            $ret = '`' . $columnName . '`';
1382
            if (isset($order[$columnName])) {
1383
                $ret .= ' ' . $order[$columnName];
1384
            }
1385
1386
            return $ret;
1387
        }, $columnNames);
1388
1389
        if (!is_array($index->getLimit())) {
1390
            if ($index->getLimit()) {
1391
                $limit = '(' . $index->getLimit() . ')';
1392
            }
1393
            $def .= ' (' . implode(',', $columnNames) . $limit . ')';
1394
        } else {
1395
            $columns = $index->getColumns();
1396
            $limits = $index->getLimit();
1397
            $def .= ' (';
1398
            foreach ($columns as $column) {
1399
                $limit = !isset($limits[$column]) || $limits[$column] <= 0 ? '' : '(' . $limits[$column] . ')';
1400
                $columnSort = isset($order[$column]) ?? '';
1401
                $def .= '`' . $column . '`' . $limit . ' ' . $columnSort . ', ';
1402
            }
1403
            $def = rtrim($def, ', ');
1404
            $def .= ' )';
1405
        }
1406
1407
        return $def;
1408
    }
1409
1410
    /**
1411
     * Gets the MySQL Foreign Key Definition for an ForeignKey object.
1412
     *
1413
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1414
     *
1415
     * @return string
1416
     */
1417
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
1418
    {
1419
        $def = '';
1420
        if ($foreignKey->getConstraint()) {
1421
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
1422
        }
1423
        $columnNames = [];
1424
        foreach ($foreignKey->getColumns() as $column) {
1425
            $columnNames[] = $this->quoteColumnName($column);
1426
        }
1427
        $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1428
        $refColumnNames = [];
1429
        foreach ($foreignKey->getReferencedColumns() as $column) {
1430
            $refColumnNames[] = $this->quoteColumnName($column);
1431
        }
1432
        $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1433
        if ($foreignKey->getOnDelete()) {
1434
            $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1435
        }
1436
        if ($foreignKey->getOnUpdate()) {
1437
            $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1438
        }
1439
1440
        return $def;
1441
    }
1442
1443
    /**
1444
     * Describes a database table. This is a MySQL adapter specific method.
1445
     *
1446
     * @param string $tableName Table name
1447
     *
1448
     * @return array
1449
     */
1450
    public function describeTable($tableName)
1451
    {
1452
        $options = $this->getOptions();
1453
1454
        // mysql specific
1455
        $sql = sprintf(
1456
            "SELECT *
1457
             FROM information_schema.tables
1458
             WHERE table_schema = '%s'
1459
             AND table_name = '%s'",
1460
            $options['name'],
1461
            $tableName
1462
        );
1463
1464
        return $this->fetchRow($sql);
1465
    }
1466
1467
    /**
1468
     * Returns MySQL column types (inherited and MySQL specified).
1469
     *
1470
     * @return string[]
1471
     */
1472
    public function getColumnTypes()
1473
    {
1474
        return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1475
    }
1476
1477
    /**
1478
     * @inheritDoc
1479
     */
1480
    public function getDecoratedConnection()
1481
    {
1482
        $options = $this->getOptions();
1483
        $options = [
1484
            'username' => $options['user'] ?? null,
1485
            'password' => $options['pass'] ?? null,
1486
            'database' => $options['name'],
1487
            'quoteIdentifiers' => true,
1488
        ] + $options;
1489
1490
        $driver = new MysqlDriver($options);
1491
        $driver->setConnection($this->connection);
1492
1493
        return new Connection(['driver' => $driver] + $options);
1494
    }
1495
}
1496