Completed
Pull Request — master (#1910)
by B
01:23
created

MysqlAdapter::afterClause()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 9.9
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Db\Adapter;
9
10
use Cake\Database\Connection;
11
use Cake\Database\Driver\Mysql as MysqlDriver;
12
use InvalidArgumentException;
13
use PDO;
14
use Phinx\Db\Table\Column;
15
use Phinx\Db\Table\ForeignKey;
16
use Phinx\Db\Table\Index;
17
use Phinx\Db\Table\Table;
18
use Phinx\Db\Util\AlterInstructions;
19
use Phinx\Util\Literal;
20
use RuntimeException;
21
22
/**
23
 * Phinx MySQL Adapter.
24
 *
25
 * @author Rob Morgan <[email protected]>
26
 */
27
class MysqlAdapter extends PdoAdapter
28
{
29
    /**
30
     * @var string[]
31
     */
32
    protected static $specificColumnTypes = [
33
        self::PHINX_TYPE_ENUM,
34
        self::PHINX_TYPE_SET,
35
        self::PHINX_TYPE_YEAR,
36
        self::PHINX_TYPE_JSON,
37
        self::PHINX_TYPE_BINARYUUID,
38
        self::PHINX_TYPE_TINYBLOB,
39
        self::PHINX_TYPE_MEDIUMBLOB,
40
        self::PHINX_TYPE_LONGBLOB,
41
    ];
42
43
    /**
44
     * @var bool[]
45
     */
46
    protected $signedColumnTypes = [
47
        self::PHINX_TYPE_INTEGER => true,
48
        self::PHINX_TYPE_TINY_INTEGER => true,
49
        self::PHINX_TYPE_SMALL_INTEGER => true,
50
        self::PHINX_TYPE_BIG_INTEGER => true,
51
        self::PHINX_TYPE_FLOAT => true,
52
        self::PHINX_TYPE_DECIMAL => true,
53
        self::PHINX_TYPE_DOUBLE => true,
54
        self::PHINX_TYPE_BOOLEAN => true,
55
    ];
56
57
    public const TEXT_TINY = 255;
58
    public const TEXT_SMALL = 255; /* deprecated, alias of TEXT_TINY */
59
    public const TEXT_REGULAR = 65535;
60
    public const TEXT_MEDIUM = 16777215;
61
    public const TEXT_LONG = 4294967295;
62
63
    // According to https://dev.mysql.com/doc/refman/5.0/en/blob.html BLOB sizes are the same as TEXT
64
    public const BLOB_TINY = 255;
65
    public const BLOB_SMALL = 255; /* deprecated, alias of BLOB_TINY */
66
    public const BLOB_REGULAR = 65535;
67
    public const BLOB_MEDIUM = 16777215;
68
    public const BLOB_LONG = 4294967295;
69
70 80
    public const INT_TINY = 255;
71
    public const INT_SMALL = 65535;
72 80
    public const INT_MEDIUM = 16777215;
73 80
    public const INT_REGULAR = 4294967295;
74
    public const INT_BIG = 18446744073709551615;
75
76
    public const BIT = 64;
77
78
    public const TYPE_YEAR = 'year';
79 80
80 80
    public const FIRST = 'FIRST';
81
82 80
    /**
83
     * {@inheritDoc}
84 80
     *
85
     * @throws \RuntimeException
86
     * @throws \InvalidArgumentException
87
     *
88
     * @return void
89 80
     */
90 80
    public function connect()
91 80
    {
92 80
        if ($this->connection === null) {
93
            if (!class_exists('PDO') || !in_array('mysql', PDO::getAvailableDrivers(), true)) {
94
                // @codeCoverageIgnoreStart
95 80
                throw new RuntimeException('You need to enable the PDO_Mysql extension for Phinx to run properly.');
96
                // @codeCoverageIgnoreEnd
97
            }
98 80
99
            $options = $this->getOptions();
100
101
            $dsn = 'mysql:';
102 80
103
            if (!empty($options['unix_socket'])) {
104
                // use socket connection
105
                $dsn .= 'unix_socket=' . $options['unix_socket'];
106 80
            } else {
107 80
                // use network connection
108
                $dsn .= 'host=' . $options['host'];
109
                if (!empty($options['port'])) {
110 80
                    $dsn .= ';port=' . $options['port'];
111
                }
112
            }
113 80
114 80
            $dsn .= ';dbname=' . $options['name'];
115 1
116 1
            // charset support
117 1
            if (!empty($options['charset'])) {
118 1
                $dsn .= ';charset=' . $options['charset'];
119
            }
120
121 80
            $driverOptions = [];
122 80
123 80
            // use custom data fetch mode
124
            if (!empty($options['fetch_mode'])) {
125
                $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
126
            }
127
128 81
            // support arbitrary \PDO::MYSQL_ATTR_* driver options and pass them to PDO
129
            // http://php.net/manual/en/ref.pdo-mysql.php#pdo-mysql.constants
130 81
            foreach ($options as $key => $option) {
131 81
                if (strpos($key, 'mysql_attr_') === 0) {
132
                    $driverOptions[constant('\PDO::' . strtoupper($key))] = $option;
133
                }
134
            }
135
136 6
            $db = $this->createPdoConnection($dsn, $options['user'] ?? null, $options['pass'] ?? null, $driverOptions);
137
138 6
            $this->setConnection($db);
139
        }
140
    }
141
142
    /**
143
     * @inheritDoc
144 6
     */
145
    public function disconnect()
146 6
    {
147 6
        $this->connection = null;
148
    }
149
150
    /**
151
     * @inheritDoc
152 6
     */
153
    public function hasTransactions()
154 6
    {
155 6
        return true;
156
    }
157
158
    /**
159
     * @inheritDoc
160 1
     */
161
    public function beginTransaction()
162 1
    {
163 1
        $this->execute('START TRANSACTION');
164
    }
165
166
    /**
167
     * @inheritDoc
168 112
     */
169
    public function commitTransaction()
170 112
    {
171
        $this->execute('COMMIT');
172
    }
173
174
    /**
175
     * @inheritDoc
176 112
     */
177
    public function rollbackTransaction()
178 112
    {
179
        $this->execute('ROLLBACK');
180
    }
181
182
    /**
183
     * @inheritDoc
184 82
     */
185
    public function quoteTableName($tableName)
186 82
    {
187
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
188 82
    }
189
190
    /**
191 82
     * @inheritDoc
192 82
     */
193
    public function quoteColumnName($columnName)
194 82
    {
195
        return '`' . str_replace('`', '``', $columnName) . '`';
196 82
    }
197
198
    /**
199
     * @inheritDoc
200
     */
201
    public function hasTable($tableName)
202 82
    {
203
        if ($this->hasCreatedTable($tableName)) {
204
            return true;
205
        }
206 82
207
        if (strpos($tableName, '.') !== false) {
208 82
            [$schema, $table] = explode('.', $tableName);
0 ignored issues
show
Bug introduced by
The variable $schema does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $table does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
209 82
            $exists = $this->hasTableWithSchema($schema, $table);
210
            // Only break here on success, because it is possible for table names to contain a dot.
211
            if ($exists) {
212 82
                return true;
213 82
            }
214 68
        }
215 68
216 68
        $options = $this->getOptions();
217 68
218 68
        return $this->hasTableWithSchema($options['name'], $tableName);
219
    }
220 68
221 68
    /**
222 82
     * @param string $schema The table schema
223
     * @param string $tableName The table name
224 2
     *
225 2
     * @return bool
226 2
     */
227 2
    protected function hasTableWithSchema($schema, $tableName)
228
    {
229 2
        $result = $this->fetchRow(sprintf(
230 2
            "SELECT TABLE_NAME
231 2
            FROM INFORMATION_SCHEMA.TABLES
232
            WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'",
233
            $schema,
234
            $tableName
235
        ));
236 82
237 82
        return !empty($result);
238 82
    }
239 82
240
    /**
241
     * @inheritDoc
242 82
     */
243 82
    public function createTable(Table $table, array $columns = [], array $indexes = [])
244 82
    {
245 82
        // This method is based on the MySQL docs here: http://dev.mysql.com/doc/refman/5.1/en/create-index.html
246 82
        $defaultOptions = [
247
            'engine' => 'InnoDB',
248
            'collation' => 'utf8_general_ci',
249 82
        ];
250 2
251 2
        $options = array_merge(
252
            $defaultOptions,
253 82
            array_intersect_key($this->getOptions(), $defaultOptions),
254 82
            $table->getOptions()
255 82
        );
256 82
257 82
        // Add the default primary key
258
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
259
            $options['id'] = 'id';
260 82
        }
261 82
262 82
        if (isset($options['id']) && is_string($options['id'])) {
263 82
            // Handle id => "field_name" to support AUTO_INCREMENT
264 81
            $column = new Column();
265 82
            $column->setName($options['id'])
266
                   ->setType('integer')
267
                   ->setSigned(isset($options['signed']) ? $options['signed'] : true)
268 2
                   ->setIdentity(true);
269 2
270 2
            array_unshift($columns, $column);
271 2
            if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) {
272 2
                throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key');
273 2
            }
274 2
            $options['primary_key'] = $options['id'];
275 2
        }
276 2
277 2
        // open: process table options like collation etc
278 82
279 82
        // process table engine (default to InnoDB)
280 1
        $optionsStr = 'ENGINE = InnoDB';
281
        if (isset($options['engine'])) {
282
            $optionsStr = sprintf('ENGINE = %s', $options['engine']);
283
        }
284 82
285 82
        // process table collation
286 10
        if (isset($options['collation'])) {
287 82
            $charset = explode('_', $options['collation']);
288
            $optionsStr .= sprintf(' CHARACTER SET %s', $charset[0]);
289
            $optionsStr .= sprintf(' COLLATE %s', $options['collation']);
290 82
        }
291 82
292 2
        // set the table comment
293 82
        if (isset($options['comment'])) {
294
            $optionsStr .= sprintf(' COMMENT=%s ', $this->getConnection()->quote($options['comment']));
295 82
        }
296 82
297
        // set the table row format
298
        if (isset($options['row_format'])) {
299 82
            $optionsStr .= sprintf(' ROW_FORMAT=%s ', $options['row_format']);
300 82
        }
301
302
        $sql = 'CREATE TABLE ';
303
        $sql .= $this->quoteTableName($table->getName()) . ' (';
304
        foreach ($columns as $column) {
305 5
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
306
        }
307 5
308 5
        // set the primary key(s)
309
        if (isset($options['primary_key'])) {
310
            $sql = rtrim($sql);
311
            $sql .= ' PRIMARY KEY (';
312
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
313 5
                $sql .= $this->quoteColumnName($options['primary_key']);
314
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
315 5
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
316 5
            }
317
            $sql .= ')';
318
        } else {
319
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
320
        }
321 1
322
        // set the indexes
323 1
        foreach ($indexes as $index) {
324 1
            $sql .= ', ' . $this->getIndexSqlDefinition($index);
325 1
        }
326 1
327
        $sql .= ') ' . $optionsStr;
328 1
        $sql = rtrim($sql);
329 1
330
        // execute the sql
331
        $this->execute($sql);
332
333
        $this->addCreatedTable($table->getName());
334 12
    }
335
336 12
    /**
337 12
     * {@inheritDoc}
338 12
     *
339 12
     * @throws \InvalidArgumentException
340
     */
341 12
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
342 12
    {
343 12
        $instructions = new AlterInstructions();
344 12
345 12
        // Drop the existing primary key
346 12
        $primaryKey = $this->getPrimaryKey($table->getName());
347
        if (!empty($primaryKey['columns'])) {
348 12
            $instructions->addAlter('DROP PRIMARY KEY');
349 12
        }
350 12
351
        // Add the primary key(s)
352 12
        if (!empty($newColumns)) {
353 3
            $sql = 'ADD PRIMARY KEY (';
354 3
            if (is_string($newColumns)) { // handle primary_key => 'id'
355
                $sql .= $this->quoteColumnName($newColumns);
356 12
            } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id')
357 12
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns));
358
            } else {
359 12
                throw new InvalidArgumentException(sprintf(
360
                    'Invalid value for primary key: %s',
361
                    json_encode($newColumns)
362
                ));
363
            }
364
            $sql .= ')';
365 79
            $instructions->addAlter($sql);
366
        }
367 79
368 79
        return $instructions;
369 79
    }
370 77
371
    /**
372 77
     * @inheritDoc
373
     */
374 21
    protected function getChangeCommentInstructions(Table $table, $newComment)
375
    {
376
        $instructions = new AlterInstructions();
377
378
        // passing 'null' is to remove table comment
379
        $newComment = ($newComment !== null)
380
            ? $newComment
381
            : '';
382
        $sql = sprintf(' COMMENT=%s ', $this->getConnection()->quote($newComment));
383 95
        $instructions->addAlter($sql);
384
385 95
        return $instructions;
386 10
    }
387 95
388 79
    /**
389 79
     * @inheritDoc
390 95
     */
391
    protected function getRenameTableInstructions($tableName, $newTableName)
392
    {
393
        $this->updateCreatedTableName($tableName, $newTableName);
394
        $sql = sprintf(
395
            'RENAME TABLE %s TO %s',
396 18
            $this->quoteTableName($tableName),
397
            $this->quoteTableName($newTableName)
398 18
        );
399 18
400 18
        return new AlterInstructions([], [$sql]);
401 18
    }
402 18
403 18
    /**
404
     * @inheritDoc
405 18
     */
406 2
    protected function getDropTableInstructions($tableName)
407 2
    {
408
        $this->removeCreatedTable($tableName);
409 18
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
410 18
411
        return new AlterInstructions([], [$sql]);
412
    }
413
414
    /**
415 7
     * @inheritDoc
416
     */
417 7
    public function truncateTable($tableName)
418 7
    {
419 7
        $sql = sprintf(
420 5
            'TRUNCATE TABLE %s',
421 5
            $this->quoteTableName($tableName)
422 5
        );
423 1
424 1
        $this->execute($sql);
425 5
    }
426
427 5
    /**
428 5
     * @inheritDoc
429 5
     */
430 5
    public function getColumns($tableName)
431 5
    {
432 5
        $columns = [];
433
        $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName)));
434 5
        foreach ($rows as $columnInfo) {
435 5
            $phinxType = $this->getPhinxType($columnInfo['Type']);
436 5
437
            $column = new Column();
438 6
            $column->setName($columnInfo['Field'])
439
                   ->setNull($columnInfo['Null'] !== 'NO')
440 2
                   ->setDefault($columnInfo['Default'])
441
                   ->setType($phinxType['name'])
442
                   ->setSigned(strpos($columnInfo['Type'], 'unsigned') === false)
443 2
                   ->setLimit($phinxType['limit'])
444
                   ->setScale($phinxType['scale']);
445
446
            if ($columnInfo['Extra'] === 'auto_increment') {
447
                $column->setIdentity(true);
448
            }
449 5
450
            if (isset($phinxType['values'])) {
451 5
                $column->setValues($phinxType['values']);
452 5
            }
453 5
454 5
            $columns[] = $column;
455 5
        }
456 5
457 5
        return $columns;
458 5
    }
459
460 5
    /**
461 5
     * @inheritDoc
462 5
     */
463
    public function hasColumn($tableName, $columnName)
464
    {
465
        $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName)));
466
        foreach ($rows as $column) {
467 5
            if (strcasecmp($column['Field'], $columnName) === 0) {
468
                return true;
469 5
            }
470 5
        }
471 5
472 5
        return false;
473 5
    }
474 5
475 5
    /**
476 5
     * @inheritDoc
477
     */
478
    protected function getAddColumnInstructions(Table $table, Column $column)
479
    {
480
        $alter = sprintf(
481
            'ADD %s %s',
482
            $this->quoteColumnName($column->getName()),
483
            $this->getColumnSqlDefinition($column)
484 19
        );
485
486 19
        $alter .= $this->afterClause($column);
487 19
488 19
        return new AlterInstructions([$alter]);
489 18
    }
490 18
491 18
    /**
492 18
     * Exposes the MySQL syntax to arrange a column `FIRST`.
493 19
     *
494 19
     * @param Column $column The column being altered.
495
     *
496
     * @return string The appropriate SQL fragment.
497
     */
498
    protected function afterClause(Column $column)
499
    {
500 14
        $after = $column->getAfter();
501
        if (empty($after)) {
502 14
            return '';
503 6
        } elseif ($after === self::FIRST) {
504 6
            return ' FIRST';
505
        } else {
506 14
            return ' AFTER ' . $this->quoteColumnName($after);
507 14
        }
508
    }
509 14
510 14
    /**
511 12
     * {@inheritDoc}
512
     *
513 13
     * @throws \InvalidArgumentException
514
     */
515 11
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
516
    {
517
        $rows = $this->fetchAll(sprintf('SHOW FULL COLUMNS FROM %s', $this->quoteTableName($tableName)));
518
519
        foreach ($rows as $row) {
520
            if (strcasecmp($row['Field'], $columnName) === 0) {
521 1
                $null = ($row['Null'] === 'NO') ? 'NOT NULL' : 'NULL';
522
                $comment = isset($row['Comment']) ? ' COMMENT ' . '\'' . addslashes($row['Comment']) . '\'' : '';
523 1
                $extra = ' ' . strtoupper($row['Extra']);
524
                if (($row['Default'] !== null)) {
525 1
                    $extra .= $this->getDefaultValueDefinition($row['Default']);
526 1
                }
527 1
                $definition = $row['Type'] . ' ' . $null . $extra . $comment;
528
529 1
                $alter = sprintf(
530
                    'CHANGE COLUMN %s %s %s',
531
                    $this->quoteColumnName($columnName),
532
                    $this->quoteColumnName($newColumnName),
533
                    $definition
534
                );
535
536
                return new AlterInstructions([$alter]);
537 4
            }
538
        }
539 4
540 4
        throw new InvalidArgumentException(sprintf(
541 4
            "The specified column doesn't exist: " .
542 4
            $columnName
543 4
        ));
544 4
    }
545 4
546 4
    /**
547
     * @inheritDoc
548
     */
549
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
550
    {
551 3
        $alter = sprintf(
552
            'CHANGE %s %s %s%s',
553 3
            $this->quoteColumnName($columnName),
554 2
            $this->quoteColumnName($newColumn->getName()),
555 2
            $this->getColumnSqlDefinition($newColumn),
556
            $this->afterClause($newColumn)
557 3
        );
558 3
559
        return new AlterInstructions([$alter]);
560 3
    }
561 3
562 3
    /**
563 3
     * @inheritDoc
564 3
     */
565 3
    protected function getDropColumnInstructions($tableName, $columnName)
566 3
    {
567 3
        $alter = sprintf('DROP COLUMN %s', $this->quoteColumnName($columnName));
568 3
569 3
        return new AlterInstructions([$alter]);
570
    }
571 3
572 1
    /**
573
     * Get an array of indexes from a particular table.
574
     *
575
     * @param string $tableName Table name
576
     *
577 2
     * @return array
578
     */
579 2
    protected function getIndexes($tableName)
580
    {
581 2
        $indexes = [];
582
        $rows = $this->fetchAll(sprintf('SHOW INDEXES FROM %s', $this->quoteTableName($tableName)));
583 2
        foreach ($rows as $row) {
584 2
            if (!isset($indexes[$row['Key_name']])) {
585 2
                $indexes[$row['Key_name']] = ['columns' => []];
586 2
            }
587 2
            $indexes[$row['Key_name']]['columns'][] = strtolower($row['Column_name']);
588 2
        }
589 2
590 2
        return $indexes;
591 2
    }
592
593 2
    /**
594
     * @inheritDoc
595
     */
596
    public function hasIndex($tableName, $columns)
597
    {
598
        if (is_string($columns)) {
599 21
            $columns = [$columns]; // str to array
600
        }
601 21
602 5
        $columns = array_map('strtolower', $columns);
603 5
        $indexes = $this->getIndexes($tableName);
604 21
605 21
        foreach ($indexes as $index) {
606 6
            if ($columns == $index['columns']) {
607 4
                return true;
608
            }
609 4
        }
610
611 15
        return false;
612 12
    }
613 10
614
    /**
615 11
     * @inheritDoc
616 11
     */
617
    public function hasIndexByName($tableName, $indexName)
618
    {
619
        $indexes = $this->getIndexes($tableName);
620
621
        foreach ($indexes as $name => $index) {
622
            if ($name === $indexName) {
623
                return true;
624
            }
625
        }
626 22
627
        return false;
628 22
    }
629 22
630
    /**
631
     * @inheritDoc
632
     */
633
    protected function getAddIndexInstructions(Table $table, Index $index)
634
    {
635
        $instructions = new AlterInstructions();
636
637
        if ($index->getType() === Index::FULLTEXT) {
638
            // Must be executed separately
639
            // SQLSTATE[HY000]: General error: 1795 InnoDB presently supports one FULLTEXT index creation at a time
640 22
            $alter = sprintf(
641
                'ALTER TABLE %s ADD %s',
642 22
                $this->quoteTableName($table->getName()),
643 22
                $this->getIndexSqlDefinition($index)
644 19
            );
645 19
646 19
            $instructions->addPostStep($alter);
647 19
        } else {
648 22
            $alter = sprintf(
649 22
                'ADD %s',
650
                $this->getIndexSqlDefinition($index)
651
            );
652
653
            $instructions->addAlter($alter);
654
        }
655 15
656
        return $instructions;
657 15
    }
658 15
659 15
    /**
660 15
     * {@inheritDoc}
661 15
     *
662 15
     * @throws \InvalidArgumentException
663 15
     */
664 15
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
665
    {
666
        if (is_string($columns)) {
667
            $columns = [$columns]; // str to array
668
        }
669 8
670
        $indexes = $this->getIndexes($tableName);
671 8
        $columns = array_map('strtolower', $columns);
672 3
673 3
        foreach ($indexes as $indexName => $index) {
674
            if ($columns == $index['columns']) {
675
                return new AlterInstructions([sprintf(
676 8
                    'DROP INDEX %s',
677 8
                    $this->quoteColumnName($indexName)
678 8
                )]);
679 8
            }
680 8
        }
681
682 8
        throw new InvalidArgumentException(sprintf(
683 8
            "The specified index on columns '%s' does not exist",
684 8
            implode(',', $columns)
685
        ));
686 7
    }
687 7
688
    /**
689
     * {@inheritDoc}
690
     *
691
     * @throws \InvalidArgumentException
692
     */
693
    protected function getDropIndexByNameInstructions($tableName, $indexName)
694
    {
695 7
        $indexes = $this->getIndexes($tableName);
696 7
697
        foreach ($indexes as $name => $index) {
698 7
            if ($name === $indexName) {
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 name '%s' does not exist",
708
            $indexName
709 96
        ));
710
    }
711
712 96
    /**
713 87
     * @inheritDoc
714
     */
715 96
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
716 4
    {
717
        $primaryKey = $this->getPrimaryKey($tableName);
718 96
719 9
        if (empty($primaryKey['constraint'])) {
720
            return false;
721
        }
722 6
723 6
        if ($constraint) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constraint of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
724 6
            return ($primaryKey['constraint'] === $constraint);
725 6
        } else {
726 6
            if (is_string($columns)) {
727 6
                $columns = [$columns]; // str to array
728 6
            }
729 6
            $missingColumns = array_diff($columns, $primaryKey['columns']);
730
731 5
            return empty($missingColumns);
732
        }
733 5
    }
734
735 95
    /**
736 5
     * Get the primary key from a particular table.
737
     *
738 95
     * @param string $tableName Table name
739 3
     *
740
     * @return array
741 95
     */
742 1
    public function getPrimaryKey($tableName)
743
    {
744
        $options = $this->getOptions();
745 1
        $rows = $this->fetchAll(sprintf(
746 1
            "SELECT
747 1
                k.CONSTRAINT_NAME,
748 1
                k.COLUMN_NAME
749 1
            FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS t
750 1
            JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
751 1
                USING(CONSTRAINT_NAME,TABLE_NAME)
752 1
            WHERE t.CONSTRAINT_TYPE='PRIMARY KEY'
753
                AND t.TABLE_SCHEMA='%s'
754 1
                AND t.TABLE_NAME='%s'",
755
            $options['name'],
756 1
            $tableName
757
        ));
758 95
759 82
        $primaryKey = [
760
            'columns' => [],
761
        ];
762 6
        foreach ($rows as $row) {
763 6
            $primaryKey['constraint'] = $row['CONSTRAINT_NAME'];
764 6
            $primaryKey['columns'][] = $row['COLUMN_NAME'];
765 6
        }
766 6
767 6
        return $primaryKey;
768
    }
769 6
770 6
    /**
771 6
     * @inheritDoc
772 6
     */
773 6
    public function hasForeignKey($tableName, $columns, $constraint = null)
774 6
    {
775 6
        if (is_string($columns)) {
776 2
            $columns = [$columns]; // str to array
777 2
        }
778 6
        $foreignKeys = $this->getForeignKeys($tableName);
779
        if ($constraint) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constraint of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
780 5
            if (isset($foreignKeys[$constraint])) {
781 82
                return !empty($foreignKeys[$constraint]);
782 76
            }
783 76
784 82
            return false;
785
        }
786 86
787 82
        foreach ($foreignKeys as $key) {
788
            if ($columns == $key['columns']) {
789 86
                return true;
790 7
            }
791
        }
792 84
793 5
        return false;
794
    }
795 83
796 7
    /**
797
     * Get an array of foreign keys from a particular table.
798 83
     *
799 80
     * @param string $tableName Table name
800
     *
801 83
     * @return array
802 4
     */
803
    protected function getForeignKeys($tableName)
804 83
    {
805 4
        if (strpos($tableName, '.') !== false) {
806
            [$schema, $tableName] = explode('.', $tableName);
0 ignored issues
show
Bug introduced by
The variable $schema does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
807 83
        }
808 80
809
        $foreignKeys = [];
810 10
        $rows = $this->fetchAll(sprintf(
811 2
            "SELECT
812
              CONSTRAINT_NAME,
813 10
              CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS TABLE_NAME,
814 10
              COLUMN_NAME,
815 10
              CONCAT(REFERENCED_TABLE_SCHEMA, '.', REFERENCED_TABLE_NAME) AS REFERENCED_TABLE_NAME,
816 10
              REFERENCED_COLUMN_NAME
817 5
            FROM information_schema.KEY_COLUMN_USAGE
818 8
            WHERE REFERENCED_TABLE_NAME IS NOT NULL
819 5
              AND TABLE_SCHEMA = %s
820
              AND TABLE_NAME = '%s'
821 6
            ORDER BY POSITION_IN_UNIQUE_CONSTRAINT",
822 4
            empty($schema) ? 'DATABASE()' : "'$schema'",
823
            $tableName
824 2
        ));
825
        foreach ($rows as $row) {
826
            $foreignKeys[$row['CONSTRAINT_NAME']]['table'] = $row['TABLE_NAME'];
827
            $foreignKeys[$row['CONSTRAINT_NAME']]['columns'][] = $row['COLUMN_NAME'];
828
            $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_table'] = $row['REFERENCED_TABLE_NAME'];
829
            $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME'];
830 2
        }
831
832
        return $foreignKeys;
833 2
    }
834 2
835 2
    /**
836
     * @inheritDoc
837
     */
838
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
839
    {
840
        $alter = sprintf(
841
            'ADD %s',
842
            $this->getForeignKeySqlDefinition($foreignKey)
843
        );
844
845
        return new AlterInstructions([$alter]);
846 17
    }
847
848 17
    /**
849 17
     * @inheritDoc
850 1
     */
851
    protected function getDropForeignKeyInstructions($tableName, $constraint)
852 16
    {
853 16
        $alter = sprintf(
854 16
            'DROP FOREIGN KEY %s',
855 16
            $constraint
856 14
        );
857 14
858 16
        return new AlterInstructions([$alter]);
859 4
    }
860 4
861 16
    /**
862 3
     * {@inheritDoc}
863 3
     *
864 3
     * @throws \InvalidArgumentException
865
     */
866 16
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
867 6
    {
868 6
        $instructions = new AlterInstructions();
869 3
870 3
        foreach ($columns as $column) {
871 6
            $rows = $this->fetchAll(sprintf(
872 16
                "SELECT
873 5
                    CONSTRAINT_NAME
874 5
                  FROM information_schema.KEY_COLUMN_USAGE
875 1
                  WHERE REFERENCED_TABLE_SCHEMA = DATABASE()
876 1
                    AND REFERENCED_TABLE_NAME IS NOT NULL
877 5
                    AND TABLE_NAME = '%s'
878 2
                    AND COLUMN_NAME = '%s'
879 2
                  ORDER BY POSITION_IN_UNIQUE_CONSTRAINT",
880 5
                $tableName,
881 16
                $column
882 2
            ));
883 2
884 2
            foreach ($rows as $row) {
885 16
                $instructions->merge($this->getDropForeignKeyInstructions($tableName, $row['CONSTRAINT_NAME']));
886 2
            }
887 2
        }
888 2
889 16
        if (empty($instructions->getAlterParts())) {
890 2
            throw new InvalidArgumentException(sprintf(
891 2
                "Not foreign key on columns '%s' exist",
892 2
                implode(',', $columns)
893 16
            ));
894 15
        }
895 15
896 12
        return $instructions;
897 12
    }
898 15
899 11
    /**
900 6
     * {@inheritDoc}
901 4
     *
902 4
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
903 6
     */
904 6
    public function getSqlType($type, $limit = null)
905 10
    {
906 2
        switch ($type) {
907 2
            case static::PHINX_TYPE_FLOAT:
908 10
            case static::PHINX_TYPE_DOUBLE:
909 1
            case static::PHINX_TYPE_DECIMAL:
910 1
            case static::PHINX_TYPE_DATE:
911 1
            case static::PHINX_TYPE_ENUM:
912 10
            case static::PHINX_TYPE_SET:
913 1
            case static::PHINX_TYPE_JSON:
914 1
            // Geospatial database types
915 1
            case static::PHINX_TYPE_GEOMETRY:
916 10
            case static::PHINX_TYPE_POINT:
917 1
            case static::PHINX_TYPE_LINESTRING:
918 1
            case static::PHINX_TYPE_POLYGON:
919 1
                return ['name' => $type];
920 10
            case static::PHINX_TYPE_DATETIME:
921 2
            case static::PHINX_TYPE_TIMESTAMP:
922 2
            case static::PHINX_TYPE_TIME:
923 2
                return ['name' => $type, 'limit' => $limit];
924 9
            case static::PHINX_TYPE_STRING:
925 2
                return ['name' => 'varchar', 'limit' => $limit ?: 255];
926 2
            case static::PHINX_TYPE_CHAR:
927 2
                return ['name' => 'char', 'limit' => $limit ?: 255];
928 8
            case static::PHINX_TYPE_TEXT:
929 2
                if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
930 2
                    $sizes = [
931 2
                        // Order matters! Size must always be tested from longest to shortest!
932
                        'longtext' => static::TEXT_LONG,
933
                        'mediumtext' => static::TEXT_MEDIUM,
934
                        'text' => static::TEXT_REGULAR,
935 16
                        'tinytext' => static::TEXT_SMALL,
936
                    ];
937
                    foreach ($sizes as $name => $length) {
938 15
                        if ($limit >= $length) {
939 15
                            return ['name' => $name];
940
                        }
941 15
                    }
942
                }
943 15
944 3
                return ['name' => 'text'];
945 3
            case static::PHINX_TYPE_BINARY:
946
                if ($limit === null) {
947 15
                    $limit = 255;
948
                }
949
950
                if ($limit > 255) {
951
                    return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit);
952
                }
953
954 83
                return ['name' => 'binary', 'limit' => $limit];
955
            case static::PHINX_TYPE_BINARYUUID:
956 83
                return ['name' => 'binary', 'limit' => 16];
957
            case static::PHINX_TYPE_VARBINARY:
958 83
                if ($limit === null) {
959 1
                    $limit = 255;
960 1
                }
961 82
962
                if ($limit > 255) {
963 83
                    return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit);
964
                }
965
966
                return ['name' => 'varbinary', 'limit' => $limit];
967
            case static::PHINX_TYPE_BLOB:
968 4
                if ($limit !== null) {
969
                    // Rework this part as the choosen types were always UNDER the required length
970 4
                    $sizes = [
971 4
                        'tinyblob' => static::BLOB_SMALL,
972 4
                        'blob' => static::BLOB_REGULAR,
973
                        'mediumblob' => static::BLOB_MEDIUM,
974 4
                    ];
975 4
976
                    foreach ($sizes as $name => $length) {
977 4
                        if ($limit <= $length) {
978 3
                            return ['name' => $name];
979 3
                        }
980
                    }
981 3
982
                    // For more length requirement, the longblob is used
983 3
                    return ['name' => 'longblob'];
984
                }
985
986
                // If not limit is provided, fallback on blob
987
                return ['name' => 'blob'];
988
            case static::PHINX_TYPE_TINYBLOB:
989 81
                // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit
990
                return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_TINY);
991 81
            case static::PHINX_TYPE_MEDIUMBLOB:
992 81
                // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit
993
                return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_MEDIUM);
994
            case static::PHINX_TYPE_LONGBLOB:
995
                // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit
996
                return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_LONG);
997
            case static::PHINX_TYPE_BIT:
998
                return ['name' => 'bit', 'limit' => $limit ?: 64];
999
            case static::PHINX_TYPE_SMALL_INTEGER:
1000 89
                return ['name' => 'smallint', 'limit' => $limit ?: 6];
1001
            case static::PHINX_TYPE_TINY_INTEGER:
1002 89
                return ['name' => 'tinyint', 'limit' => $limit ?: 4];
1003
            case static::PHINX_TYPE_INTEGER:
1004 89
                if ($limit && $limit >= static::INT_TINY) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1005 89
                    $sizes = [
1006 89
                        // Order matters! Size must always be tested from longest to shortest!
1007 2
                        'bigint' => static::INT_BIG,
1008 89
                        'int' => static::INT_REGULAR,
1009 86
                        'mediumint' => static::INT_MEDIUM,
1010 86
                        'smallint' => static::INT_SMALL,
1011 89
                        'tinyint' => static::INT_TINY,
1012 5
                    ];
1013 5
                    $limits = [
1014 89
                        'tinyint' => 4,
1015 89
                        'smallint' => 6,
1016 89
                        'int' => 11,
1017 89
                        'bigint' => 20,
1018 89
                    ];
1019 89
                    foreach ($sizes as $name => $length) {
1020
                        if ($limit >= $length) {
1021 89
                            $def = ['name' => $name];
1022 2
                            if (isset($limits[$name])) {
1023 2
                                $def['limit'] = $limits[$name];
1024
                            }
1025 89
1026 1
                            return $def;
1027 1
                        }
1028
                    }
1029 89
                } elseif (!$limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1030
                    $limit = 11;
1031
                }
1032
1033
                return ['name' => 'int', 'limit' => $limit];
1034
            case static::PHINX_TYPE_BIG_INTEGER:
1035
                return ['name' => 'bigint', 'limit' => $limit ?: 20];
1036
            case static::PHINX_TYPE_BOOLEAN:
1037
                return ['name' => 'tinyint', 'limit' => 1];
1038 16
            case static::PHINX_TYPE_UUID:
1039
                return ['name' => 'char', 'limit' => 36];
1040 16
            case static::PHINX_TYPE_YEAR:
1041 16
                if (!$limit || in_array($limit, [2, 4])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1042 16
                    $limit = 4;
1043 2
                }
1044 2
1045
                return ['name' => 'year', 'limit' => $limit];
1046 16
            default:
1047 5
                throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by MySQL.');
1048 5
        }
1049
    }
1050 16
1051 1
    /**
1052 1
     * Returns Phinx type by SQL type
1053
     *
1054 16
     * @internal param string $sqlType SQL type
1055
     *
1056 16
     * @param string $sqlTypeDef SQL Type definition
1057 5
     *
1058 5
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1059
     *
1060 16
     * @return array Phinx type
1061
     */
1062 16
    public function getPhinxType($sqlTypeDef)
1063
    {
1064
        $matches = [];
1065
        if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*(.+)*$/', $sqlTypeDef, $matches)) {
1066
            throw new UnsupportedColumnTypeException('Column type "' . $sqlTypeDef . '" is not supported by MySQL.');
1067
        }
1068
1069
        $limit = null;
1070
        $scale = null;
1071 17
        $type = $matches[1];
1072
        if (count($matches) > 2) {
1073 17
            $limit = $matches[3] ? (int)$matches[3] : null;
1074 17
        }
1075 5
        if (count($matches) > 4) {
1076 5
            $scale = (int)$matches[5];
1077 17
        }
1078 17
        if ($type === 'tinyint' && $limit === 1) {
1079 17
            $type = static::PHINX_TYPE_BOOLEAN;
1080 17
            $limit = null;
1081 17
        }
1082 17
        switch ($type) {
1083 17
            case 'varchar':
1084 17
                $type = static::PHINX_TYPE_STRING;
1085 17
                if ($limit === 255) {
1086 17
                    $limit = null;
1087 17
                }
1088 2
                break;
1089 2
            case 'char':
1090 17
                $type = static::PHINX_TYPE_CHAR;
1091 2
                if ($limit === 255) {
1092 2
                    $limit = null;
1093 17
                }
1094
                if ($limit === 36) {
1095
                    $type = static::PHINX_TYPE_UUID;
1096
                }
1097
                break;
1098
            case 'tinyint':
1099
                $type = static::PHINX_TYPE_TINY_INTEGER;
1100
                $limit = static::INT_TINY;
1101
                break;
1102 2
            case 'smallint':
1103
                $type = static::PHINX_TYPE_SMALL_INTEGER;
1104 2
                $limit = static::INT_SMALL;
1105
                break;
1106
            case 'mediumint':
1107 2
                $type = static::PHINX_TYPE_INTEGER;
1108
                $limit = static::INT_MEDIUM;
1109
                break;
1110
            case 'int':
1111 2
                $type = static::PHINX_TYPE_INTEGER;
1112 2
                if ($limit === 11) {
1113
                    $limit = null;
1114 2
                }
1115
                break;
1116 2
            case 'bigint':
1117
                if ($limit === 20) {
1118
                    $limit = null;
1119
                }
1120
                $type = static::PHINX_TYPE_BIG_INTEGER;
1121
                break;
1122
            case 'bit':
1123 85
                $type = static::PHINX_TYPE_BIT;
1124
                if ($limit === 64) {
1125 85
                    $limit = null;
1126
                }
1127
                break;
1128
            case 'blob':
1129
                $type = static::PHINX_TYPE_BLOB;
1130
                $limit = static::BLOB_REGULAR;
1131
                break;
1132
            case 'tinyblob':
1133
                $type = static::PHINX_TYPE_TINYBLOB;
1134
                $limit = static::BLOB_TINY;
1135
                break;
1136
            case 'mediumblob':
1137
                $type = static::PHINX_TYPE_MEDIUMBLOB;
1138
                $limit = static::BLOB_MEDIUM;
1139
                break;
1140
            case 'longblob':
1141
                $type = static::PHINX_TYPE_LONGBLOB;
1142
                $limit = static::BLOB_LONG;
1143
                break;
1144
            case 'tinytext':
1145
                $type = static::PHINX_TYPE_TEXT;
1146
                $limit = static::TEXT_TINY;
1147
                break;
1148
            case 'mediumtext':
1149
                $type = static::PHINX_TYPE_TEXT;
1150
                $limit = static::TEXT_MEDIUM;
1151
                break;
1152
            case 'longtext':
1153
                $type = static::PHINX_TYPE_TEXT;
1154
                $limit = static::TEXT_LONG;
1155
                break;
1156
            case 'binary':
1157
                if ($limit === null) {
1158
                    $limit = 255;
1159
                }
1160
1161
                if ($limit > 255) {
1162
                    $type = static::PHINX_TYPE_BLOB;
1163
                    break;
1164
                }
1165
1166
                if ($limit === 16) {
1167
                    $type = static::PHINX_TYPE_BINARYUUID;
1168
                }
1169
                break;
1170
        }
1171
1172
        try {
1173
            // Call this to check if parsed type is supported.
1174
            $this->getSqlType($type, $limit);
1175
        } catch (UnsupportedColumnTypeException $e) {
1176
            $type = Literal::from($type);
1177
        }
1178
1179
        $phinxType = [
1180
            'name' => $type,
1181
            'limit' => $limit,
1182
            'scale' => $scale,
1183
        ];
1184
1185
        if ($type === static::PHINX_TYPE_ENUM || $type === static::PHINX_TYPE_SET) {
1186
            $values = trim($matches[6], "()");
1187
            $phinxType['values'] = [];
1188
            $opened = false;
1189
            $escaped = false;
1190
            $wasEscaped = false;
1191
            $value = '';
1192
            $valuesLength = strlen($values);
1193
            for ($i = 0; $i < $valuesLength; $i++) {
1194
                $char = $values[$i];
1195
                if ($char === "'" && !$opened) {
1196
                    $opened = true;
1197
                } elseif (
1198
                    !$escaped
1199
                    && ($i + 1) < $valuesLength
1200
                    && (
1201
                        $char === "'" && $values[$i + 1] === "'"
1202
                        || $char === "\\" && $values[$i + 1] === "\\"
1203
                    )
1204
                ) {
1205
                    $escaped = true;
1206
                } elseif ($char === "'" && $opened && !$escaped) {
1207
                    $phinxType['values'][] = $value;
1208
                    $value = '';
1209
                    $opened = false;
1210
                } elseif (($char === "'" || $char === "\\") && $opened && $escaped) {
1211
                    $value .= $char;
1212
                    $escaped = false;
1213
                    $wasEscaped = true;
1214
                } elseif ($opened) {
1215
                    if ($values[$i - 1] === "\\" && !$wasEscaped) {
1216
                        if ($char === 'n') {
1217
                            $char = "\n";
1218
                        } elseif ($char === 'r') {
1219
                            $char = "\r";
1220
                        } elseif ($char === 't') {
1221
                            $char = "\t";
1222
                        }
1223
                        if ($values[$i] !== $char) {
1224
                            $value = substr($value, 0, strlen($value) - 1);
1225
                        }
1226
                    }
1227
                    $value .= $char;
1228
                    $wasEscaped = false;
1229
                }
1230
            }
1231
        }
1232
1233
        return $phinxType;
1234
    }
1235
1236
    /**
1237
     * @inheritDoc
1238
     */
1239
    public function createDatabase($name, $options = [])
1240
    {
1241
        $charset = $options['charset'] ?? 'utf8';
1242
1243
        if (isset($options['collation'])) {
1244
            $this->execute(sprintf(
1245
                'CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s` COLLATE `%s`',
1246
                $name,
1247
                $charset,
1248
                $options['collation']
1249
            ));
1250
        } else {
1251
            $this->execute(sprintf('CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s`', $name, $charset));
1252
        }
1253
    }
1254
1255
    /**
1256
     * @inheritDoc
1257
     */
1258
    public function hasDatabase($name)
1259
    {
1260
        $rows = $this->fetchAll(
1261
            sprintf(
1262
                'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = \'%s\'',
1263
                $name
1264
            )
1265
        );
1266
1267
        foreach ($rows as $row) {
1268
            if (!empty($row)) {
1269
                return true;
1270
            }
1271
        }
1272
1273
        return false;
1274
    }
1275
1276
    /**
1277
     * @inheritDoc
1278
     */
1279
    public function dropDatabase($name)
1280
    {
1281
        $this->execute(sprintf('DROP DATABASE IF EXISTS `%s`', $name));
1282
        $this->createdTables = [];
1283
    }
1284
1285
    /**
1286
     * Gets the MySQL Column Definition for a Column object.
1287
     *
1288
     * @param \Phinx\Db\Table\Column $column Column
1289
     *
1290
     * @return string
1291
     */
1292
    protected function getColumnSqlDefinition(Column $column)
1293
    {
1294
        if ($column->getType() instanceof Literal) {
1295
            $def = (string)$column->getType();
1296
        } else {
1297
            $sqlType = $this->getSqlType($column->getType(), $column->getLimit());
1298
            $def = strtoupper($sqlType['name']);
1299
        }
1300
        if ($column->getPrecision() && $column->getScale()) {
1301
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1302
        } elseif (isset($sqlType['limit'])) {
1303
            $def .= '(' . $sqlType['limit'] . ')';
1304
        }
1305
        if (($values = $column->getValues()) && is_array($values)) {
1306
            $def .= "(" . implode(", ", array_map(function ($value) {
1307
                // we special case NULL as it's not actually allowed an enum value,
1308
                // and we want MySQL to issue an error on the create statement, but
1309
                // quote coerces it to an empty string, which will not error
1310
                return $value === null ? 'NULL' : $this->getConnection()->quote($value);
1311
            }, $values)) . ")";
1312
        }
1313
1314
        $def .= $column->getEncoding() ? ' CHARACTER SET ' . $column->getEncoding() : '';
1315
        $def .= $column->getCollation() ? ' COLLATE ' . $column->getCollation() : '';
1316
        $def .= !$column->isSigned() && isset($this->signedColumnTypes[$column->getType()]) ? ' unsigned' : '';
1317
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1318
1319
        if (
1320
            version_compare($this->getAttribute(\PDO::ATTR_SERVER_VERSION), '8') > -1
1321
            && in_array($column->getType(), [
1322
                static::PHINX_TYPE_GEOMETRY,
1323
                static::PHINX_TYPE_POINT,
1324
                static::PHINX_TYPE_LINESTRING,
1325
                static::PHINX_TYPE_POLYGON,
1326
            ])
1327
            && !is_null($column->getSrid())
1328
        ) {
1329
            $def .= " SRID {$column->getSrid()}";
1330
        }
1331
1332
        $def .= $column->isIdentity() ? ' AUTO_INCREMENT' : '';
1333
        $def .= $this->getDefaultValueDefinition($column->getDefault(), $column->getType());
0 ignored issues
show
Bug introduced by
It seems like $column->getType() targeting Phinx\Db\Table\Column::getType() can also be of type object<Phinx\Util\Literal>; however, Phinx\Db\Adapter\PdoAdap...efaultValueDefinition() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1334
1335
        if ($column->getComment()) {
1336
            $def .= ' COMMENT ' . $this->getConnection()->quote($column->getComment());
1337
        }
1338
1339
        if ($column->getUpdate()) {
1340
            $def .= ' ON UPDATE ' . $column->getUpdate();
1341
        }
1342
1343
        return $def;
1344
    }
1345
1346
    /**
1347
     * Gets the MySQL Index Definition for an Index object.
1348
     *
1349
     * @param \Phinx\Db\Table\Index $index Index
1350
     *
1351
     * @return string
1352
     */
1353
    protected function getIndexSqlDefinition(Index $index)
1354
    {
1355
        $def = '';
1356
        $limit = '';
1357
1358
        if ($index->getType() === Index::UNIQUE) {
1359
            $def .= ' UNIQUE';
1360
        }
1361
1362
        if ($index->getType() === Index::FULLTEXT) {
1363
            $def .= ' FULLTEXT';
1364
        }
1365
1366
        $def .= ' KEY';
1367
1368
        if (is_string($index->getName())) {
1369
            $def .= ' `' . $index->getName() . '`';
1370
        }
1371
1372
        $columnNames = $index->getColumns();
1373
        $order = $index->getOrder() ?? [];
1374
        $columnNames = array_map(function ($columnName) use ($order) {
1375
            $ret = '`' . $columnName . '`';
1376
            if (isset($order[$columnName])) {
1377
                $ret .= ' ' . $order[$columnName];
1378
            }
1379
1380
            return $ret;
1381
        }, $columnNames);
1382
1383
        if (!is_array($index->getLimit())) {
1384
            if ($index->getLimit()) {
1385
                $limit = '(' . $index->getLimit() . ')';
1386
            }
1387
            $def .= ' (' . implode(',', $columnNames) . $limit . ')';
1388
        } else {
1389
            $columns = $index->getColumns();
1390
            $limits = $index->getLimit();
1391
            $def .= ' (';
1392
            foreach ($columns as $column) {
1393
                $limit = !isset($limits[$column]) || $limits[$column] <= 0 ? '' : '(' . $limits[$column] . ')';
1394
                $columnSort = isset($order[$column]) ?? '';
1395
                $def .= '`' . $column . '`' . $limit . ' ' . $columnSort . ', ';
1396
            }
1397
            $def = rtrim($def, ', ');
1398
            $def .= ' )';
1399
        }
1400
1401
        return $def;
1402
    }
1403
1404
    /**
1405
     * Gets the MySQL Foreign Key Definition for an ForeignKey object.
1406
     *
1407
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1408
     *
1409
     * @return string
1410
     */
1411
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
1412
    {
1413
        $def = '';
1414
        if ($foreignKey->getConstraint()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $foreignKey->getConstraint() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1415
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
1416
        }
1417
        $columnNames = [];
1418
        foreach ($foreignKey->getColumns() as $column) {
1419
            $columnNames[] = $this->quoteColumnName($column);
1420
        }
1421
        $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1422
        $refColumnNames = [];
1423
        foreach ($foreignKey->getReferencedColumns() as $column) {
1424
            $refColumnNames[] = $this->quoteColumnName($column);
1425
        }
1426
        $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1427
        if ($foreignKey->getOnDelete()) {
1428
            $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1429
        }
1430
        if ($foreignKey->getOnUpdate()) {
1431
            $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1432
        }
1433
1434
        return $def;
1435
    }
1436
1437
    /**
1438
     * Describes a database table. This is a MySQL adapter specific method.
1439
     *
1440
     * @param string $tableName Table name
1441
     *
1442
     * @return array
1443
     */
1444
    public function describeTable($tableName)
1445
    {
1446
        $options = $this->getOptions();
1447
1448
        // mysql specific
1449
        $sql = sprintf(
1450
            "SELECT *
1451
             FROM information_schema.tables
1452
             WHERE table_schema = '%s'
1453
             AND table_name = '%s'",
1454
            $options['name'],
1455
            $tableName
1456
        );
1457
1458
        return $this->fetchRow($sql);
1459
    }
1460
1461
    /**
1462
     * Returns MySQL column types (inherited and MySQL specified).
1463
     *
1464
     * @return string[]
1465
     */
1466
    public function getColumnTypes()
1467
    {
1468
        return array_merge(parent::getColumnTypes(), static::$specificColumnTypes);
1469
    }
1470
1471
    /**
1472
     * @inheritDoc
1473
     */
1474
    public function getDecoratedConnection()
1475
    {
1476
        $options = $this->getOptions();
1477
        $options = [
1478
            'username' => $options['user'] ?? null,
1479
            'password' => $options['pass'] ?? null,
1480
            'database' => $options['name'],
1481
            'quoteIdentifiers' => true,
1482
        ] + $options;
1483
1484
        $driver = new MysqlDriver($options);
1485
        $driver->setConnection($this->connection);
0 ignored issues
show
Bug introduced by
It seems like $this->connection can be null; however, setConnection() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1486
1487
        return new Connection(['driver' => $driver] + $options);
1488
    }
1489
}
1490