Passed
Pull Request — master (#2026)
by
unknown
03:35 queued 36s
created

MysqlAdapter::hasDatabase()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 16
c 0
b 0
f 0
ccs 0
cts 0
cp 0
rs 10
cc 3
nc 3
nop 1
crap 12
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
                $default = preg_replace("/^_(?:[a-zA-Z0-9]+?)\\\'(.*)\\\'$/", '\1', $default);
471 5
            }
472 5
            $column->setDefault($default);
473 5
474 5
            $columns[] = $column;
475 5
        }
476 5
477
        return $columns;
478
    }
479
480
    /**
481
     * @inheritDoc
482
     */
483
    public function hasColumn($tableName, $columnName)
484 19
    {
485
        $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName)));
486 19
        foreach ($rows as $column) {
487 19
            if (strcasecmp($column['Field'], $columnName) === 0) {
488 19
                return true;
489 18
            }
490 18
        }
491 18
492 18
        return false;
493 19
    }
494 19
495
    /**
496
     * @inheritDoc
497
     */
498
    protected function getAddColumnInstructions(Table $table, Column $column)
499
    {
500 14
        $alter = sprintf(
501
            'ADD %s %s',
502 14
            $this->quoteColumnName($column->getName()),
503 6
            $this->getColumnSqlDefinition($column)
504 6
        );
505
506 14
        $alter .= $this->afterClause($column);
507 14
508
        return new AlterInstructions([$alter]);
509 14
    }
510 14
511 12
    /**
512
     * Exposes the MySQL syntax to arrange a column `FIRST`.
513 13
     *
514
     * @param \Phinx\Db\Table\Column $column The column being altered.
515 11
     * @return string The appropriate SQL fragment.
516
     */
517
    protected function afterClause(Column $column)
518
    {
519
        $after = $column->getAfter();
520
        if (empty($after)) {
521 1
            return '';
522
        }
523 1
524
        if ($after === self::FIRST) {
525 1
            return ' FIRST';
526 1
        }
527 1
528
        return ' AFTER ' . $this->quoteColumnName($after);
529 1
    }
530
531
    /**
532
     * {@inheritDoc}
533
     *
534
     * @throws \InvalidArgumentException
535
     */
536
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
537 4
    {
538
        $rows = $this->fetchAll(sprintf('SHOW FULL COLUMNS FROM %s', $this->quoteTableName($tableName)));
539 4
540 4
        foreach ($rows as $row) {
541 4
            if (strcasecmp($row['Field'], $columnName) === 0) {
542 4
                $null = $row['Null'] === 'NO' ? 'NOT NULL' : 'NULL';
543 4
                $comment = isset($row['Comment']) ? ' COMMENT ' . '\'' . addslashes($row['Comment']) . '\'' : '';
544 4
                $extra = ' ' . strtoupper($row['Extra']);
545 4
                if (($row['Default'] !== null)) {
546 4
                    $extra .= $this->getDefaultValueDefinition($row['Default']);
547
                }
548
                $definition = $row['Type'] . ' ' . $null . $extra . $comment;
549
550
                $alter = sprintf(
551 3
                    'CHANGE COLUMN %s %s %s',
552
                    $this->quoteColumnName($columnName),
553 3
                    $this->quoteColumnName($newColumnName),
554 2
                    $definition
555 2
                );
556
557 3
                return new AlterInstructions([$alter]);
558 3
            }
559
        }
560 3
561 3
        throw new InvalidArgumentException(sprintf(
562 3
            "The specified column doesn't exist: " .
563 3
            $columnName
564 3
        ));
565 3
    }
566 3
567 3
    /**
568 3
     * @inheritDoc
569 3
     */
570
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
571 3
    {
572 1
        $alter = sprintf(
573
            'CHANGE %s %s %s%s',
574
            $this->quoteColumnName($columnName),
575
            $this->quoteColumnName($newColumn->getName()),
576
            $this->getColumnSqlDefinition($newColumn),
577 2
            $this->afterClause($newColumn)
578
        );
579 2
580
        return new AlterInstructions([$alter]);
581 2
    }
582
583 2
    /**
584 2
     * @inheritDoc
585 2
     */
586 2
    protected function getDropColumnInstructions($tableName, $columnName)
587 2
    {
588 2
        $alter = sprintf('DROP COLUMN %s', $this->quoteColumnName($columnName));
589 2
590 2
        return new AlterInstructions([$alter]);
591 2
    }
592
593 2
    /**
594
     * Get an array of indexes from a particular table.
595
     *
596
     * @param string $tableName Table name
597
     * @return array
598
     */
599 21
    protected function getIndexes($tableName)
600
    {
601 21
        $indexes = [];
602 5
        $rows = $this->fetchAll(sprintf('SHOW INDEXES FROM %s', $this->quoteTableName($tableName)));
603 5
        foreach ($rows as $row) {
604 21
            if (!isset($indexes[$row['Key_name']])) {
605 21
                $indexes[$row['Key_name']] = ['columns' => []];
606 6
            }
607 4
            $indexes[$row['Key_name']]['columns'][] = strtolower($row['Column_name']);
608
        }
609 4
610
        return $indexes;
611 15
    }
612 12
613 10
    /**
614
     * @inheritDoc
615 11
     */
616 11
    public function hasIndex($tableName, $columns)
617
    {
618
        if (is_string($columns)) {
619
            $columns = [$columns]; // str to array
620
        }
621
622
        $columns = array_map('strtolower', $columns);
623
        $indexes = $this->getIndexes($tableName);
624
625
        foreach ($indexes as $index) {
626 22
            if ($columns == $index['columns']) {
627
                return true;
628 22
            }
629 22
        }
630
631
        return false;
632
    }
633
634
    /**
635
     * @inheritDoc
636
     */
637
    public function hasIndexByName($tableName, $indexName)
638
    {
639
        $indexes = $this->getIndexes($tableName);
640 22
641
        foreach ($indexes as $name => $index) {
642 22
            if ($name === $indexName) {
643 22
                return true;
644 19
            }
645 19
        }
646 19
647 19
        return false;
648 22
    }
649 22
650
    /**
651
     * @inheritDoc
652
     */
653
    protected function getAddIndexInstructions(Table $table, Index $index)
654
    {
655 15
        $instructions = new AlterInstructions();
656
657 15
        if ($index->getType() === Index::FULLTEXT) {
658 15
            // Must be executed separately
659 15
            // SQLSTATE[HY000]: General error: 1795 InnoDB presently supports one FULLTEXT index creation at a time
660 15
            $alter = sprintf(
661 15
                'ALTER TABLE %s ADD %s',
662 15
                $this->quoteTableName($table->getName()),
663 15
                $this->getIndexSqlDefinition($index)
664 15
            );
665
666
            $instructions->addPostStep($alter);
667
        } else {
668
            $alter = sprintf(
669 8
                'ADD %s',
670
                $this->getIndexSqlDefinition($index)
671 8
            );
672 3
673 3
            $instructions->addAlter($alter);
674
        }
675
676 8
        return $instructions;
677 8
    }
678 8
679 8
    /**
680 8
     * {@inheritDoc}
681
     *
682 8
     * @throws \InvalidArgumentException
683 8
     */
684 8
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
685
    {
686 7
        if (is_string($columns)) {
687 7
            $columns = [$columns]; // str to array
688
        }
689
690
        $indexes = $this->getIndexes($tableName);
691
        $columns = array_map('strtolower', $columns);
692
693
        foreach ($indexes as $indexName => $index) {
694
            if ($columns == $index['columns']) {
695 7
                return new AlterInstructions([sprintf(
696 7
                    'DROP INDEX %s',
697
                    $this->quoteColumnName($indexName)
698 7
                )]);
699 7
            }
700 7
        }
701 7
702 7
        throw new InvalidArgumentException(sprintf(
703
            "The specified index on columns '%s' does not exist",
704 7
            implode(',', $columns)
705
        ));
706
    }
707
708
    /**
709 96
     * {@inheritDoc}
710
     *
711
     * @throws \InvalidArgumentException
712 96
     */
713 87
    protected function getDropIndexByNameInstructions($tableName, $indexName)
714
    {
715 96
        $indexes = $this->getIndexes($tableName);
716 4
717
        foreach ($indexes as $name => $index) {
718 96
            if ($name === $indexName) {
719 9
                return new AlterInstructions([sprintf(
720
                    'DROP INDEX %s',
721
                    $this->quoteColumnName($indexName)
722 6
                )]);
723 6
            }
724 6
        }
725 6
726 6
        throw new InvalidArgumentException(sprintf(
727 6
            "The specified index name '%s' does not exist",
728 6
            $indexName
729 6
        ));
730
    }
731 5
732
    /**
733 5
     * @inheritDoc
734
     */
735 95
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
736 5
    {
737
        $primaryKey = $this->getPrimaryKey($tableName);
738 95
739 3
        if (empty($primaryKey['constraint'])) {
740
            return false;
741 95
        }
742 1
743
        if ($constraint) {
744
            return $primaryKey['constraint'] === $constraint;
745 1
        } else {
746 1
            if (is_string($columns)) {
747 1
                $columns = [$columns]; // str to array
748 1
            }
749 1
            $missingColumns = array_diff($columns, $primaryKey['columns']);
750 1
751 1
            return empty($missingColumns);
752 1
        }
753
    }
754 1
755
    /**
756 1
     * Get the primary key from a particular table.
757
     *
758 95
     * @param string $tableName Table name
759 82
     * @return array
760
     */
761
    public function getPrimaryKey($tableName)
762 6
    {
763 6
        $options = $this->getOptions();
764 6
        $rows = $this->fetchAll(sprintf(
765 6
            "SELECT
766 6
                k.CONSTRAINT_NAME,
767 6
                k.COLUMN_NAME
768
            FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS t
769 6
            JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
770 6
                USING(CONSTRAINT_NAME,TABLE_SCHEMA,TABLE_NAME)
771 6
            WHERE t.CONSTRAINT_TYPE='PRIMARY KEY'
772 6
                AND t.TABLE_SCHEMA='%s'
773 6
                AND t.TABLE_NAME='%s'",
774 6
            $options['name'],
775 6
            $tableName
776 2
        ));
777 2
778 6
        $primaryKey = [
779
            'columns' => [],
780 5
        ];
781 82
        foreach ($rows as $row) {
782 76
            $primaryKey['constraint'] = $row['CONSTRAINT_NAME'];
783 76
            $primaryKey['columns'][] = $row['COLUMN_NAME'];
784 82
        }
785
786 86
        return $primaryKey;
787 82
    }
788
789 86
    /**
790 7
     * @inheritDoc
791
     */
792 84
    public function hasForeignKey($tableName, $columns, $constraint = null)
793 5
    {
794
        if (is_string($columns)) {
795 83
            $columns = [$columns]; // str to array
796 7
        }
797
        $foreignKeys = $this->getForeignKeys($tableName);
798 83
        if ($constraint) {
799 80
            if (isset($foreignKeys[$constraint])) {
800
                return !empty($foreignKeys[$constraint]);
801 83
            }
802 4
803
            return false;
804 83
        }
805 4
806
        foreach ($foreignKeys as $key) {
807 83
            if ($columns == $key['columns']) {
808 80
                return true;
809
            }
810 10
        }
811 2
812
        return false;
813 10
    }
814 10
815 10
    /**
816 10
     * Get an array of foreign keys from a particular table.
817 5
     *
818 8
     * @param string $tableName Table name
819 5
     * @return array
820
     */
821 6
    protected function getForeignKeys($tableName)
822 4
    {
823
        if (strpos($tableName, '.') !== false) {
824 2
            [$schema, $tableName] = explode('.', $tableName);
825
        }
826
827
        $foreignKeys = [];
828
        $rows = $this->fetchAll(sprintf(
829
            "SELECT
830 2
              CONSTRAINT_NAME,
831
              CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS TABLE_NAME,
832
              COLUMN_NAME,
833 2
              CONCAT(REFERENCED_TABLE_SCHEMA, '.', REFERENCED_TABLE_NAME) AS REFERENCED_TABLE_NAME,
834 2
              REFERENCED_COLUMN_NAME
835 2
            FROM information_schema.KEY_COLUMN_USAGE
836
            WHERE REFERENCED_TABLE_NAME IS NOT NULL
837
              AND TABLE_SCHEMA = %s
838
              AND TABLE_NAME = '%s'
839
            ORDER BY POSITION_IN_UNIQUE_CONSTRAINT",
840
            empty($schema) ? 'DATABASE()' : "'$schema'",
841
            $tableName
842
        ));
843
        foreach ($rows as $row) {
844
            $foreignKeys[$row['CONSTRAINT_NAME']]['table'] = $row['TABLE_NAME'];
845
            $foreignKeys[$row['CONSTRAINT_NAME']]['columns'][] = $row['COLUMN_NAME'];
846 17
            $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_table'] = $row['REFERENCED_TABLE_NAME'];
847
            $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME'];
848 17
        }
849 17
850 1
        return $foreignKeys;
851
    }
852 16
853 16
    /**
854 16
     * @inheritDoc
855 16
     */
856 14
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
857 14
    {
858 16
        $alter = sprintf(
859 4
            'ADD %s',
860 4
            $this->getForeignKeySqlDefinition($foreignKey)
861 16
        );
862 3
863 3
        return new AlterInstructions([$alter]);
864 3
    }
865
866 16
    /**
867 6
     * @inheritDoc
868 6
     */
869 3
    protected function getDropForeignKeyInstructions($tableName, $constraint)
870 3
    {
871 6
        $alter = sprintf(
872 16
            'DROP FOREIGN KEY %s',
873 5
            $constraint
874 5
        );
875 1
876 1
        return new AlterInstructions([$alter]);
877 5
    }
878 2
879 2
    /**
880 5
     * {@inheritDoc}
881 16
     *
882 2
     * @throws \InvalidArgumentException
883 2
     */
884 2
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
885 16
    {
886 2
        $instructions = new AlterInstructions();
887 2
888 2
        foreach ($columns as $column) {
889 16
            $rows = $this->fetchAll(sprintf(
890 2
                "SELECT
891 2
                    CONSTRAINT_NAME
892 2
                  FROM information_schema.KEY_COLUMN_USAGE
893 16
                  WHERE REFERENCED_TABLE_SCHEMA = DATABASE()
894 15
                    AND REFERENCED_TABLE_NAME IS NOT NULL
895 15
                    AND TABLE_NAME = '%s'
896 12
                    AND COLUMN_NAME = '%s'
897 12
                  ORDER BY POSITION_IN_UNIQUE_CONSTRAINT",
898 15
                $tableName,
899 11
                $column
900 6
            ));
901 4
902 4
            foreach ($rows as $row) {
903 6
                $instructions->merge($this->getDropForeignKeyInstructions($tableName, $row['CONSTRAINT_NAME']));
904 6
            }
905 10
        }
906 2
907 2
        if (empty($instructions->getAlterParts())) {
908 10
            throw new InvalidArgumentException(sprintf(
909 1
                "Not foreign key on columns '%s' exist",
910 1
                implode(',', $columns)
911 1
            ));
912 10
        }
913 1
914 1
        return $instructions;
915 1
    }
916 10
917 1
    /**
918 1
     * {@inheritDoc}
919 1
     *
920 10
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
921 2
     */
922 2
    public function getSqlType($type, $limit = null)
923 2
    {
924 9
        switch ($type) {
925 2
            case static::PHINX_TYPE_FLOAT:
926 2
            case static::PHINX_TYPE_DOUBLE:
927 2
            case static::PHINX_TYPE_DECIMAL:
928 8
            case static::PHINX_TYPE_DATE:
929 2
            case static::PHINX_TYPE_ENUM:
930 2
            case static::PHINX_TYPE_SET:
931 2
            case static::PHINX_TYPE_JSON:
932
            // Geospatial database types
933
            case static::PHINX_TYPE_GEOMETRY:
934
            case static::PHINX_TYPE_POINT:
935 16
            case static::PHINX_TYPE_LINESTRING:
936
            case static::PHINX_TYPE_POLYGON:
937
                return ['name' => $type];
938 15
            case static::PHINX_TYPE_DATETIME:
939 15
            case static::PHINX_TYPE_TIMESTAMP:
940
            case static::PHINX_TYPE_TIME:
941 15
                return ['name' => $type, 'limit' => $limit];
942
            case static::PHINX_TYPE_STRING:
943 15
                return ['name' => 'varchar', 'limit' => $limit ?: 255];
944 3
            case static::PHINX_TYPE_CHAR:
945 3
                return ['name' => 'char', 'limit' => $limit ?: 255];
946
            case static::PHINX_TYPE_TEXT:
947 15
                if ($limit) {
948
                    $sizes = [
949
                        // Order matters! Size must always be tested from longest to shortest!
950
                        'longtext' => static::TEXT_LONG,
951
                        'mediumtext' => static::TEXT_MEDIUM,
952
                        'text' => static::TEXT_REGULAR,
953
                        'tinytext' => static::TEXT_SMALL,
954 83
                    ];
955
                    foreach ($sizes as $name => $length) {
956 83
                        if ($limit >= $length) {
957
                            return ['name' => $name];
958 83
                        }
959 1
                    }
960 1
                }
961 82
962
                return ['name' => 'text'];
963 83
            case static::PHINX_TYPE_BINARY:
964
                if ($limit === null) {
965
                    $limit = 255;
966
                }
967
968 4
                if ($limit > 255) {
969
                    return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit);
970 4
                }
971 4
972 4
                return ['name' => 'binary', 'limit' => $limit];
973
            case static::PHINX_TYPE_BINARYUUID:
974 4
                return ['name' => 'binary', 'limit' => 16];
975 4
            case static::PHINX_TYPE_VARBINARY:
976
                if ($limit === null) {
977 4
                    $limit = 255;
978 3
                }
979 3
980
                if ($limit > 255) {
981 3
                    return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit);
982
                }
983 3
984
                return ['name' => 'varbinary', 'limit' => $limit];
985
            case static::PHINX_TYPE_BLOB:
986
                if ($limit !== null) {
987
                    // Rework this part as the choosen types were always UNDER the required length
988
                    $sizes = [
989 81
                        'tinyblob' => static::BLOB_SMALL,
990
                        'blob' => static::BLOB_REGULAR,
991 81
                        'mediumblob' => static::BLOB_MEDIUM,
992 81
                    ];
993
994
                    foreach ($sizes as $name => $length) {
995
                        if ($limit <= $length) {
996
                            return ['name' => $name];
997
                        }
998
                    }
999
1000 89
                    // For more length requirement, the longblob is used
1001
                    return ['name' => 'longblob'];
1002 89
                }
1003
1004 89
                // If not limit is provided, fallback on blob
1005 89
                return ['name' => 'blob'];
1006 89
            case static::PHINX_TYPE_TINYBLOB:
1007 2
                // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit
1008 89
                return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_TINY);
1009 86
            case static::PHINX_TYPE_MEDIUMBLOB:
1010 86
                // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit
1011 89
                return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_MEDIUM);
1012 5
            case static::PHINX_TYPE_LONGBLOB:
1013 5
                // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit
1014 89
                return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_LONG);
1015 89
            case static::PHINX_TYPE_BIT:
1016 89
                return ['name' => 'bit', 'limit' => $limit ?: 64];
1017 89
            case static::PHINX_TYPE_BIG_INTEGER:
1018 89
                return ['name' => 'bigint', 'limit' => $limit ?: 20];
1019 89
            case static::PHINX_TYPE_MEDIUM_INTEGER:
1020
                return ['name' => 'mediumint', 'limit' => $limit ?: 8];
1021 89
            case static::PHINX_TYPE_SMALL_INTEGER:
1022 2
                return ['name' => 'smallint', 'limit' => $limit ?: 6];
1023 2
            case static::PHINX_TYPE_TINY_INTEGER:
1024
                return ['name' => 'tinyint', 'limit' => $limit ?: 4];
1025 89
            case static::PHINX_TYPE_INTEGER:
1026 1
                if ($limit && $limit >= static::INT_TINY) {
1027 1
                    $sizes = [
1028
                        // Order matters! Size must always be tested from longest to shortest!
1029 89
                        'bigint' => static::INT_BIG,
1030
                        'int' => static::INT_REGULAR,
1031
                        'mediumint' => static::INT_MEDIUM,
1032
                        'smallint' => static::INT_SMALL,
1033
                        'tinyint' => static::INT_TINY,
1034
                    ];
1035
                    $limits = [
1036
                        'tinyint' => 4,
1037
                        'smallint' => 6,
1038 16
                        'mediumint' => 8,
1039
                        'int' => 11,
1040 16
                        'bigint' => 20,
1041 16
                    ];
1042 16
                    foreach ($sizes as $name => $length) {
1043 2
                        if ($limit >= $length) {
1044 2
                            $def = ['name' => $name];
1045
                            if (isset($limits[$name])) {
1046 16
                                $def['limit'] = $limits[$name];
1047 5
                            }
1048 5
1049
                            return $def;
1050 16
                        }
1051 1
                    }
1052 1
                } elseif (!$limit) {
1053
                    $limit = 11;
1054 16
                }
1055
1056 16
                return ['name' => 'int', 'limit' => $limit];
1057 5
            case static::PHINX_TYPE_BOOLEAN:
1058 5
                return ['name' => 'tinyint', 'limit' => 1];
1059
            case static::PHINX_TYPE_UUID:
1060 16
                return ['name' => 'char', 'limit' => 36];
1061
            case static::PHINX_TYPE_YEAR:
1062 16
                if (!$limit || in_array($limit, [2, 4])) {
1063
                    $limit = 4;
1064
                }
1065
1066
                return ['name' => 'year', 'limit' => $limit];
1067
            default:
1068
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by MySQL.');
1069
        }
1070
    }
1071 17
1072
    /**
1073 17
     * Returns Phinx type by SQL type
1074 17
     *
1075 5
     * @internal param string $sqlType SQL type
1076 5
     * @param string $sqlTypeDef SQL Type definition
1077 17
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1078 17
     * @return array Phinx type
1079 17
     */
1080 17
    public function getPhinxType($sqlTypeDef)
1081 17
    {
1082 17
        $matches = [];
1083 17
        if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*(.+)*$/', $sqlTypeDef, $matches)) {
1084 17
            throw new UnsupportedColumnTypeException('Column type "' . $sqlTypeDef . '" is not supported by MySQL.');
1085 17
        }
1086 17
1087 17
        $limit = null;
1088 2
        $scale = null;
1089 2
        $type = $matches[1];
1090 17
        if (count($matches) > 2) {
1091 2
            $limit = $matches[3] ? (int)$matches[3] : null;
1092 2
        }
1093 17
        if (count($matches) > 4) {
1094
            $scale = (int)$matches[5];
1095
        }
1096
        if ($type === 'tinyint' && $limit === 1) {
1097
            $type = static::PHINX_TYPE_BOOLEAN;
1098
            $limit = null;
1099
        }
1100
        switch ($type) {
1101
            case 'varchar':
1102 2
                $type = static::PHINX_TYPE_STRING;
1103
                if ($limit === 255) {
1104 2
                    $limit = null;
1105
                }
1106
                break;
1107 2
            case 'char':
1108
                $type = static::PHINX_TYPE_CHAR;
1109
                if ($limit === 255) {
1110
                    $limit = null;
1111 2
                }
1112 2
                if ($limit === 36) {
1113
                    $type = static::PHINX_TYPE_UUID;
1114 2
                }
1115
                break;
1116 2
            case 'tinyint':
1117
                $type = static::PHINX_TYPE_TINY_INTEGER;
1118
                $limit = static::INT_TINY;
1119
                break;
1120
            case 'smallint':
1121
                $type = static::PHINX_TYPE_SMALL_INTEGER;
1122
                $limit = static::INT_SMALL;
1123 85
                break;
1124
            case 'mediumint':
1125 85
                $type = static::PHINX_TYPE_MEDIUM_INTEGER;
1126
                $limit = static::INT_MEDIUM;
1127
                break;
1128
            case 'int':
1129
                $type = static::PHINX_TYPE_INTEGER;
1130
                if ($limit === 11) {
1131
                    $limit = null;
1132
                }
1133
                break;
1134
            case 'bigint':
1135
                if ($limit === 20) {
1136
                    $limit = null;
1137
                }
1138
                $type = static::PHINX_TYPE_BIG_INTEGER;
1139
                break;
1140
            case 'bit':
1141
                $type = static::PHINX_TYPE_BIT;
1142
                if ($limit === 64) {
1143
                    $limit = null;
1144
                }
1145
                break;
1146
            case 'blob':
1147
                $type = static::PHINX_TYPE_BLOB;
1148
                $limit = static::BLOB_REGULAR;
1149
                break;
1150
            case 'tinyblob':
1151
                $type = static::PHINX_TYPE_TINYBLOB;
1152
                $limit = static::BLOB_TINY;
1153
                break;
1154
            case 'mediumblob':
1155
                $type = static::PHINX_TYPE_MEDIUMBLOB;
1156
                $limit = static::BLOB_MEDIUM;
1157
                break;
1158
            case 'longblob':
1159
                $type = static::PHINX_TYPE_LONGBLOB;
1160
                $limit = static::BLOB_LONG;
1161
                break;
1162
            case 'tinytext':
1163
                $type = static::PHINX_TYPE_TEXT;
1164
                $limit = static::TEXT_TINY;
1165
                break;
1166
            case 'mediumtext':
1167
                $type = static::PHINX_TYPE_TEXT;
1168
                $limit = static::TEXT_MEDIUM;
1169
                break;
1170
            case 'longtext':
1171
                $type = static::PHINX_TYPE_TEXT;
1172
                $limit = static::TEXT_LONG;
1173
                break;
1174
            case 'binary':
1175
                if ($limit === null) {
1176
                    $limit = 255;
1177
                }
1178
1179
                if ($limit > 255) {
1180
                    $type = static::PHINX_TYPE_BLOB;
1181
                    break;
1182
                }
1183
1184
                if ($limit === 16) {
1185
                    $type = static::PHINX_TYPE_BINARYUUID;
1186
                }
1187
                break;
1188
        }
1189
1190
        try {
1191
            // Call this to check if parsed type is supported.
1192
            $this->getSqlType($type, $limit);
1193
        } catch (UnsupportedColumnTypeException $e) {
1194
            $type = Literal::from($type);
1195
        }
1196
1197
        $phinxType = [
1198
            'name' => $type,
1199
            'limit' => $limit,
1200
            'scale' => $scale,
1201
        ];
1202
1203
        if ($type === static::PHINX_TYPE_ENUM || $type === static::PHINX_TYPE_SET) {
1204
            $values = trim($matches[6], '()');
1205
            $phinxType['values'] = [];
1206
            $opened = false;
1207
            $escaped = false;
1208
            $wasEscaped = false;
1209
            $value = '';
1210
            $valuesLength = strlen($values);
1211
            for ($i = 0; $i < $valuesLength; $i++) {
1212
                $char = $values[$i];
1213
                if ($char === "'" && !$opened) {
1214
                    $opened = true;
1215
                } elseif (
1216
                    !$escaped
1217
                    && ($i + 1) < $valuesLength
1218
                    && (
1219
                        $char === "'" && $values[$i + 1] === "'"
1220
                        || $char === '\\' && $values[$i + 1] === '\\'
1221
                    )
1222
                ) {
1223
                    $escaped = true;
1224
                } elseif ($char === "'" && $opened && !$escaped) {
1225
                    $phinxType['values'][] = $value;
1226
                    $value = '';
1227
                    $opened = false;
1228
                } elseif (($char === "'" || $char === '\\') && $opened && $escaped) {
1229
                    $value .= $char;
1230
                    $escaped = false;
1231
                    $wasEscaped = true;
1232
                } elseif ($opened) {
1233
                    if ($values[$i - 1] === '\\' && !$wasEscaped) {
1234
                        if ($char === 'n') {
1235
                            $char = "\n";
1236
                        } elseif ($char === 'r') {
1237
                            $char = "\r";
1238
                        } elseif ($char === 't') {
1239
                            $char = "\t";
1240
                        }
1241
                        if ($values[$i] !== $char) {
1242
                            $value = substr($value, 0, strlen($value) - 1);
1243
                        }
1244
                    }
1245
                    $value .= $char;
1246
                    $wasEscaped = false;
1247
                }
1248
            }
1249
        }
1250
1251
        return $phinxType;
1252
    }
1253
1254
    /**
1255
     * @inheritDoc
1256
     */
1257
    public function createDatabase($name, $options = [])
1258
    {
1259
        $charset = $options['charset'] ?? 'utf8';
1260
1261
        if (isset($options['collation'])) {
1262
            $this->execute(sprintf(
1263
                'CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s` COLLATE `%s`',
1264
                $name,
1265
                $charset,
1266
                $options['collation']
1267
            ));
1268
        } else {
1269
            $this->execute(sprintf('CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s`', $name, $charset));
1270
        }
1271
    }
1272
1273
    /**
1274
     * @inheritDoc
1275
     */
1276
    public function hasDatabase($name)
1277
    {
1278
        $rows = $this->fetchAll(
1279
            sprintf(
1280
                'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = \'%s\'',
1281
                $name
1282
            )
1283
        );
1284
1285
        foreach ($rows as $row) {
1286
            if (!empty($row)) {
1287
                return true;
1288
            }
1289
        }
1290
1291
        return false;
1292
    }
1293
1294
    /**
1295
     * @inheritDoc
1296
     */
1297
    public function dropDatabase($name)
1298
    {
1299
        $this->execute(sprintf('DROP DATABASE IF EXISTS `%s`', $name));
1300
        $this->createdTables = [];
1301
    }
1302
1303
    /**
1304
     * Gets the MySQL Column Definition for a Column object.
1305
     *
1306
     * @param \Phinx\Db\Table\Column $column Column
1307
     * @return string
1308
     */
1309
    protected function getColumnSqlDefinition(Column $column)
1310
    {
1311
        if ($column->getType() instanceof Literal) {
1312
            $def = (string)$column->getType();
1313
        } else {
1314
            $sqlType = $this->getSqlType($column->getType(), $column->getLimit());
1315
            $def = strtoupper($sqlType['name']);
1316
        }
1317
        if ($column->getPrecision() && $column->getScale()) {
1318
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1319
        } elseif (isset($sqlType['limit'])) {
1320
            $def .= '(' . $sqlType['limit'] . ')';
1321
        }
1322
1323
        $values = $column->getValues();
1324
        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...
1325
            $def .= '(' . implode(', ', array_map(function ($value) {
1326
                // we special case NULL as it's not actually allowed an enum value,
1327
                // and we want MySQL to issue an error on the create statement, but
1328
                // quote coerces it to an empty string, which will not error
1329
                return $value === null ? 'NULL' : $this->getConnection()->quote($value);
1330
            }, $values)) . ')';
1331
        }
1332
1333
        $def .= $column->getEncoding() ? ' CHARACTER SET ' . $column->getEncoding() : '';
1334
        $def .= $column->getCollation() ? ' COLLATE ' . $column->getCollation() : '';
1335
        $def .= !$column->isSigned() && isset($this->signedColumnTypes[$column->getType()]) ? ' unsigned' : '';
1336
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1337
1338
        if (
1339
            version_compare($this->getAttribute(\PDO::ATTR_SERVER_VERSION), '8') > -1
1340
            && in_array($column->getType(), static::PHINX_TYPES_GEOSPATIAL)
1341
            && !is_null($column->getSrid())
1342
        ) {
1343
            $def .= " SRID {$column->getSrid()}";
1344
        }
1345
1346
        $def .= $column->isIdentity() ? ' AUTO_INCREMENT' : '';
1347
1348
        $default = $column->getDefault();
1349
        // MySQL 8 supports setting default for the following tested types, but only if they are "cast as expressions"
1350
        if (
1351
            version_compare($this->getAttribute(\PDO::ATTR_SERVER_VERSION), '8') > -1 &&
1352
            is_string($default) &&
1353
            in_array(
1354
                $column->getType(),
1355
                array_merge(
1356
                    static::PHINX_TYPES_GEOSPATIAL,
1357
                    [static::PHINX_TYPE_BLOB, static::PHINX_TYPE_JSON, static::PHINX_TYPE_TEXT]
1358
                )
1359
            )
1360
        ) {
1361
            $default = Literal::from('(' . $this->getConnection()->quote($column->getDefault()) . ')');
1362
        }
1363
        $def .= $this->getDefaultValueDefinition($default, $column->getType());
1364
1365
        if ($column->getComment()) {
1366
            $def .= ' COMMENT ' . $this->getConnection()->quote($column->getComment());
1367
        }
1368
1369
        if ($column->getUpdate()) {
1370
            $def .= ' ON UPDATE ' . $column->getUpdate();
1371
        }
1372
1373
        return $def;
1374
    }
1375
1376
    /**
1377
     * Gets the MySQL Index Definition for an Index object.
1378
     *
1379
     * @param \Phinx\Db\Table\Index $index Index
1380
     * @return string
1381
     */
1382
    protected function getIndexSqlDefinition(Index $index)
1383
    {
1384
        $def = '';
1385
        $limit = '';
1386
1387
        if ($index->getType() === Index::UNIQUE) {
1388
            $def .= ' UNIQUE';
1389
        }
1390
1391
        if ($index->getType() === Index::FULLTEXT) {
1392
            $def .= ' FULLTEXT';
1393
        }
1394
1395
        $def .= ' KEY';
1396
1397
        if (is_string($index->getName())) {
1398
            $def .= ' `' . $index->getName() . '`';
1399
        }
1400
1401
        $columnNames = $index->getColumns();
1402
        $order = $index->getOrder() ?? [];
1403
        $columnNames = array_map(function ($columnName) use ($order) {
1404
            $ret = '`' . $columnName . '`';
1405
            if (isset($order[$columnName])) {
1406
                $ret .= ' ' . $order[$columnName];
1407
            }
1408
1409
            return $ret;
1410
        }, $columnNames);
1411
1412
        if (!is_array($index->getLimit())) {
1413
            if ($index->getLimit()) {
1414
                $limit = '(' . $index->getLimit() . ')';
1415
            }
1416
            $def .= ' (' . implode(',', $columnNames) . $limit . ')';
1417
        } else {
1418
            $columns = $index->getColumns();
1419
            $limits = $index->getLimit();
1420
            $def .= ' (';
1421
            foreach ($columns as $column) {
1422
                $limit = !isset($limits[$column]) || $limits[$column] <= 0 ? '' : '(' . $limits[$column] . ')';
1423
                $columnSort = isset($order[$column]) ?? '';
1424
                $def .= '`' . $column . '`' . $limit . ' ' . $columnSort . ', ';
1425
            }
1426
            $def = rtrim($def, ', ');
1427
            $def .= ' )';
1428
        }
1429
1430
        return $def;
1431
    }
1432
1433
    /**
1434
     * Gets the MySQL Foreign Key Definition for an ForeignKey object.
1435
     *
1436
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1437
     * @return string
1438
     */
1439
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
1440
    {
1441
        $def = '';
1442
        if ($foreignKey->getConstraint()) {
1443
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
1444
        }
1445
        $columnNames = [];
1446
        foreach ($foreignKey->getColumns() as $column) {
1447
            $columnNames[] = $this->quoteColumnName($column);
1448
        }
1449
        $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1450
        $refColumnNames = [];
1451
        foreach ($foreignKey->getReferencedColumns() as $column) {
1452
            $refColumnNames[] = $this->quoteColumnName($column);
1453
        }
1454
        $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1455
        if ($foreignKey->getOnDelete()) {
1456
            $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1457
        }
1458
        if ($foreignKey->getOnUpdate()) {
1459
            $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1460
        }
1461
1462
        return $def;
1463
    }
1464
1465
    /**
1466
     * Describes a database table. This is a MySQL adapter specific method.
1467
     *
1468
     * @param string $tableName Table name
1469
     * @return array
1470
     */
1471
    public function describeTable($tableName)
1472
    {
1473
        $options = $this->getOptions();
1474
1475
        // mysql specific
1476
        $sql = sprintf(
1477
            "SELECT *
1478
             FROM information_schema.tables
1479
             WHERE table_schema = '%s'
1480
             AND table_name = '%s'",
1481
            $options['name'],
1482
            $tableName
1483
        );
1484
1485
        return $this->fetchRow($sql);
1486
    }
1487
1488
    /**
1489
     * Returns MySQL column types (inherited and MySQL specified).
1490
     *
1491
     * @return string[]
1492
     */
1493
    public function getColumnTypes()
1494
    {
1495
        return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1496
    }
1497
1498
    /**
1499
     * @inheritDoc
1500
     */
1501
    public function getDecoratedConnection()
1502
    {
1503
        $options = $this->getOptions();
1504
        $options = [
1505
            'username' => $options['user'] ?? null,
1506
            'password' => $options['pass'] ?? null,
1507
            'database' => $options['name'],
1508
            'quoteIdentifiers' => true,
1509
        ] + $options;
1510
1511
        $driver = new MysqlDriver($options);
1512
        $driver->setConnection($this->connection);
1513
1514
        return new Connection(['driver' => $driver] + $options);
1515
    }
1516
}
1517