Passed
Pull Request — master (#125)
by
unknown
11:00
created

QueryBuilder::defaultTimeTypeMap()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 10
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 17
ccs 5
cts 5
cp 1
crap 2
rs 9.9332
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
94 2
            ->getDb()
95 2
            ->quoteTableName($table);
96
97
        /** @psalm-var array<array-key, string> $row */
98 1
        $row = $this
99 2
            ->getDb()
100 2
            ->createCommand('SHOW CREATE TABLE ' . $quotedTable)
101 2
            ->queryOne();
102
103 1
        if (isset($row['Create Table'])) {
104 1
            $sql = $row['Create Table'];
105
        } else {
106
            $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

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

604
            $row = array_values(/** @scrutinizer ignore-type */ $row);
Loading history...
605
            $sql = $row[1];
606
        } else {
607 1
            $sql = $row['Create Table'];
608
        }
609
610 1
        if (preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m', $sql, $matches)) {
611 1
            foreach ($matches[1] as $i => $c) {
612 1
                if ($c === $column) {
613 1
                    $result = $matches[2][$i];
614
                }
615
            }
616
        }
617
618 1
        return $result;
619
    }
620
621
    /**
622
     * Converts an abstract column type into a physical column type.
623
     *
624
     * The conversion is done using the type map specified in {@see typeMap}.
625
     * The following abstract column types are supported (using MySQL as an example to explain the corresponding
626
     * physical types):
627
     *
628
     * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY
629
     *    KEY"
630
     * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT
631
     *    PRIMARY KEY"
632
     * - `upk`: an unsigned auto-incremental primary key type, will be converted into "int(10) UNSIGNED NOT NULL
633
     *    AUTO_INCREMENT PRIMARY KEY"
634
     * - `char`: char type, will be converted into "char(1)"
635
     * - `string`: string type, will be converted into "varchar(255)"
636
     * - `text`: a long string type, will be converted into "text"
637
     * - `smallint`: a small integer type, will be converted into "smallint(6)"
638
     * - `integer`: integer type, will be converted into "int(11)"
639
     * - `bigint`: a big integer type, will be converted into "bigint(20)"
640
     * - `boolean`: boolean type, will be converted into "tinyint(1)"
641
     * - `float``: float number type, will be converted into "float"
642
     * - `decimal`: decimal number type, will be converted into "decimal"
643
     * - `datetime`: datetime type, will be converted into "datetime"
644
     * - `timestamp`: timestamp type, will be converted into "timestamp"
645
     * - `time`: time type, will be converted into "time"
646
     * - `date`: date type, will be converted into "date"
647
     * - `money`: money type, will be converted into "decimal(19,4)"
648
     * - `binary`: binary data type, will be converted into "blob"
649
     *
650
     * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only the first
651
     * part will be converted, and the rest of the parts will be appended to the converted result.
652
     *
653
     * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
654
     *
655
     * For some of the abstract types you can also specify a length or precision constraint by appending it in round
656
     * brackets directly to the type.
657
     *
658
     * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. If the underlying DBMS does
659
     * not support these kind of constraints for a type it will be ignored.
660
     *
661
     * If a type cannot be found in {@see typeMap}, it will be returned without any change.
662
     *
663
     * @param ColumnSchemaBuilder|string $type abstract column type
664
     *
665
     * @throws Exception
666
     *
667
     * @return string physical column type.
668
     */
669 12
    public function getColumnType($type): string
670
    {
671 12
        $this->typeMap = array_merge($this->typeMap, $this->defaultTimeTypeMap());
672
673 12
        return parent::getColumnType($type);
674
    }
675
676
    /**
677
     * Returns the map for default time type.
678
     *
679
     * If the version of MySQL is lower than 5.6.4, then the types will be without fractional seconds, otherwise with
680
     * fractional seconds.
681
     *
682
     * @throws Exception
683
     *
684
     * @return array
685
     * @psalm-return array<string, string>
686
     */
687 12
    private function defaultTimeTypeMap(): array
688
    {
689 12
        $map = [
690
            Schema::TYPE_DATETIME => 'datetime',
691
            Schema::TYPE_TIMESTAMP => 'timestamp',
692
            Schema::TYPE_TIME => 'time',
693
        ];
694
695 12
        if ($this->supportsFractionalSeconds()) {
696 12
            $map = [
697
                Schema::TYPE_DATETIME => 'datetime(0)',
698
                Schema::TYPE_TIMESTAMP => 'timestamp(0)',
699
                Schema::TYPE_TIME => 'time(0)',
700
            ];
701
        }
702
703 12
        return $map;
704
    }
705
706
    /**
707
     * Checks the ability to use fractional seconds.
708
     *
709
     * @throws Exception
710
     *
711
     * {@see https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html}
712
     *
713
     * @return bool
714
     */
715 12
    private function supportsFractionalSeconds(): bool
716
    {
717 12
        $result = false;
718
719 12
        $slavePdo = $this
720 12
            ->getDb()
721 12
            ->getSlavePdo();
722
723 12
        if ($slavePdo !== null) {
724
            /** @var string $version */
725 12
            $version = $slavePdo->getAttribute(PDO::ATTR_SERVER_VERSION);
726 12
            $result = version_compare($version, '5.6.4', '>=');
727
        }
728
729 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...
730
    }
731
}
732