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

QueryBuilder::createIndex()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

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

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