Passed
Push — 2.x ( 82378e...876fbc )
by Aleksei
40:03 queued 20:06
created

MySQLColumn::size()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver\MySQL\Schema;
13
14
use Cycle\Database\Driver\DriverInterface;
15
use Cycle\Database\Exception\DefaultValueException;
16
use Cycle\Database\Exception\SchemaException;
17
use Cycle\Database\Injection\Fragment;
18
use Cycle\Database\Injection\FragmentInterface;
19
use Cycle\Database\Schema\AbstractColumn;
0 ignored issues
show
Bug introduced by
The type Cycle\Database\Schema\AbstractColumn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use Cycle\Database\Schema\Attribute\ColumnAttribute;
21
22
/**
23
 * Attention! You can use only one timestamp or datetime with DATETIME_NOW setting! Thought, it will
24
 * work on multiple fields with MySQL 5.6.6+ version.
25
 *
26
 * @method $this primary(int $size, bool $unsigned = false, $zerofill = false)
27
 * @method $this smallPrimary(int $size, bool $unsigned = false, $zerofill = false)
28
 * @method $this bigPrimary(int $size, bool $unsigned = false, $zerofill = false)
29
 * @method $this integer(int $size, bool $unsigned = false, $zerofill = false)
30
 * @method $this tinyInteger(int $size, bool $unsigned = false, $zerofill = false)
31
 * @method $this smallInteger(int $size, bool $unsigned = false, $zerofill = false)
32
 * @method $this bigInteger(int $size, bool $unsigned = false, $zerofill = false)
33
 * @method $this unsigned(bool $value)
34
 * @method $this zerofill(bool $value)
35
 * @method $this comment(string $value)
36
 * @method $this after(string $column)
37
 */
38
class MySQLColumn extends AbstractColumn
39
{
40
    /**
41
     * Default timestamp expression ().
42
     */
43
    public const DATETIME_NOW = 'CURRENT_TIMESTAMP';
44
45
    public const EXCLUDE_FROM_COMPARE = ['size', 'timezone', 'userType', 'attributes', 'first', 'after', 'unknownSize'];
46
    protected const INTEGER_TYPES = ['tinyint', 'smallint', 'mediumint', 'int', 'bigint'];
47
48
    protected array $mapping = [
49
        //Primary sequences
50
        'primary'     => [
51
            'type'          => 'int',
52
            'size'          => 11,
53
            'autoIncrement' => true,
54
            'nullable'      => false,
55
        ],
56
        'smallPrimary'  => [
57
            'type'          => 'smallint',
58
            'size'          => 6,
59
            'autoIncrement' => true,
60
            'nullable'      => false,
61
        ],
62
        'bigPrimary'  => [
63
            'type'          => 'bigint',
64
            'size'          => 20,
65
            'autoIncrement' => true,
66
            'nullable'      => false,
67
        ],
68
69
        //Enum type (mapped via method)
70
        'enum'        => 'enum',
71
72
        //Set type (mapped via method)
73
        'set'         => 'set',
74
75
        //Logical types
76
        'boolean'     => ['type' => 'tinyint', 'size' => 1],
77
78
        //Integer types (size can always be changed with size method), longInteger has method alias
79
        //bigInteger
80
        'integer'     => ['type' => 'int', 'size' => 11, 'unsigned' => false, 'zerofill' => false],
81
        'tinyInteger' => ['type' => 'tinyint', 'size' => 4, 'unsigned' => false, 'zerofill' => false],
82
        'smallInteger' => ['type' => 'smallint', 'size' => 6, 'unsigned' => false, 'zerofill' => false],
83
        'bigInteger'  => ['type' => 'bigint', 'size' => 20, 'unsigned' => false, 'zerofill' => false],
84
85
        //String with specified length (mapped via method)
86
        'string'      => ['type' => 'varchar', 'size' => 255],
87
88
        //Generic types
89
        'text'        => 'text',
90
        'tinyText'    => 'tinytext',
91
        'mediumText'  => 'mediumtext',
92
        'longText'    => 'longtext',
93
94
        //Real types
95
        'double'      => 'double',
96
        'float'       => 'float',
97
98
        //Decimal type (mapped via method)
99
        'decimal'     => 'decimal',
100
101
        //Date and Time types
102
        'datetime'    => 'datetime',
103
        'date'        => 'date',
104
        'time'        => 'time',
105
        'timestamp'   => ['type' => 'timestamp', 'defaultValue' => null],
106
107
        //Binary types
108
        'binary'      => 'blob',
109
        'tinyBinary'  => 'tinyblob',
110
        'longBinary'  => 'longblob',
111
        'varbinary'   => ['type' => 'varbinary', 'size' => 255],
112
113
        //Additional types
114
        'json'        => 'json',
115
        'ulid'        => ['type' => 'varchar', 'size' => 26],
116
        'uuid'        => ['type' => 'varchar', 'size' => 36],
117
    ];
118
    protected array $reverseMapping = [
119
        'primary'     => [['type' => 'int', 'autoIncrement' => true]],
120
        'bigPrimary'  => ['serial', ['type' => 'bigint', 'size' => 20, 'autoIncrement' => true]],
121
        'enum'        => ['enum'],
122
        'set'         => ['set'],
123
        'boolean'     => ['bool', 'boolean', ['type' => 'tinyint', 'size' => 1]],
124
        'integer'     => ['int', 'integer', 'mediumint'],
125
        'tinyInteger' => ['tinyint'],
126
        'smallInteger' => ['smallint'],
127
        'bigInteger'  => ['bigint'],
128
        'string'      => ['varchar', 'char'],
129
        'text'        => ['text'],
130
        'tinyText'    => ['tinytext'],
131
        'mediumText'  => ['mediumtext'],
132
        'longText'    => ['longtext'],
133
        'double'      => ['double'],
134 474
        'float'       => ['float', 'real'],
135
        'decimal'     => ['decimal'],
136 474
        'datetime'    => ['datetime'],
137
        'date'        => ['date'],
138 474
        'time'        => ['time'],
139
        'timestamp'   => ['timestamp'],
140 214
        'binary'      => ['blob', 'binary', 'varbinary'],
141
        'tinyBinary'  => ['tinyblob'],
142
        'longBinary'  => ['longblob'],
143 474
        'json'        => ['json'],
144
    ];
145 474
146 474
    /**
147 332
     * List of types forbids default value set.
148
     */
149
    protected array $forbiddenDefaults = [
150 456
        'text',
151
        'mediumtext',
152
        'tinytext',
153
        'longtext',
154
        'blob',
155
        'tinyblob',
156 470
        'longblob',
157
        'json',
158 470
    ];
159
160 470
    #[ColumnAttribute(
161 470
        ['int', 'tinyint', 'smallint', 'bigint', 'varchar', 'varbinary', 'time', 'datetime', 'timestamp'],
162 470
    )]
163 470
    protected int $size = 0;
164
165
    /**
166 470
     * True if size is not defined in DB schema.
167 470
     */
168 470
    protected bool $unknownSize = false;
169
170
    /**
171
     * Column is auto incremental.
172
     */
173
    #[ColumnAttribute(self::INTEGER_TYPES)]
174
    protected bool $autoIncrement = false;
175
176 470
    /**
177
     * Unsigned integer type. Related to {@see INTEGER_TYPES} only.
178 470
     */
179 470
    #[ColumnAttribute(self::INTEGER_TYPES)]
180 280
    protected bool $unsigned = false;
181
182 280
    /**
183 162
     * Zerofill option. Related to {@see INTEGER_TYPES} only.
184 162
     */
185
    #[ColumnAttribute(self::INTEGER_TYPES)]
186 262
    protected bool $zerofill = false;
187
188
    /**
189
     * Column comment.
190
     */
191 470
    #[ColumnAttribute]
192 446
    protected string $comment = '';
193 446
194 338
    /**
195 338
     * Column name to position after.
196 320
     */
197 6
    #[ColumnAttribute]
198 6
    protected string $after = '';
199 316
200 2
    /**
201 2
     * Whether the column should be positioned first.
202
     */
203
    #[ColumnAttribute]
204
    protected bool $first = false;
205
206
    /**
207 470
     * @psalm-param non-empty-string $table
208 152
     */
209
    public static function createInstance(string $table, array $schema, ?\DateTimeZone $timezone = null): self
210 152
    {
211
        $column = new self($table, $schema['Field'], $timezone);
212
213
        $column->type = $schema['Type'];
0 ignored issues
show
Bug Best Practice introduced by
The property type does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
214 462
        $column->comment = $schema['Comment'];
215
        $column->nullable = \strtolower($schema['Null']) === 'yes';
0 ignored issues
show
Bug Best Practice introduced by
The property nullable does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
216
        $column->defaultValue = $schema['Default'];
0 ignored issues
show
Bug Best Practice introduced by
The property defaultValue does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
217
        $column->autoIncrement = \stripos($schema['Extra'], 'auto_increment') !== false;
218
219
        if (
220 462
            !\preg_match(
221 462
                '/^(?P<type>[a-z]+)(?:\((?P<options>[^)]+)\))?(?: (?P<attr>[a-z ]+))?/',
222
                $column->type,
223
                $matches,
224
            )
225
        ) {
226
            //No extra definitions
227 462
            return $column;
228
        }
229
230
        $column->type = $matches['type'];
231
232
        $options = [];
233
        if (!empty($matches['options'])) {
234
            $options = \explode(',', $matches['options']);
235
236
            if (\count($options) > 1) {
237 170
                $column->precision = (int) $options[0];
0 ignored issues
show
Bug Best Practice introduced by
The property precision does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
238
                $column->scale = (int) $options[1];
0 ignored issues
show
Bug Best Practice introduced by
The property scale does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
239
            } else {
240
                $column->size = (int) $options[0];
241 170
            }
242
        }
243
244
        if (!empty($matches['attr'])) {
245 170
            if (\in_array($column->type, self::INTEGER_TYPES, true)) {
246
                $intAttr = \array_map('trim', \explode(' ', $matches['attr']));
247
                if (\in_array('unsigned', $intAttr, true)) {
248
                    $column->unsigned = true;
249
                }
250
                if (\in_array('zerofill', $intAttr, true)) {
251
                    $column->zerofill = true;
252
                }
253
                unset($intAttr);
254
            }
255
        }
256
257
        // since 8.0 database does not provide size for some columns
258
        if ($column->size === 0) {
259
            $column->unknownSize = true;
260
            switch ($column->type) {
261
                case 'int':
262
                    $column->size = 11;
263
                    break;
264
                case 'bigint':
265
                    $column->size = 20;
266
                    break;
267
                case 'tinyint':
268
                    $column->size = 4;
269
                    break;
270
                case 'smallint':
271
                    $column->size = 6;
272
                    break;
273
            }
274
        }
275
276
        //Fetching enum and set values
277
        if ($options !== [] && static::isEnum($column)) {
278
            $column->enumValues = \array_map(static fn($value) => \trim($value, $value[0]), $options);
0 ignored issues
show
Bug Best Practice introduced by
The property enumValues does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
279
280
            return $column;
281
        }
282
283
        //Default value conversions
284
        if ($column->type === 'bit' && $column->hasDefaultValue()) {
285
            //Cutting b\ and '
286
            $column->defaultValue = new Fragment($column->defaultValue);
287
        }
288
289
        if (
290
            $column->defaultValue === '0000-00-00 00:00:00'
291
            && $column->getAbstractType() === 'timestamp'
292
        ) {
293
            //Normalizing default value for timestamps
294
            $column->defaultValue = 0;
295
        }
296
297
        return $column;
298
    }
299
300
    public function size(int $value): self
301
    {
302
        $this->unknownSize = false;
303
        return parent::__call('size', [$value]);
304
    }
305
306
    /**
307
     * @psalm-return non-empty-string
308
     */
309
    public function sqlStatement(DriverInterface $driver): string
310
    {
311
        if (\in_array($this->type, self::INTEGER_TYPES, true)) {
312
            $statement = $this->sqlStatementInteger($driver);
313
        } else {
314
            $defaultValue = $this->defaultValue;
315
316
            if (\in_array($this->type, $this->forbiddenDefaults, true)) {
317
                // Flushing default value for forbidden types
318
                $this->defaultValue = null;
0 ignored issues
show
Bug Best Practice introduced by
The property defaultValue does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
319
            }
320
321
            $statement = parent::sqlStatement($driver);
322
323
            $this->defaultValue = $defaultValue;
324
        }
325
326
        $this->comment === '' or $statement .= " COMMENT {$driver->quote($this->comment)}";
327
328
        $first = $this->first;
329
        $after = $first ? '' : $this->after;
330
331
        $statement .= match (true) {
332
            $first => ' FIRST',
333
            $after !== '' => " AFTER {$driver->identifier($after)}",
0 ignored issues
show
Bug introduced by
The method identifier() does not exist on Cycle\Database\Driver\DriverInterface. It seems like you code against a sub-type of Cycle\Database\Driver\DriverInterface such as Cycle\Database\Driver\Driver. ( Ignorable by Annotation )

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

333
            $after !== '' => " AFTER {$driver->/** @scrutinizer ignore-call */ identifier($after)}",
Loading history...
334
            default => '',
335
        };
336
337
        return $statement;
338
    }
339
340
    public function compare(AbstractColumn $initial): bool
341
    {
342
        \assert($initial instanceof self);
343
        $self = $this;
344
345
        // MySQL 8.0 does not provide size for unsigned integers without zerofill
346
        // so we can get wrong results in comparison of boolean columns
347
        if ($self->unknownSize || $initial->unknownSize) {
348
            // if one of the columns is boolean, we can safely assume that size is 1
349
            if (\in_array($self->userType, ['bool', 'boolean'], true)) {
350
                $initial = clone $initial;
351
                $initial->size = 1;
352
            } elseif (\in_array($initial->userType, ['bool', 'boolean'], true)) {
353
                $self = clone $self;
354
                $self->size = 1;
355
            }
356
        }
357
358
        $result = \Closure::fromCallable([parent::class, 'compare'])->bindTo($self)($initial);
359
360
361
        if ($self->type === 'varchar' || $self->type === 'varbinary') {
362
            return $result && $self->size === $initial->size;
363
        }
364
365
        return $result;
366
    }
367
368
    public function isUnsigned(): bool
369
    {
370
        return $this->unsigned;
371
    }
372
373
    public function isZerofill(): bool
374
    {
375
        return $this->zerofill;
376
    }
377
378
    public function first(bool $value = true): self
379
    {
380
        $this->first = $value;
381
382
        return $this;
383
    }
384
385
    public function set(string|array $values): self
386
    {
387
        $this->type('set');
388
        $this->enumValues = \array_map('strval', \is_array($values) ? $values : \func_get_args());
0 ignored issues
show
Bug Best Practice introduced by
The property enumValues does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
introduced by
The condition is_array($values) is always true.
Loading history...
389
390
        return $this;
391
    }
392
393
    /**
394
     * @param int<0, max> $size
395
     */
396
    public function varbinary(int $size = 255): self
397
    {
398
        $this->type('varbinary');
399
400
        $size < 0 && throw new SchemaException('Invalid varbinary size value');
401
402
        $this->size = $size;
403
404
        return $this;
405
    }
406
407
    /**
408
     * If a size is provided, a varbinary column of the specified size will be created.
409
     * Otherwise, a blob type column will be created.
410
     *
411
     * @param int<0, max> $size
412
     */
413
    public function binary(int $size = 0): self
414
    {
415
        if ($size > 0) {
416
            return $this->varbinary($size);
417
        }
418
419
        $this->type('blob');
420
421
        return $this;
422
    }
423
424
    public function getComment(): string
425
    {
426
        return $this->comment;
427
    }
428
429
    protected static function isEnum(AbstractColumn $column): bool
430
    {
431
        return $column->getAbstractType() === 'enum' || $column->getAbstractType() === 'set';
432
    }
433
434
    /**
435
     * Ensure that datetime fields are correctly formatted.
436
     *
437
     * @psalm-param non-empty-string $type
438
     *
439
     * @throws DefaultValueException
440
     */
441
    protected function formatDatetime(
442
        string $type,
443
        string|int|\DateTimeInterface $value,
444
    ): \DateTimeInterface|FragmentInterface|string {
445
        if ($value === 'current_timestamp()') {
446
            $value = self::DATETIME_NOW;
447
        }
448
449
        return parent::formatDatetime($type, $value);
450
    }
451
452
    private function sqlStatementInteger(DriverInterface $driver): string
453
    {
454
        return \sprintf(
455
            '%s %s(%s)%s%s%s%s%s',
456
            $driver->identifier($this->name),
457
            $this->type,
458
            $this->size,
459
            $this->unsigned ? ' UNSIGNED' : '',
460
            $this->zerofill ? ' ZEROFILL' : '',
461
            $this->nullable ? ' NULL' : ' NOT NULL',
462
            $this->defaultValue !== null ? " DEFAULT {$this->quoteDefault($driver)}" : '',
463
            $this->autoIncrement ? ' AUTO_INCREMENT' : '',
464
        );
465
    }
466
}
467