Passed
Push — master ( 0cc03e...7bd564 )
by Wilmer
09:34 queued 07:32
created

QueryBuilder::addCommentOnTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

299
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
300
301 9
            if ($this->hasOffset($offset)) {
302 9
                $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

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