Passed
Pull Request — master (#30)
by Wilmer
15:21
created

QueryBuilder::dropForeignKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

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

286
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
287 153
288 9
            if ($this->hasOffset($offset)) {
289
                $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

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