Completed
Push — master ( ecbf31...0cca94 )
by mark
01:51 queued 11s
created

MysqlAdapter::connect()   C

Complexity

Conditions 11
Paths 50

Size

Total Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 11.353

Importance

Changes 0
Metric Value
dl 0
loc 55
ccs 24
cts 28
cp 0.8571
rs 6.8351
c 0
b 0
f 0
cc 11
nc 50
nop 0
crap 11.353

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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