Passed
Pull Request — master (#40)
by Wilmer
14:30
created

MysqlQueryBuilder::dropForeignKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql\Query;
6
7
use PDO;
8
use Yiisoft\Db\Exception\Exception;
9
use Yiisoft\Db\Exception\InvalidArgumentException;
10
use Yiisoft\Db\Exception\InvalidConfigException;
11
use Yiisoft\Db\Exception\NotSupportedException;
12
use Yiisoft\Db\Expression\Expression;
13
use Yiisoft\Db\Expression\ExpressionBuilder;
14
use Yiisoft\Db\Expression\JsonExpression;
15
use Yiisoft\Db\Mysql\Expression\JsonExpressionBuilder;
16
use Yiisoft\Db\Mysql\Schema\MysqlColumnSchemaBuilder;
17
use Yiisoft\Db\Mysql\Schema\MysqlSchema;
18
use Yiisoft\Db\Query\Query;
19
use Yiisoft\Db\Query\QueryBuilder;
20
21
use function array_merge;
22
use function array_values;
23
use function ctype_digit;
24
use function implode;
25
use function preg_match;
26
use function preg_match_all;
27
use function preg_replace;
28
use function reset;
29
use function trim;
30
use function version_compare;
31
32
final class MysqlQueryBuilder extends QueryBuilder
33
{
34
    /**
35
     * @var array mapping from abstract column types (keys) to physical column types (values).
36
     */
37
    protected array $typeMap = [
38
        MysqlSchema::TYPE_PK => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY',
39
        MysqlSchema::TYPE_UPK => 'int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
40
        MysqlSchema::TYPE_BIGPK => 'bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY',
41
        MysqlSchema::TYPE_UBIGPK => 'bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
42
        MysqlSchema::TYPE_CHAR => 'char(1)',
43
        MysqlSchema::TYPE_STRING => 'varchar(255)',
44
        MysqlSchema::TYPE_TEXT => 'text',
45
        MysqlSchema::TYPE_TINYINT => 'tinyint(3)',
46
        MysqlSchema::TYPE_SMALLINT => 'smallint(6)',
47
        MysqlSchema::TYPE_INTEGER => 'int(11)',
48
        MysqlSchema::TYPE_BIGINT => 'bigint(20)',
49
        MysqlSchema::TYPE_FLOAT => 'float',
50
        MysqlSchema::TYPE_DOUBLE => 'double',
51
        MysqlSchema::TYPE_DECIMAL => 'decimal(10,0)',
52
        MysqlSchema::TYPE_DATE => 'date',
53
        MysqlSchema::TYPE_BINARY => 'blob',
54
        MysqlSchema::TYPE_BOOLEAN => 'tinyint(1)',
55
        MysqlSchema::TYPE_MONEY => 'decimal(19,4)',
56
        MysqlSchema::TYPE_JSON => 'json'
57
    ];
58
59
    /**
60
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
61
     * expression builders for this query builder.
62
     *
63
     * @return array
64
     *
65
     * See {@see ExpressionBuilder} docs for details.
66
     */
67
    protected function defaultExpressionBuilders(): array
68
    {
69
        return array_merge(
70
            parent::defaultExpressionBuilders(),
71
            [
72
                JsonExpression::class => JsonExpressionBuilder::class,
73
            ]
74
        );
75
    }
76
77
    /**
78
     * Builds a SQL statement for renaming a column.
79
     *
80
     * @param string $table   the table whose column is to be renamed. The name will be properly quoted by the method.
81
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
82
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
83
     *
84
     * @throws Exception
85
     *
86
     * @return string the SQL statement for renaming a DB column.
87
     */
88
    public function renameColumn(string $table, string $oldName, string $newName): string
89
    {
90
        $quotedTable = $this->db->quoteTableName($table);
0 ignored issues
show
Bug introduced by
The method quoteTableName() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

90
        /** @scrutinizer ignore-call */ 
91
        $quotedTable = $this->db->quoteTableName($table);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
91
92
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne();
93
94
        if ($row === false) {
95
            throw new Exception("Unable to find column '$oldName' in table '$table'.");
96
        }
97
98
        if (isset($row['Create Table'])) {
99
            $sql = $row['Create Table'];
100
        } else {
101
            $row = array_values($row);
102
            $sql = $row[1];
103
        }
104
105
        if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
106
            foreach ($matches[1] as $i => $c) {
107
                if ($c === $oldName) {
108
                    return "ALTER TABLE $quotedTable CHANGE "
109
                        . $this->db->quoteColumnName($oldName) . ' '
110
                        . $this->db->quoteColumnName($newName) . ' '
111
                        . $matches[2][$i];
112
                }
113
            }
114
        }
115
116
        /* try to give back a SQL anyway */
117
        return "ALTER TABLE $quotedTable CHANGE "
118
            . $this->db->quoteColumnName($oldName) . ' '
119
            . $this->db->quoteColumnName($newName);
120
    }
121
122
    /**
123
     * Builds a SQL statement for creating a new index.
124
     *
125
     * @param string $name the name of the index. The name will be properly quoted by the method.
126
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
127
     * the method.
128
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
129
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
130
     * method, unless a parenthesis is found in the name.
131
     * @param bool $unique whether to add UNIQUE constraint on the created index.
132
     *
133
     * @throws Exception
134
     * @throws InvalidArgumentException
135
     * @throws InvalidConfigException
136
     * @throws NotSupportedException
137
     *
138
     * @return string the SQL statement for creating a new index.
139
     *
140
     * {@see https://bugs.mysql.com/bug.php?id=48875}
141
     */
142
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
143
    {
144
        return 'ALTER TABLE '
145
            . $this->db->quoteTableName($table)
146
            . ($unique ? ' ADD UNIQUE INDEX ' : ' ADD INDEX ')
147
            . $this->db->quoteTableName($name)
148
            . ' (' . $this->buildColumns($columns) . ')';
149
    }
150
151
    /**
152
     * Builds a SQL statement for dropping a foreign key constraint.
153
     *
154
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the
155
     * method.
156
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
157
     *
158
     * @throws Exception
159
     * @throws InvalidConfigException
160
     * @throws NotSupportedException
161
     *
162
     * @return string the SQL statement for dropping a foreign key constraint.
163
     */
164
    public function dropForeignKey(string $name, string $table): string
165
    {
166
        return 'ALTER TABLE '
167
            . $this->db->quoteTableName($table)
168
            . ' DROP FOREIGN KEY ' . $this->db->quoteColumnName($name);
169
    }
170
171
    /**
172
     * Builds a SQL statement for removing a primary key constraint to an existing table.
173
     *
174
     * @param string $name the name of the primary key constraint to be removed.
175
     * @param string $table the table that the primary key constraint will be removed from.
176
     *
177
     * @throws Exception
178
     * @throws InvalidConfigException
179
     * @throws NotSupportedException
180
     *
181
     * @return string the SQL statement for removing a primary key constraint from an existing table.
182
     */
183
    public function dropPrimaryKey(string $name, string $table): string
184
    {
185
        return 'ALTER TABLE '
186
            . $this->db->quoteTableName($table) . ' DROP PRIMARY KEY';
187
    }
188
189
    /**
190
     * Creates a SQL command for dropping an unique constraint.
191
     *
192
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
193
     * method.
194
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by the
195
     * method.
196
     *
197
     * @throws Exception
198
     * @throws InvalidConfigException
199
     * @throws NotSupportedException
200
     *
201
     * @return string the SQL statement for dropping an unique constraint.
202
     */
203
    public function dropUnique(string $name, string $table): string
204
    {
205
        return $this->dropIndex($name, $table);
206
    }
207
208
    /**
209
     * @param string $name
210
     * @param string $table
211
     * @param string $expression
212
     *
213
     * @throws NotSupportedException Method not supported by Mysql.
214
     *
215
     * @return string the SQL statement for adding a check constraint to an existing table.
216
     */
217
    public function addCheck(string $name, string $table, string $expression): string
218
    {
219
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
220
    }
221
222
    /**
223
     * @param string $name
224
     * @param string $table
225
     *
226
     * @throws NotSupportedException Method not supported by Mysql.
227
     *
228
     * @return string the SQL statement for adding a check constraint to an existing table.
229
     */
230
    public function dropCheck(string $name, string $table): string
231
    {
232
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
233
    }
234
235
    /**
236
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
237
     *
238
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
239
     * or 1.
240
     *
241
     * @param string $tableName the name of the table whose primary key sequence will be reset.
242
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
243
     * row's primary key will have a value 1.
244
     *
245
     * @throws Exception
246
     * @throws InvalidArgumentException
247
     * @throws InvalidConfigException
248
     * @throws NotSupportedException
249
     *
250
     * @return string the SQL statement for resetting sequence.
251
     */
252
    public function resetSequence(string $tableName, $value = null): string
253
    {
254
        $table = $this->db->getTableSchema($tableName);
255
256
        if ($table !== null && $table->getSequenceName() !== null) {
257
            $tableName = $this->db->quoteTableName($tableName);
258
259
            if ($value === null) {
260
                $pk = $table->getPrimaryKey();
261
                $key = reset($pk);
262
                $value = $this->db->createCommand("SELECT MAX(`$key`) FROM $tableName")->queryScalar() + 1;
263
            } else {
264
                $value = (int) $value;
265
            }
266
267
            return "ALTER TABLE $tableName AUTO_INCREMENT=$value";
268
        }
269
270
        if ($table === null) {
271
            throw new InvalidArgumentException("Table not found: $tableName");
272
        }
273
274
        throw new InvalidArgumentException("There is no sequence associated with table '$tableName'.");
275
    }
276
277
    /**
278
     * Builds a SQL statement for enabling or disabling integrity check.
279
     *
280
     * @param bool $check  whether to turn on or off the integrity check.
281
     * @param string $schema the schema of the tables. Meaningless for MySQL.
282
     * @param string $table  the table name. Meaningless for MySQL.
283
     *
284
     * @return string the SQL statement for checking integrity.
285
     */
286
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
287
    {
288
        return 'SET FOREIGN_KEY_CHECKS = ' . ($check ? 1 : 0);
289
    }
290
291
    /**
292
     * @param int|object|null $limit
293
     * @param int|object|null $offset
294
     *
295
     * @return string the LIMIT and OFFSET clauses.
296
     */
297
    public function buildLimit($limit, $offset): string
298
    {
299
        $sql = '';
300
301
        if ($this->hasLimit($limit)) {
302
            $sql = 'LIMIT ' . $limit;
0 ignored issues
show
Bug introduced by
Are you sure $limit of type integer|null|object can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

302
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
303
304
            if ($this->hasOffset($offset)) {
305
                $sql .= ' OFFSET ' . $offset;
0 ignored issues
show
Bug introduced by
Are you sure $offset of type integer|null|object can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

305
                $sql .= ' OFFSET ' . /** @scrutinizer ignore-type */ $offset;
Loading history...
306
            }
307
        } elseif ($this->hasOffset($offset)) {
308
            /**
309
             * limit is not optional in MySQL.
310
             *
311
             * http://stackoverflow.com/a/271650/1106908
312
             * http://dev.mysql.com/doc/refman/5.0/en/select.html#idm47619502796240
313
             */
314
            $sql = "LIMIT $offset, 18446744073709551615"; // 2^64-1
315
        }
316
317
        return $sql;
318
    }
319
320
    /**
321
     * Checks to see if the given limit is effective.
322
     *
323
     * @param mixed $limit the given limit.
324
     *
325
     * @return bool whether the limit is effective.
326
     */
327
    protected function hasLimit($limit): bool
328
    {
329
        /** In MySQL limit argument must be non negative integer constant */
330
        return ctype_digit((string) $limit);
331
    }
332
333
    /**
334
     * Checks to see if the given offset is effective.
335
     *
336
     * @param mixed $offset the given offset.
337
     *
338
     * @return bool whether the offset is effective.
339
     */
340
    protected function hasOffset($offset): bool
341
    {
342
        /** In MySQL offset argument must be non negative integer constant */
343
        $offset = (string) $offset;
344
345
        return ctype_digit($offset) && $offset !== '0';
346
    }
347
348
    /**
349
     * Prepares a `VALUES` part for an `INSERT` SQL statement.
350
     *
351
     * @param string $table the table that new rows will be inserted into.
352
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
353
     * {@see Query|Query} to perform INSERT INTO ... SELECT SQL statement.
354
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the DB
355
     * command later.
356
     *
357
     * @throws Exception
358
     * @throws InvalidArgumentException
359
     * @throws InvalidConfigException
360
     * @throws NotSupportedException
361
     *
362
     * @return array array of column names, placeholders, values and params.
363
     */
364
    protected function prepareInsertValues(string $table, $columns, array $params = []): array
365
    {
366
        [$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);
367
        if (!$columns instanceof Query && empty($names)) {
368
            $tableSchema = $this->db->getSchema()->getTableSchema($table);
369
            $columns = $tableSchema->getColumns();
370
            if ($tableSchema !== null) {
371
                $columns = !empty($tableSchema->getPrimaryKey())
372
                    ? $tableSchema->getPrimaryKey() : [reset($columns)->getName()];
373
                foreach ($columns as $name) {
374
                    $names[] = $this->db->quoteColumnName($name);
375
                    $placeholders[] = 'DEFAULT';
376
                }
377
            }
378
        }
379
380
        return [$names, $placeholders, $values, $params];
381
    }
382
383
    /**
384
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
385
     * constraints), or update them if they do.
386
     *
387
     * For example,
388
     *
389
     * ```php
390
     * $sql = $queryBuilder->upsert('pages', [
391
     *     'name' => 'Front page',
392
     *     'url' => 'http://example.com/', // url is unique
393
     *     'visits' => 0,
394
     * ], [
395
     *     'visits' => new Expression('visits + 1'),
396
     * ], $params);
397
     * ```
398
     *
399
     * The method will properly escape the table and column names.
400
     *
401
     * @param string $table the table that new rows will be inserted into/updated in.
402
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance of
403
     * {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
404
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist. If `true`
405
     * is passed, the column data will be updated to match the insert column data. If `false` is passed, no update will
406
     * be performed if the column data already exists.
407
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the DB
408
     * command later.
409
     *
410
     * @throws Exception
411
     * @throws InvalidConfigException
412
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
413
     *
414
     * @return string the resulting SQL.
415
     */
416
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
417
    {
418
        $insertSql = $this->insert($table, $insertColumns, $params);
419
420
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
421
422
        if (empty($uniqueNames)) {
423
            return $insertSql;
424
        }
425
426
        if ($updateColumns === true) {
427
            $updateColumns = [];
428
            foreach ($updateNames as $name) {
429
                $updateColumns[$name] = new Expression('VALUES(' . $this->db->quoteColumnName($name) . ')');
430
            }
431
        } elseif ($updateColumns === false) {
432
            $name = $this->db->quoteColumnName(reset($uniqueNames));
433
            $updateColumns = [$name => new Expression($this->db->quoteTableName($table) . '.' . $name)];
434
        }
435
436
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
437
438
        return $insertSql . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
439
    }
440
441
    /**
442
     * Builds a SQL command for adding comment to column.
443
     *
444
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
445
     * method.
446
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
447
     * method.
448
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
449
     *
450
     * @throws Exception
451
     * @throws InvalidConfigException
452
     * @throws NotSupportedException
453
     *
454
     * @return string the SQL statement for adding comment on column.
455
     */
456
    public function addCommentOnColumn(string $table, string $column, string $comment): string
457
    {
458
        /** Strip existing comment which may include escaped quotes */
459
        $definition = trim(
460
            preg_replace(
461
                "/COMMENT '(?:''|[^'])*'/i",
462
                '',
463
                $this->getColumnDefinition($table, $column)
464
            )
465
        );
466
467
        $checkRegex = '/CHECK *(\(([^()]|(?-2))*\))/';
468
469
        $check = preg_match($checkRegex, $definition, $checkMatches);
470
471
        if ($check === 1) {
472
            $definition = preg_replace($checkRegex, '', $definition);
473
        }
474
475
        $alterSql = 'ALTER TABLE ' . $this->db->quoteTableName($table)
476
            . ' CHANGE ' . $this->db->quoteColumnName($column)
477
            . ' ' . $this->db->quoteColumnName($column)
478
            . (empty($definition) ? '' : ' ' . $definition)
479
            . ' COMMENT ' . $this->db->quoteValue($comment);
480
481
        if ($check === 1) {
482
            $alterSql .= ' ' . $checkMatches[0];
483
        }
484
485
        return $alterSql;
486
    }
487
488
    /**
489
     * Builds a SQL command for adding comment to table.
490
     *
491
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
492
     * method.
493
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
494
     *
495
     * @throws Exception
496
     * @throws InvalidConfigException
497
     * @throws NotSupportedException
498
     *
499
     * @return string the SQL statement for adding comment on table.
500
     */
501
    public function addCommentOnTable(string $table, string $comment): string
502
    {
503
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' COMMENT ' . $this->db->quoteValue($comment);
504
    }
505
506
    /**
507
     * Builds a SQL command for adding comment to column.
508
     *
509
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
510
     * method.
511
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
512
     * method.
513
     *
514
     * @throws Exception
515
     * @throws InvalidConfigException
516
     * @throws NotSupportedException
517
     *
518
     * @return string the SQL statement for adding comment on column.
519
     */
520
    public function dropCommentFromColumn(string $table, string $column): string
521
    {
522
        return $this->addCommentOnColumn($table, $column, '');
523
    }
524
525
    /**
526
     * Builds a SQL command for adding comment to table.
527
     *
528
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
529
     * method.
530
     *
531
     * @throws Exception
532
     * @throws InvalidConfigException
533
     * @throws NotSupportedException
534
     *
535
     * @return string the SQL statement for adding comment on column.
536
     */
537
    public function dropCommentFromTable(string $table): string
538
    {
539
        return $this->addCommentOnTable($table, '');
540
    }
541
542
    /**
543
     * Gets column definition.
544
     *
545
     * @param string $table table name.
546
     * @param string $column column name.
547
     *
548
     * @throws Exception in case when table does not contain column.
549
     *
550
     * @return string|null the column definition.
551
     */
552
    private function getColumnDefinition(string $table, string $column): ?string
553
    {
554
        $quotedTable = $this->db->quoteTableName($table);
555
556
        $row = $this->db->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne();
557
558
        if ($row === false) {
559
            throw new Exception("Unable to find column '$column' in table '$table'.");
560
        }
561
562
        if (!isset($row['Create Table'])) {
563
            $row = array_values($row);
564
            $sql = $row[1];
565
        } else {
566
            $sql = $row['Create Table'];
567
        }
568
569
        if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
570
            foreach ($matches[1] as $i => $c) {
571
                if ($c === $column) {
572
                    return $matches[2][$i];
573
                }
574
            }
575
        }
576
    }
577
578
    /**
579
     * Converts an abstract column type into a physical column type.
580
     *
581
     * The conversion is done using the type map specified in {@see typeMap}.
582
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
583
     * physical types):
584
     *
585
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY
586
     *    KEY"
587
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT
588
     *    PRIMARY KEY"
589
     * - `upk`: an unsigned auto-incremental primary key type, will be converted into "int(10) UNSIGNED NOT NULL
590
     *    AUTO_INCREMENT PRIMARY KEY"
591
     * - `char`: char type, will be converted into "char(1)"
592
     * - `string`: string type, will be converted into "varchar(255)"
593
     * - `text`: a long string type, will be converted into "text"
594
     * - `smallint`: a small integer type, will be converted into "smallint(6)"
595
     * - `integer`: integer type, will be converted into "int(11)"
596
     * - `bigint`: a big integer type, will be converted into "bigint(20)"
597
     * - `boolean`: boolean type, will be converted into "tinyint(1)"
598
     * - `float``: float number type, will be converted into "float"
599
     * - `decimal`: decimal number type, will be converted into "decimal"
600
     * - `datetime`: datetime type, will be converted into "datetime"
601
     * - `timestamp`: timestamp type, will be converted into "timestamp"
602
     * - `time`: time type, will be converted into "time"
603
     * - `date`: date type, will be converted into "date"
604
     * - `money`: money type, will be converted into "decimal(19,4)"
605
     * - `binary`: binary data type, will be converted into "blob"
606
     *
607
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only the first
608
     * part will be converted, and the rest of the parts will be appended to the converted result.
609
     *
610
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
611
     *
612
     * For some of the abstract types you can also specify a length or precision constraint by appending it in round
613
     * brackets directly to the type.
614
     *
615
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. If the underlying DBMS does
616
     * not support these kind of constraints for a type it will be ignored.
617
     *
618
     * If a type cannot be found in {@see typeMap}, it will be returned without any change.
619
     *
620
     * @param string|MysqlColumnSchemaBuilder $type abstract column type
621
     *
622
     * @throws Exception
623
     * @throws InvalidConfigException
624
     *
625
     * @return string physical column type.
626
     */
627
    public function getColumnType($type): string
628
    {
629
        $this->typeMap = array_merge($this->typeMap, $this->defaultTimeTypeMap());
630
631
        return parent::getColumnType($type);
632
    }
633
634
    /**
635
     * Returns the map for default time type.
636
     *
637
     * If the version of MySQL is lower than 5.6.4, then the types will be without fractional seconds, otherwise with
638
     * fractional seconds.
639
     *
640
     * @throws Exception
641
     * @throws InvalidConfigException
642
     *
643
     * @return array
644
     */
645
    private function defaultTimeTypeMap(): array
646
    {
647
        $map = [
648
            MysqlSchema::TYPE_DATETIME => 'datetime',
649
            MysqlSchema::TYPE_TIMESTAMP => 'timestamp',
650
            MysqlSchema::TYPE_TIME => 'time',
651
        ];
652
653
        if ($this->supportsFractionalSeconds()) {
654
            $map = [
655
                MysqlSchema::TYPE_DATETIME => 'datetime(0)',
656
                MysqlSchema::TYPE_TIMESTAMP => 'timestamp(0)',
657
                MysqlSchema::TYPE_TIME => 'time(0)',
658
            ];
659
        }
660
661
        return $map;
662
    }
663
664
    /**
665
     * Checks the ability to use fractional seconds.
666
     *
667
     * @return bool
668
     *
669
     * @throws Exception
670
     * @throws InvalidConfigException
671
     *
672
     * {@see https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html}
673
     */
674
    private function supportsFractionalSeconds(): bool
675
    {
676
        $version = $this->db->getSlavePdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
677
678
        return version_compare($version, '5.6.4', '>=');
679
    }
680
}
681