Passed
Pull Request — master (#114)
by Wilmer
02:37
created

QueryBuilder::resetSequence()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

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

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

101
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
102
            $sql = $row[1];
103
        }
104
105 1
        if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
106 1
            foreach ($matches[1] as $i => $c) {
107 1
                if ($c === $oldName) {
108 1
                    return "ALTER TABLE $quotedTable CHANGE "
109 1
                        . $this->getDb()->quoteColumnName($oldName) . ' '
110 1
                        . $this->getDb()->quoteColumnName($newName) . ' '
111 1
                        . $matches[2][$i];
112
                }
113
            }
114
        }
115
116
        /* try to give back a SQL anyway */
117 1
        return "ALTER TABLE $quotedTable CHANGE "
118 1
            . $this->getDb()->quoteColumnName($oldName) . ' '
119 1
            . $this->getDb()->quoteColumnName($newName);
120
    }
121
122
    /**
123
     * Builds a SQL statement for creating a new index.
124
     *
125
     * @param string $name the name of the index. The name will be properly quoted by the method.
126
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
127
     * the method.
128
     * @param array|string $columns the column(s) that should be included in the index. If there are multiple columns,
129
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
130
     * method, unless a parenthesis is found in the name.
131
     * @param bool $unique whether to add UNIQUE constraint on the created index.
132
     *
133
     * @psalm-param array<array-key, ExpressionInterface|string>|string $columns
134
     *
135
     * @throws Exception|InvalidArgumentException
136
     *
137
     * @return string the SQL statement for creating a new index.
138
     *
139
     * {@see https://bugs.mysql.com/bug.php?id=48875}
140
     */
141 6
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
142
    {
143
        return 'ALTER TABLE '
144 6
            . $this->getDb()->quoteTableName($table)
145 6
            . ($unique ? ' ADD UNIQUE INDEX ' : ' ADD INDEX ')
146 6
            . $this->getDb()->quoteTableName($name)
147 6
            . ' (' . $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
     * @return string the SQL statement for dropping a foreign key constraint.
158
     */
159 2
    public function dropForeignKey(string $name, string $table): string
160
    {
161
        return 'ALTER TABLE '
162 2
            . $this->getDb()->quoteTableName($table)
163 2
            . ' DROP FOREIGN KEY ' . $this->getDb()->quoteColumnName($name);
164
    }
165
166
    /**
167
     * Builds a SQL statement for removing a primary key constraint to an existing table.
168
     *
169
     * @param string $name the name of the primary key constraint to be removed.
170
     * @param string $table the table that the primary key constraint will be removed from.
171
     *
172
     * @return string the SQL statement for removing a primary key constraint from an existing table.
173
     */
174 2
    public function dropPrimaryKey(string $name, string $table): string
175
    {
176
        return 'ALTER TABLE '
177 2
            . $this->getDb()->quoteTableName($table) . ' DROP PRIMARY KEY';
178
    }
179
180
    /**
181
     * Creates a SQL command for dropping an unique constraint.
182
     *
183
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
184
     * method.
185
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by the
186
     * method.
187
     *
188
     * @return string the SQL statement for dropping an unique constraint.
189
     */
190 2
    public function dropUnique(string $name, string $table): string
191
    {
192 2
        return $this->dropIndex($name, $table);
193
    }
194
195
    /**
196
     * @param string $name
197
     * @param string $table
198
     * @param string $expression
199
     *
200
     * @throws NotSupportedException Method not supported by Mysql.
201
     *
202
     * @return string the SQL statement for adding a check constraint to an existing table.
203
     */
204 1
    public function addCheck(string $name, string $table, string $expression): string
205
    {
206 1
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
207
    }
208
209
    /**
210
     * @param string $name
211
     * @param string $table
212
     *
213
     * @throws NotSupportedException Method not supported by Mysql.
214
     *
215
     * @return string the SQL statement for adding a check constraint to an existing table.
216
     */
217 1
    public function dropCheck(string $name, string $table): string
218
    {
219 1
        throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.');
220
    }
221
222
    /**
223
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
224
     *
225
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
226
     * or 1.
227
     *
228
     * @param string $tableName the name of the table whose primary key sequence will be reset.
229
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
230
     * row's primary key will have a value 1.
231
     *
232
     * @throws Exception|InvalidArgumentException|InvalidConfigException|Throwable
233
     *
234
     * @return string the SQL statement for resetting sequence.
235
     */
236 3
    public function resetSequence(string $tableName, $value = null): string
237
    {
238 3
        $table = $this->getDb()->getTableSchema($tableName);
239
240 3
        if ($table !== null && $table->getSequenceName() !== null) {
241 1
            $tableName = $this->getDb()->quoteTableName($tableName);
242
243 1
            if ($value === null) {
244 1
                $pk = $table->getPrimaryKey();
245 1
                $key = (string) reset($pk);
246 1
                $value = (int) $this->getDb()->createCommand("SELECT MAX(`$key`) FROM $tableName")->queryScalar() + 1;
247
            } else {
248 1
                $value = (int) $value;
249
            }
250
251 1
            return "ALTER TABLE $tableName AUTO_INCREMENT=$value";
252
        }
253
254 2
        if ($table === null) {
255 1
            throw new InvalidArgumentException("Table not found: $tableName");
256
        }
257
258 1
        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 1
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
271
    {
272 1
        return 'SET FOREIGN_KEY_CHECKS = ' . ($check ? 1 : 0);
273
    }
274
275
    /**
276
     * @param Expression|int|null $limit
277
     * @param Expression|int|null $offset
278
     *
279
     * @return string the LIMIT and OFFSET clauses.
280
     */
281 188
    public function buildLimit($limit, $offset): string
282
    {
283 188
        $sql = '';
284
285 188
        if ($this->hasLimit($limit)) {
286 9
            $sql = 'LIMIT ' . (string) $limit;
287
288 9
            if ($this->hasOffset($offset)) {
289 9
                $sql .= ' OFFSET ' . (string) $offset;
290
            }
291 182
        } elseif ($this->hasOffset($offset)) {
292
            /**
293
             * 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 188
        return $sql;
302
    }
303
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 188
    protected function hasLimit($limit): bool
312
    {
313
        /** In MySQL limit argument must be non negative integer constant */
314 188
        return ctype_digit((string) $limit);
315
    }
316
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 188
    protected function hasOffset($offset): bool
325
    {
326
        /** In MySQL offset argument must be non negative integer constant */
327 188
        $offset = (string) $offset;
328
329 188
        return ctype_digit($offset) && $offset !== '0';
330
    }
331
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 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|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
342
     *
343
     * @return array array of column names, placeholders, values and params.
344
     */
345 48
    protected function prepareInsertValues(string $table, $columns, array $params = []): array
346
    {
347
        /**
348
         * @var array $names
349
         * @var array $placeholders
350
         */
351 48
        [$names, $placeholders, $values, $params] = parent::prepareInsertValues($table, $columns, $params);
352 45
        if (!$columns instanceof Query && empty($names)) {
353
            $tableSchema = $this->getDb()->getSchema()->getTableSchema($table);
354
355
            if ($tableSchema !== null) {
356
                $columns = $tableSchema->getColumns();
357
                $columns = !empty($tableSchema->getPrimaryKey())
358
                    ? $tableSchema->getPrimaryKey() : [reset($columns)->getName()];
359
                /** @var string $name */
360
                foreach ($columns as $name) {
361
                    $names[] = $this->getDb()->quoteColumnName($name);
362
                    $placeholders[] = 'DEFAULT';
363
                }
364
            }
365
        }
366
367 45
        return [$names, $placeholders, $values, $params];
368
    }
369
370
    /**
371
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
372
     * constraints), or update them if they do.
373
     *
374
     * For example,
375
     *
376
     * ```php
377
     * $sql = $queryBuilder->upsert('pages', [
378
     *     'name' => 'Front page',
379
     *     'url' => 'http://example.com/', // url is unique
380
     *     'visits' => 0,
381
     * ], [
382
     *     'visits' => new Expression('visits + 1'),
383
     * ], $params);
384
     * ```
385
     *
386
     * The method will properly escape the table and column names.
387
     *
388
     * @param string $table the table that new rows will be inserted into/updated in.
389
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance of
390
     * {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
391
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist. If `true`
392
     * is passed, the column data will be updated to match the insert column data. If `false` is passed, no update will
393
     * be performed if the column data already exists.
394
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the DB
395
     * command later.
396
     *
397
     * @throws Exception|InvalidConfigException|JsonException|NotSupportedException if this is not supported by the
398
     * underlying DBMS.
399
     *
400
     * @return string the resulting SQL.
401
     */
402 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
403
    {
404 18
        $insertSql = $this->insert($table, $insertColumns, $params);
405
406
        /** @var array $uniqueNames */
407 18
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
408
409 18
        if (empty($uniqueNames)) {
410 3
            return $insertSql;
411
        }
412
413 15
        if ($updateColumns === true) {
414 4
            $updateColumns = [];
415
            /** @var string $name */
416 4
            foreach ($updateNames as $name) {
417 4
                $updateColumns[$name] = new Expression('VALUES(' . $this->getDb()->quoteColumnName($name) . ')');
418
            }
419 11
        } elseif ($updateColumns === false) {
420 5
            $columnName = (string) reset($uniqueNames);
421 5
            $name = $this->getDb()->quoteColumnName($columnName);
422 5
            $updateColumns = [$name => new Expression($this->getDb()->quoteTableName($table) . '.' . $name)];
423
        }
424
425
        /**
426
         *  @psalm-var array<array-key, string> $updates
427
         *  @psalm-var array<string, ExpressionInterface|string> $updateColumns
428
         */
429 15
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
430
431 15
        return $insertSql . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
432
    }
433
434
    /**
435
     * Builds a SQL command for adding comment to column.
436
     *
437
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
438
     * method.
439
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
440
     * method.
441
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
442
     *
443
     * @throws Exception|Throwable
444
     *
445
     * @return string the SQL statement for adding comment on column.
446
     */
447 1
    public function addCommentOnColumn(string $table, string $column, string $comment): string
448
    {
449
        /* Strip existing comment which may include escaped quotes */
450 1
        $definition = trim(
451 1
            preg_replace(
452
                "/COMMENT '(?:''|[^'])*'/i",
453
                '',
454 1
                $this->getColumnDefinition($table, $column)
455
            )
456
        );
457
458 1
        $checkRegex = '/CHECK *(\(([^()]|(?-2))*\))/';
459
460 1
        $check = preg_match($checkRegex, $definition, $checkMatches);
461
462 1
        if ($check === 1) {
463
            $definition = preg_replace($checkRegex, '', $definition);
464
        }
465
466 1
        $alterSql = 'ALTER TABLE ' . $this->getDb()->quoteTableName($table)
467 1
            . ' CHANGE ' . $this->getDb()->quoteColumnName($column)
468 1
            . ' ' . $this->getDb()->quoteColumnName($column)
469 1
            . (empty($definition) ? '' : ' ' . $definition)
470 1
            . ' COMMENT ' . $this->getDb()->quoteValue($comment);
471
472 1
        if ($check === 1) {
473
            $alterSql .= ' ' . $checkMatches[0];
474
        }
475
476 1
        return $alterSql;
477
    }
478
479
    /**
480
     * Builds a SQL command for adding comment to table.
481
     *
482
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
483
     * method.
484
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
485
     *
486
     * @throws Exception
487
     *
488
     * @return string the SQL statement for adding comment on table.
489
     */
490 1
    public function addCommentOnTable(string $table, string $comment): string
491
    {
492 1
        return 'ALTER TABLE ' . $this->getDb()->quoteTableName($table) . ' COMMENT ' . $this->getDb()->quoteValue($comment);
493
    }
494
495
    /**
496
     * Builds a SQL command for adding comment to column.
497
     *
498
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
499
     * method.
500
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
501
     * method.
502
     *
503
     * @throws Exception|Throwable
504
     *
505
     * @return string the SQL statement for adding comment on column.
506
     */
507 1
    public function dropCommentFromColumn(string $table, string $column): string
508
    {
509 1
        return $this->addCommentOnColumn($table, $column, '');
510
    }
511
512
    /**
513
     * Builds a SQL command for adding comment to table.
514
     *
515
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
516
     * method.
517
     *
518
     * @throws Exception
519
     *
520
     * @return string the SQL statement for adding comment on column.
521
     */
522 1
    public function dropCommentFromTable(string $table): string
523
    {
524 1
        return $this->addCommentOnTable($table, '');
525
    }
526
527
    /**
528
     * Gets column definition.
529
     *
530
     * @param string $table table name.
531
     * @param string $column column name.
532
     *
533
     * @throws Exception|Throwable in case when table does not contain column.
534
     *
535
     * @return string the column definition.
536
     */
537 1
    private function getColumnDefinition(string $table, string $column): string
538
    {
539 1
        $result = '';
540
541 1
        $quotedTable = $this->getDb()->quoteTableName($table);
542
543
        /** @var array<array-key, string> $row */
544 1
        $row = $this->getDb()->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryOne();
545
546 1
        if (!isset($row['Create Table'])) {
547
            $row = array_values($row);
0 ignored issues
show
Bug introduced by
$row of type false is incompatible with the type array expected by parameter $array of array_values(). ( Ignorable by Annotation )

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

547
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
548
            $sql = $row[1];
549
        } else {
550 1
            $sql = $row['Create Table'];
551
        }
552
553 1
        if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
554 1
            foreach ($matches[1] as $i => $c) {
555 1
                if ($c === $column) {
556 1
                    $result = $matches[2][$i];
557
                }
558
            }
559
        }
560
561 1
        return $result;
562
    }
563
564
    /**
565
     * Converts an abstract column type into a physical column type.
566
     *
567
     * The conversion is done using the type map specified in {@see typeMap}.
568
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
569
     * physical types):
570
     *
571
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY
572
     *    KEY"
573
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT
574
     *    PRIMARY KEY"
575
     * - `upk`: an unsigned auto-incremental primary key type, will be converted into "int(10) UNSIGNED NOT NULL
576
     *    AUTO_INCREMENT PRIMARY KEY"
577
     * - `char`: char type, will be converted into "char(1)"
578
     * - `string`: string type, will be converted into "varchar(255)"
579
     * - `text`: a long string type, will be converted into "text"
580
     * - `smallint`: a small integer type, will be converted into "smallint(6)"
581
     * - `integer`: integer type, will be converted into "int(11)"
582
     * - `bigint`: a big integer type, will be converted into "bigint(20)"
583
     * - `boolean`: boolean type, will be converted into "tinyint(1)"
584
     * - `float``: float number type, will be converted into "float"
585
     * - `decimal`: decimal number type, will be converted into "decimal"
586
     * - `datetime`: datetime type, will be converted into "datetime"
587
     * - `timestamp`: timestamp type, will be converted into "timestamp"
588
     * - `time`: time type, will be converted into "time"
589
     * - `date`: date type, will be converted into "date"
590
     * - `money`: money type, will be converted into "decimal(19,4)"
591
     * - `binary`: binary data type, will be converted into "blob"
592
     *
593
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only the first
594
     * part will be converted, and the rest of the parts will be appended to the converted result.
595
     *
596
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
597
     *
598
     * For some of the abstract types you can also specify a length or precision constraint by appending it in round
599
     * brackets directly to the type.
600
     *
601
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. If the underlying DBMS does
602
     * not support these kind of constraints for a type it will be ignored.
603
     *
604
     * If a type cannot be found in {@see typeMap}, it will be returned without any change.
605
     *
606
     * @param ColumnSchemaBuilder|string $type abstract column type
607
     *
608
     * @throws Exception
609
     *
610
     * @return string physical column type.
611
     */
612 12
    public function getColumnType($type): string
613
    {
614 12
        $this->typeMap = array_merge($this->typeMap, $this->defaultTimeTypeMap());
615
616 12
        return parent::getColumnType($type);
617
    }
618
619
    /**
620
     * Returns the map for default time type.
621
     *
622
     * If the version of MySQL is lower than 5.6.4, then the types will be without fractional seconds, otherwise with
623
     * fractional seconds.
624
     *
625
     * @throws Exception
626
     *
627
     * @return array
628
     * @psalm-return array<string, string>
629
     */
630 12
    private function defaultTimeTypeMap(): array
631
    {
632 12
        $map = [
633
            Schema::TYPE_DATETIME => 'datetime',
634
            Schema::TYPE_TIMESTAMP => 'timestamp',
635
            Schema::TYPE_TIME => 'time',
636
        ];
637
638 12
        if ($this->supportsFractionalSeconds()) {
639 12
            $map = [
640
                Schema::TYPE_DATETIME => 'datetime(0)',
641
                Schema::TYPE_TIMESTAMP => 'timestamp(0)',
642
                Schema::TYPE_TIME => 'time(0)',
643
            ];
644
        }
645
646 12
        return $map;
647
    }
648
649
    /**
650
     * Checks the ability to use fractional seconds.
651
     *
652
     * @throws Exception
653
     *
654
     * {@see https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html}
655
     *
656
     * @return bool
657
     */
658 12
    private function supportsFractionalSeconds(): bool
659
    {
660 12
        $result = false;
661
662 12
        $slavePdo = $this->getDb()->getSlavePdo();
663
664 12
        if ($slavePdo !== null) {
665
            /** @var string $version */
666 12
            $version = $slavePdo->getAttribute(PDO::ATTR_SERVER_VERSION);
667 12
            $result = version_compare($version, '5.6.4', '>=');
668
        }
669
670 12
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type integer which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
671
    }
672
}
673