MysqlAdapter::getPhinxType()   F
last analyzed

Complexity

Conditions 56
Paths 1297

Size

Total Lines 163
Code Lines 131

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 56

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 131
c 7
b 0
f 0
dl 0
loc 163
ccs 6
cts 6
cp 1
rs 0
cc 56
nc 1297
nop 1
crap 56

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1347
            $def .= '(' . implode(', ', array_map(function ($value) {
1348
                // we special case NULL as it's not actually allowed an enum value,
1349
                // and we want MySQL to issue an error on the create statement, but
1350
                // quote coerces it to an empty string, which will not error
1351
                return $value === null ? 'NULL' : $this->getConnection()->quote($value);
1352
            }, $values)) . ')';
1353
        }
1354
1355
        $def .= $column->getEncoding() ? ' CHARACTER SET ' . $column->getEncoding() : '';
1356
        $def .= $column->getCollation() ? ' COLLATE ' . $column->getCollation() : '';
1357
        $def .= !$column->isSigned() && isset($this->signedColumnTypes[$column->getType()]) ? ' unsigned' : '';
1358
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1359
1360
        if (
1361
            version_compare($this->getAttribute(\PDO::ATTR_SERVER_VERSION), '8', '>=')
1362
            && in_array($column->getType(), static::PHINX_TYPES_GEOSPATIAL)
1363
            && !is_null($column->getSrid())
1364
        ) {
1365
            $def .= " SRID {$column->getSrid()}";
1366
        }
1367
1368
        $def .= $column->isIdentity() ? ' AUTO_INCREMENT' : '';
1369
1370
        $default = $column->getDefault();
1371
        // MySQL 8 supports setting default for the following tested types, but only if they are "cast as expressions"
1372
        if (
1373
            version_compare($this->getAttribute(\PDO::ATTR_SERVER_VERSION), '8', '>=') &&
1374
            is_string($default) &&
1375
            in_array(
1376
                $column->getType(),
1377
                array_merge(
1378
                    static::PHINX_TYPES_GEOSPATIAL,
1379
                    [static::PHINX_TYPE_BLOB, static::PHINX_TYPE_JSON, static::PHINX_TYPE_TEXT]
1380
                )
1381
            )
1382
        ) {
1383
            $default = Literal::from('(' . $this->getConnection()->quote($column->getDefault()) . ')');
1384
        }
1385
        $def .= $this->getDefaultValueDefinition($default, $column->getType());
1386
1387
        if ($column->getComment()) {
1388
            $def .= ' COMMENT ' . $this->getConnection()->quote($column->getComment());
1389
        }
1390
1391
        if ($column->getUpdate()) {
1392
            $def .= ' ON UPDATE ' . $column->getUpdate();
1393
        }
1394
1395
        return $def;
1396
    }
1397
1398
    /**
1399
     * Gets the MySQL Index Definition for an Index object.
1400
     *
1401
     * @param \Phinx\Db\Table\Index $index Index
1402
     * @return string
1403
     */
1404
    protected function getIndexSqlDefinition(Index $index)
1405
    {
1406
        $def = '';
1407
        $limit = '';
1408
1409
        if ($index->getType() === Index::UNIQUE) {
1410
            $def .= ' UNIQUE';
1411
        }
1412
1413
        if ($index->getType() === Index::FULLTEXT) {
1414
            $def .= ' FULLTEXT';
1415
        }
1416
1417
        $def .= ' KEY';
1418
1419
        if (is_string($index->getName())) {
1420
            $def .= ' `' . $index->getName() . '`';
1421
        }
1422
1423
        $columnNames = $index->getColumns();
1424
        $order = $index->getOrder() ?? [];
1425
        $columnNames = array_map(function ($columnName) use ($order) {
1426
            $ret = '`' . $columnName . '`';
1427
            if (isset($order[$columnName])) {
1428
                $ret .= ' ' . $order[$columnName];
1429
            }
1430
1431
            return $ret;
1432
        }, $columnNames);
1433
1434
        if (!is_array($index->getLimit())) {
1435
            if ($index->getLimit()) {
1436
                $limit = '(' . $index->getLimit() . ')';
1437
            }
1438
            $def .= ' (' . implode(',', $columnNames) . $limit . ')';
1439
        } else {
1440
            $columns = $index->getColumns();
1441
            $limits = $index->getLimit();
1442
            $def .= ' (';
1443
            foreach ($columns as $column) {
1444
                $limit = !isset($limits[$column]) || $limits[$column] <= 0 ? '' : '(' . $limits[$column] . ')';
1445
                $columnSort = isset($order[$column]) ?? '';
1446
                $def .= '`' . $column . '`' . $limit . ' ' . $columnSort . ', ';
1447
            }
1448
            $def = rtrim($def, ', ');
1449
            $def .= ' )';
1450
        }
1451
1452
        return $def;
1453
    }
1454
1455
    /**
1456
     * Gets the MySQL Foreign Key Definition for an ForeignKey object.
1457
     *
1458
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1459
     * @return string
1460
     */
1461
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
1462
    {
1463
        $def = '';
1464
        if ($foreignKey->getConstraint()) {
1465
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
1466
        }
1467
        $columnNames = [];
1468
        foreach ($foreignKey->getColumns() as $column) {
1469
            $columnNames[] = $this->quoteColumnName($column);
1470
        }
1471
        $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1472
        $refColumnNames = [];
1473
        foreach ($foreignKey->getReferencedColumns() as $column) {
1474
            $refColumnNames[] = $this->quoteColumnName($column);
1475
        }
1476
        $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1477
        if ($foreignKey->getOnDelete()) {
1478
            $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1479
        }
1480
        if ($foreignKey->getOnUpdate()) {
1481
            $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1482
        }
1483
1484
        return $def;
1485
    }
1486
1487
    /**
1488
     * Describes a database table. This is a MySQL adapter specific method.
1489
     *
1490
     * @param string $tableName Table name
1491
     * @return array
1492
     */
1493
    public function describeTable($tableName)
1494
    {
1495
        $options = $this->getOptions();
1496
1497
        // mysql specific
1498
        $sql = sprintf(
1499
            "SELECT *
1500
             FROM information_schema.tables
1501
             WHERE table_schema = '%s'
1502
             AND table_name = '%s'",
1503
            $options['name'],
1504
            $tableName
1505
        );
1506
1507
        return $this->fetchRow($sql);
1508
    }
1509
1510
    /**
1511
     * Returns MySQL column types (inherited and MySQL specified).
1512
     *
1513
     * @return string[]
1514
     */
1515
    public function getColumnTypes()
1516
    {
1517
        return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1518
    }
1519
1520
    /**
1521
     * @inheritDoc
1522
     */
1523
    public function getDecoratedConnection()
1524
    {
1525
        $options = $this->getOptions();
1526
        $options = [
1527
            'username' => $options['user'] ?? null,
1528
            'password' => $options['pass'] ?? null,
1529
            'database' => $options['name'],
1530
            'quoteIdentifiers' => true,
1531
        ] + $options;
1532
1533
        $driver = new MysqlDriver($options);
1534
        $driver->setConnection($this->connection);
1535
1536
        return new Connection(['driver' => $driver] + $options);
1537
    }
1538
}
1539