Completed
Push — master ( 5a0146...15be19 )
by mark
18s queued 13s
created

MysqlAdapter::getColumns()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 46
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 6

Importance

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