Completed
Pull Request — master (#1910)
by B
01:53 queued 15s
created

MysqlAdapter::afterClause()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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