Passed
Pull Request — master (#50)
by Wilmer
12:16
created

QueryBuilder::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 1
cts 1
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Mysql;
6
7
use PDO;
8
use Throwable;
9
use Yiisoft\Db\Connection\ConnectionInterface;
10
use Yiisoft\Db\Exception\Exception;
11
use Yiisoft\Db\Exception\InvalidArgumentException;
12
use Yiisoft\Db\Exception\InvalidConfigException;
13
use Yiisoft\Db\Exception\NotSupportedException;
14
use Yiisoft\Db\Expression\Expression;
15
use Yiisoft\Db\Expression\ExpressionBuilder;
16
use Yiisoft\Db\Expression\JsonExpression;
17
use Yiisoft\Db\Query\Query;
18
use Yiisoft\Db\Query\QueryBuilder as AbstractQueryBuilder;
19
20
use function array_merge;
21
use function array_values;
22
use function ctype_digit;
23
use function implode;
24
use function preg_match;
25
use function preg_match_all;
26
use function preg_replace;
27
use function reset;
28
use function trim;
29
use function version_compare;
30
31
final class QueryBuilder extends AbstractQueryBuilder
32
{
33
    /**
34
     * @var array mapping from abstract column types (keys) to physical column types (values).
35
     */
36
    protected array $typeMap = [
37
        Schema::TYPE_PK => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY',
38
        Schema::TYPE_UPK => 'int(10) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
39
        Schema::TYPE_BIGPK => 'bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY',
40
        Schema::TYPE_UBIGPK => 'bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
41
        Schema::TYPE_CHAR => 'char(1)',
42
        Schema::TYPE_STRING => 'varchar(255)',
43
        Schema::TYPE_TEXT => 'text',
44
        Schema::TYPE_TINYINT => 'tinyint(3)',
45
        Schema::TYPE_SMALLINT => 'smallint(6)',
46
        Schema::TYPE_INTEGER => 'int(11)',
47
        Schema::TYPE_BIGINT => 'bigint(20)',
48
        Schema::TYPE_FLOAT => 'float',
49
        Schema::TYPE_DOUBLE => 'double',
50
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
51
        Schema::TYPE_DATE => 'date',
52
        Schema::TYPE_BINARY => 'blob',
53
        Schema::TYPE_BOOLEAN => 'tinyint(1)',
54
        Schema::TYPE_MONEY => 'decimal(19,4)',
55
        Schema::TYPE_JSON => 'json'
56
    ];
57
58
    /** @var Connection $db */
59
    private ConnectionInterface $db;
60
61
    public function __construct(ConnectionInterface $db)
62
    {
63
        $this->db = $db;
0 ignored issues
show
Documentation Bug introduced by
$db is of type Yiisoft\Db\Connection\ConnectionInterface, but the property $db was declared to be of type Yiisoft\Db\Mysql\Connection. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

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

311
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
312
313
            if ($this->hasOffset($offset)) {
314 176
                $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

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