Passed
Push — 2.x ( 199029...7ebea5 )
by Aleksei
16:49
created

AbstractColumn::getScale()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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\Schema;
13
14
use Cycle\Database\Schema\Attribute\ColumnAttribute;
15
use Cycle\Database\Schema\Traits\ColumnAttributesTrait;
16
use DateTimeImmutable;
17
use Cycle\Database\ColumnInterface;
18
use Cycle\Database\Driver\DriverInterface;
19
use Cycle\Database\Exception\DefaultValueException;
20
use Cycle\Database\Exception\SchemaException;
21
use Cycle\Database\Injection\Fragment;
22
use Cycle\Database\Injection\FragmentInterface;
23
use Cycle\Database\Query\QueryParameters;
24
use Cycle\Database\Schema\Traits\ElementTrait;
25
26
/**
27
 * Abstract column schema with read (see ColumnInterface) and write abilities. Must be implemented
28
 * by driver to support DBMS specific syntax and creation rules.
29
 *
30
 * Shortcuts for various column types:
31
 *
32
 * @method $this|AbstractColumn primary()
33
 * @method $this|AbstractColumn bigPrimary()
34
 * @method $this|AbstractColumn boolean()
35
 * @method $this|AbstractColumn integer()
36
 * @method $this|AbstractColumn tinyInteger()
37
 * @method $this|AbstractColumn smallInteger()
38
 * @method $this|AbstractColumn bigInteger()
39
 * @method $this|AbstractColumn text()
40
 * @method $this|AbstractColumn tinyText()
41
 * @method $this|AbstractColumn longText()
42
 * @method $this|AbstractColumn double()
43
 * @method $this|AbstractColumn float()
44
 * @method $this|AbstractColumn date()
45
 * @method $this|AbstractColumn time()
46
 * @method $this|AbstractColumn timestamp()
47
 * @method $this|AbstractColumn binary()
48
 * @method $this|AbstractColumn tinyBinary()
49
 * @method $this|AbstractColumn longBinary()
50
 * @method $this|AbstractColumn json()
51
 * @method $this|AbstractColumn uuid()
52
 */
53
abstract class AbstractColumn implements ColumnInterface, ElementInterface
54
{
55
    use ColumnAttributesTrait;
56
    use ElementTrait;
57
58
    /**
59
     * Default timestamp expression (driver specific).
60
     */
61
    public const DATETIME_NOW = 'CURRENT_TIMESTAMP';
62
63
    /**
64
     * Value to be excluded from comparison.
65
     */
66
    public const EXCLUDE_FROM_COMPARE = ['timezone', 'userType', 'attributes'];
67
68
    /**
69
     * Normalization for time and dates.
70
     */
71
    public const DATE_FORMAT = 'Y-m-d';
72
    public const TIME_FORMAT = 'H:i:s';
73
    public const DATETIME_PRECISION = 6;
74
75
    /**
76
     * Mapping between abstract type and internal database type with it's options. Multiple abstract
77
     * types can map into one database type, this implementation allows us to equalize two columns
78
     * if they have different abstract types but same database one. Must be declared by DBMS
79
     * specific implementation.
80
     *
81
     * Example:
82
     * integer => array('type' => 'int', 'size' => 1),
83
     * boolean => array('type' => 'tinyint', 'size' => 1)
84
     *
85
     * @internal
86
     */
87
    protected array $mapping = [
88
        //Primary sequences
89
        'primary'     => null,
90
        'bigPrimary'  => null,
91
92
        //Enum type (mapped via method)
93
        'enum'        => null,
94
95
        //Logical types
96
        'boolean'     => null,
97
98
        //Integer types (size can always be changed with size method), longInteger has method alias
99
        //bigInteger
100
        'integer'     => null,
101
        'tinyInteger' => null,
102
        'smallInteger'=> null,
103
        'bigInteger'  => null,
104
105
        //String with specified length (mapped via method)
106
        'string'      => null,
107
108
        //Generic types
109
        'text'        => null,
110
        'tinyText'    => null,
111
        'longText'    => null,
112
113
        //Real types
114
        'double'      => null,
115
        'float'       => null,
116
117
        //Decimal type (mapped via method)
118
        'decimal'     => null,
119
120
        //Date and Time types
121
        'datetime'    => null,
122
        'date'        => null,
123
        'time'        => null,
124
        'timestamp'   => null,
125
126
        //Binary types
127
        'binary'      => null,
128
        'tinyBinary'  => null,
129
        'longBinary'  => null,
130
131
        //Additional types
132
        'json'        => null,
133
    ];
134
135
    /**
136
     * Reverse mapping is responsible for generating abstract type based on database type and it's
137
     * options. Multiple database types can be mapped into one abstract type.
138
     *
139
     * @internal
140
     */
141
    protected array $reverseMapping = [
142
        'primary'     => [],
143
        'bigPrimary'  => [],
144
        'enum'        => [],
145
        'boolean'     => [],
146
        'integer'     => [],
147
        'tinyInteger' => [],
148
        'smallInteger'=> [],
149
        'bigInteger'  => [],
150
        'string'      => [],
151
        'text'        => [],
152
        'tinyText'    => [],
153
        'longText'    => [],
154
        'double'      => [],
155
        'float'       => [],
156
        'decimal'     => [],
157
        'datetime'    => [],
158
        'date'        => [],
159
        'time'        => [],
160
        'timestamp'   => [],
161
        'binary'      => [],
162
        'tinyBinary'  => [],
163
        'longBinary'  => [],
164
        'json'        => [],
165
    ];
166
167
    /**
168
     * User defined type. Only until actual mapping.
169
     */
170
    protected ?string $userType = null;
171
172
    /**
173
     * DBMS specific column type.
174
     */
175
    protected string $type = '';
176
177
    protected ?\DateTimeZone $timezone = null;
178
179
    /**
180
     * Indicates that column can contain null values.
181
     */
182
    #[ColumnAttribute]
183
    protected bool $nullable = true;
184
185
    /**
186
     * Default column value, may not be applied to some datatypes (for example to primary keys),
187
     * should follow type size and other options.
188
     */
189
    #[ColumnAttribute]
190
    protected mixed $defaultValue = null;
191
192
    /**
193
     * Column type size, can have different meanings for different datatypes.
194
     */
195
    #[ColumnAttribute]
196
    protected int $size = 0;
197
198
    /**
199
     * Precision of column, applied only for "decimal" type.
200
     */
201
    #[ColumnAttribute(['decimal'])]
202
    protected int $precision = 0;
203
204
    /**
205
     * Scale of column, applied only for "decimal" type.
206
     */
207
    #[ColumnAttribute(['decimal'])]
208
    protected int $scale = 0;
209
210
    /**
211
     * List of allowed enum values.
212
     */
213
    protected array $enumValues = [];
214
215
    /**
216
     * Abstract type aliases (for consistency).
217
     */
218
    protected array $aliases = [
219
        'int'            => 'integer',
220
        'smallint'       => 'smallInteger',
221
        'bigint'         => 'bigInteger',
222
        'incremental'    => 'primary',
223
        'bigIncremental' => 'bigPrimary',
224
        'bool'           => 'boolean',
225
        'blob'           => 'binary',
226
    ];
227
228
    /**
229
     * Association list between abstract types and native PHP types. Every non listed type will be
230
     * converted into string.
231
     *
232 1950
     * @internal
233
     */
234
    private array $phpMapping = [
235
        self::INT   => ['primary', 'bigPrimary', 'integer', 'tinyInteger', 'smallInteger', 'bigInteger'],
236
        self::BOOL  => ['boolean'],
237 1950
        self::FLOAT => ['double', 'float', 'decimal'],
238 1950
    ];
239
240
    /**
241
     * @psalm-param non-empty-string $table
242
     * @psalm-param non-empty-string $name
243
     */
244
    public function __construct(
245 1750
        protected string $table,
246
        protected string $name,
247 1750
        \DateTimeZone $timezone = null
248
    ) {
249
        $this->timezone = $timezone ?? new \DateTimeZone(date_default_timezone_get());
250 856
    }
251
252 856
    /**
253
     * Shortcut for AbstractColumn->type() method.
254
     *
255
     * @psalm-param non-empty-string $name
256
     */
257
    public function __call(string $name, array $arguments = []): self
258 24
    {
259
        if (isset($this->aliases[$name]) || isset($this->mapping[$name])) {
260 24
            $this->type($name);
261 24
        }
262
263 24
        // The type must be set before the attributes are filled.
264 24
        !empty($this->type) or throw new SchemaException('Undefined abstract/virtual type');
265 24
266
        if (\count($arguments) === 1 && \key($arguments) === 0) {
267
            if (\array_key_exists($name, $this->getAttributesMap())) {
268
                $this->fillAttributes([$name => $arguments[0]]);
269 24
                return $this;
270 8
            }
271
        }
272
273 24
        $this->fillAttributes($arguments);
274 24
275
        return $this;
276
    }
277 24
278 8
    public function __toString(): string
279
    {
280
        return $this->table . '.' . $this->getName();
281 24
    }
282
283
    /**
284
     * Simplified way to dump information.
285 24
     */
286 16
    public function __debugInfo(): array
287 16
    {
288
        $column = [
289
            'name' => $this->name,
290 24
            'type' => [
291
                'database' => $this->type,
292
                'schema'   => $this->getAbstractType(),
293 8
                'php'      => $this->getType(),
294
            ],
295 8
        ];
296
297
        if (!empty($this->size)) {
298 852
            $column['size'] = $this->size;
299
        }
300 852
301
        if ($this->nullable) {
302
            $column['nullable'] = true;
303 852
        }
304
305 852
        if ($this->defaultValue !== null) {
306
            $column['defaultValue'] = $this->getDefaultValue();
307
        }
308 8
309
        if (static::isEnum($this)) {
310 8
            $column['enumValues'] = $this->enumValues;
311
        }
312
313 1798
        if ($this->getAbstractType() === 'decimal') {
314
            $column['precision'] = $this->precision;
315 1798
            $column['scale'] = $this->scale;
316
        }
317
318
        if ($this->attributes !== []) {
319
            $column['attributes'] = $this->attributes;
320
        }
321 1798
322
        return $column;
323 1798
    }
324 1514
325
    public function getSize(): int
326
    {
327 1074
        return $this->size;
328
    }
329 368
330
    public function getPrecision(): int
331
    {
332 850
        return $this->precision;
333 682
    }
334
335
    public function getScale(): int
336 698
    {
337 170
        return $this->scale;
338 293
    }
339 205
340 205
    public function isNullable(): bool
341 698
    {
342
        return $this->nullable;
343
    }
344
345
    public function hasDefaultValue(): bool
346
    {
347
        return $this->defaultValue !== null;
348 60
    }
349
350 60
    /**
351
     * @throws DefaultValueException
352
     */
353
    public function getDefaultValue(): mixed
354
    {
355
        if (!$this->hasDefaultValue()) {
356 852
            return null;
357
        }
358 852
359
        if ($this->defaultValue instanceof FragmentInterface) {
360
            //Defined as SQL piece
361 856
            return $this->defaultValue;
362
        }
363 856
364
        if (\in_array($this->getAbstractType(), ['time', 'date', 'datetime', 'timestamp'])) {
365
            return $this->formatDatetime($this->getAbstractType(), $this->defaultValue);
366
        }
367
368
        return match ($this->getType()) {
369 842
            'int' => (int) $this->defaultValue,
370
            'float' => (float) $this->defaultValue,
371 842
            'bool' => \is_string($this->defaultValue) && strtolower($this->defaultValue) === 'false'
372 842
                ? false : (bool) $this->defaultValue,
373 842
            default => (string)$this->defaultValue
374 618
        };
375
    }
376
377
    /**
378 754
     * Get every associated column constraint names.
379
     */
380
    public function getConstraints(): array
381
    {
382
        return [];
383
    }
384
385
    /**
386
     * Get allowed enum values.
387
     */
388
    public function getEnumValues(): array
389
    {
390
        return $this->enumValues;
391
    }
392
393
    public function getInternalType(): string
394
    {
395
        return $this->type;
396 1922
    }
397
398 1922
    /**
399 1922
     * @psalm-return non-empty-string
400 1922
     */
401 1890
    public function getType(): string
402 1862
    {
403
        $schemaType = $this->getAbstractType();
404
        foreach ($this->phpMapping as $phpType => $candidates) {
405 1838
            if (\in_array($schemaType, $candidates, true)) {
406
                return $phpType;
407
            }
408 1414
        }
409 1360
410
        return self::STRING;
411
    }
412 1034
413 1034
    /**
414 1034
     * Returns type defined by the user, only until schema sync. Attention, this value is only preserved during the
415
     * declaration process. Value will become null after the schema fetched from database.
416
     *
417 1034
     * @internal
418 876
     */
419
    public function getDeclaredType(): ?string
420
    {
421
        return $this->userType;
422 688
    }
423
424
    /**
425
     * DBMS specific reverse mapping must map database specific type into limited set of abstract
426 4
     * types.
427
     */
428
    public function getAbstractType(): string
429
    {
430
        foreach ($this->reverseMapping as $type => $candidates) {
431
            foreach ($candidates as $candidate) {
432
                if (\is_string($candidate)) {
433
                    if (strtolower($candidate) === strtolower($this->type)) {
434
                        return $type;
435
                    }
436
437
                    continue;
438
                }
439
440
                if (strtolower($candidate['type']) !== strtolower($this->type)) {
441
                    continue;
442 1932
                }
443
444 1932
                foreach ($candidate as $option => $required) {
445
                    if ($option === 'type') {
446
                        continue;
447
                    }
448
449 1932
                    if ($this->{$option} !== $required) {
450
                        continue 2;
451
                    }
452 1932
                }
453
454
                return $type;
455 1932
            }
456 1932
        }
457
458
        return 'unknown';
459 1932
    }
460 1656
461
    /**
462 1656
     * Give column new abstract type. DBMS specific implementation must map provided type into one
463
     * of internal database values.
464
     *
465
     * Attention, changing type of existed columns in some databases has a lot of restrictions like
466 1442
     * cross type conversions and etc. Try do not change column type without a reason.
467 1442
     *
468
     * @psalm-param non-empty-string $abstract Abstract or virtual type declared in mapping.
469
     *
470 1442
     * @todo Support native database types (simply bypass abstractType)!
471
     */
472
    public function type(string $abstract): self
473
    {
474
        if (isset($this->aliases[$abstract])) {
475
            //Make recursive
476 698
            $abstract = $this->aliases[$abstract];
477
        }
478 698
479
        if (!isset($this->mapping[$abstract])) {
480 698
            $this->type = $abstract;
481
            $this->userType = $abstract;
482
483
            return $this;
484
        }
485
486
        // Originally specified type.
487 954
        $this->userType = $abstract;
488
489
        // Resetting all values to default state.
490 954
        $this->size = $this->precision = $this->scale = 0;
491 578
        $this->enumValues = [];
492
493
        // Abstract type points to DBMS specific type
494 954
        if (\is_string($this->mapping[$abstract])) {
495
            $this->type = $this->mapping[$abstract];
496 954
497
            return $this;
498
        }
499
500
        // Configuring column properties based on abstractType preferences
501
        foreach ($this->mapping[$abstract] as $property => $value) {
502
            $this->{$property} = $value;
503
        }
504
505
        return $this;
506
    }
507
508
    /**
509 304
     * Set column nullable/not nullable.
510
     */
511 304
    public function nullable(bool $nullable = true): self
512 304
    {
513 304
        $this->nullable = $nullable;
514 304
515
        return $this;
516
    }
517 304
518
    /**
519
     * Change column default value (can be forbidden for some column types).
520
     * Use Database::TIMESTAMP_NOW to use driver specific NOW() function.
521
     */
522
    public function defaultValue(mixed $value): self
523
    {
524
        //Forcing driver specific values
525
        if ($value === self::DATETIME_NOW) {
526
            $value = static::DATETIME_NOW;
527
        }
528
529
        $this->defaultValue = $value;
530
531
        return $this;
532
    }
533
534 1008
    /**
535
     * Set column as enum type and specify set of allowed values. Most of drivers will emulate enums
536 1008
     * using column constraints.
537
     *
538 1008
     * Examples:
539
     * $table->status->enum(['active', 'disabled']);
540 1008
     * $table->status->enum('active', 'disabled');
541
     *
542 1008
     * @param array|string $values Enum values (array or comma separated). String values only.
543
     */
544
    public function enum(string|array $values): self
545
    {
546
        $this->type('enum');
547
        $this->enumValues = array_map(
548
            'strval',
549
            is_array($values) ? $values : func_get_args()
0 ignored issues
show
introduced by
The condition is_array($values) is always true.
Loading history...
550 64
        );
551
552 64
        return $this;
553
    }
554 64
555
    /**
556 56
     * Set column type as string with limited size. Maximum allowed size is 255 bytes, use "text"
557 56
     * abstract types for longer strings.
558
     *
559 56
     * Strings are perfect type to store email addresses as it big enough to store valid address
560
     * and
561
     * can be covered with unique index.
562 1464
     *
563
     * @link http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
564 1464
     *
565
     * @param int $size Max string length.
566 1464
     *
567
     * @throws SchemaException
568 458
     */
569 458
    public function string(int $size = 255): self
570
    {
571 1440
        $this->type('string');
572 42
573 1410
        $size < 0 && throw new SchemaException('Invalid string length value');
574 878
575
        $this->size = $size;
576
577 1464
        return $this;
578
    }
579 1464
580 634
    public function datetime(int $size = 0, mixed ...$attributes): self
581
    {
582
        $this->type('datetime');
583 1464
        $this->fillAttributes($attributes);
584
585
        ($size < 0 || $size > static::DATETIME_PRECISION) && throw new SchemaException(
586 1894
            \sprintf('Invalid %s precision value.', $this->getAbstractType())
587
        );
588 1894
        $this->size = $size;
589
590
        return $this;
591 1894
    }
592 1838
593
    /**
594
     * Set column type as decimal with specific precision and scale.
595 1598
     *
596 1598
     * @throws SchemaException
597
     */
598 1598
    public function decimal(int $precision, int $scale = 0): self
599 1598
    {
600 1598
        $this->type('decimal');
601 1598
602
        empty($precision) && throw new SchemaException('Invalid precision value');
603
604 1598
        $this->precision = $precision;
605
        $this->scale = $scale;
606 1598
607 380
        return $this;
608
    }
609 1554
610 1554
    public function sqlStatement(DriverInterface $driver): string
611
    {
612 16
        $statement = [$driver->identifier($this->name), $this->type];
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

612
        $statement = [$driver->/** @scrutinizer ignore-call */ identifier($this->name), $this->type];
Loading history...
613
614
        if (static::isEnum($this)) {
615 1598
            //Enum specific column options
616
            if (!empty($enumDefinition = $this->quoteEnum($driver))) {
617
                $statement[] = $enumDefinition;
618 1598
            }
619 154
        } elseif (!empty($this->precision)) {
620
            $statement[] = "({$this->precision}, {$this->scale})";
621
        } elseif (!empty($this->size)) {
622
            $statement[] = "({$this->size})";
623 1598
        }
624
625
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
626
627
        if ($this->defaultValue !== null) {
628
            $statement[] = "DEFAULT {$this->quoteDefault($driver)}";
629 152
        }
630
631 152
        return \implode(' ', $statement);
632 152
    }
633 152
634
    public function compare(self $initial): bool
635
    {
636 152
        $normalized = clone $initial;
637
638
        // soft compare, todo: improve
639
        if ($this == $normalized) {
640
            return true;
641
        }
642 846
643
        $columnVars = get_object_vars($this);
644 846
        $dbColumnVars = get_object_vars($normalized);
645 846
646
        $difference = [];
647
        foreach ($columnVars as $name => $value) {
648
            if (\in_array($name, static::EXCLUDE_FROM_COMPARE, true)) {
649 846
                continue;
650 578
            }
651 578
652 578
            if ($name === 'type') {
653
                // user defined type
654
                if (!isset($this->mapping[$this->type]) && $this->type === $this->userType) {
655
                    continue;
656
                }
657 798
            }
658 410
659 293
            if ($name === 'defaultValue') {
660 83
                //Default values has to compared using type-casted value
661 798
                if ($this->getDefaultValue() != $initial->getDefaultValue()) {
662
                    $difference[] = $name;
663
                } elseif (
664
                    $this->getDefaultValue() !== $initial->getDefaultValue()
665
                    && (!\is_object($this->getDefaultValue()) && !\is_object($initial->getDefaultValue()))
666
                ) {
667
                    $difference[] = $name;
668
                }
669
670
                continue;
671
            }
672 682
673
            if ($value !== $dbColumnVars[$name]) {
674
                $difference[] = $name;
675
            }
676 682
        }
677
678 578
        return empty($difference);
679
    }
680
681 634
    public function isReadonlySchema(): bool
682 8
    {
683
        return $this->getAttributes()['readonlySchema'] ?? false;
684 634
    }
685
686 32
    /**
687 32
     * Get database specific enum type definition options.
688
     */
689 632
    protected function quoteEnum(DriverInterface $driver): string
690
    {
691
        $enumValues = [];
692
        foreach ($this->enumValues as $value) {
693 634
            $enumValues[] = $driver->quote($value);
694 32
        }
695 301
696
        return !empty($enumValues) ? '(' . implode(', ', $enumValues) . ')' : '';
697 634
    }
698
699
    /**
700
     * Must return driver specific default value.
701
     */
702
    protected function quoteDefault(DriverInterface $driver): string
703
    {
704
        $defaultValue = $this->getDefaultValue();
705
        if ($defaultValue === null) {
706
            return 'NULL';
707
        }
708
709
        if ($defaultValue instanceof FragmentInterface) {
710
            return $driver->getQueryCompiler()->compile(
711
                new QueryParameters(),
712
                '',
713
                $defaultValue
714
            );
715
        }
716
717
        return match ($this->getType()) {
718
            'bool' => $defaultValue ? 'TRUE' : 'FALSE',
719
            'float' => sprintf('%F', $defaultValue),
720
            'int' => (string) $defaultValue,
721
            default => $driver->quote($defaultValue)
722
        };
723
    }
724
725
    /**
726
     * Ensure that datetime fields are correctly formatted.
727
     *
728
     * @psalm-param non-empty-string $type
729
     *
730
     * @throws DefaultValueException
731
     */
732
    protected function formatDatetime(
733
        string $type,
734
        string|int|\DateTimeInterface $value
735
    ): \DateTimeInterface|FragmentInterface|string {
736
        if ($value === static::DATETIME_NOW) {
737
            //Dynamic default value
738
            return new Fragment($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type DateTimeInterface; however, parameter $fragment of Cycle\Database\Injection\Fragment::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

738
            return new Fragment(/** @scrutinizer ignore-type */ $value);
Loading history...
739
        }
740
741
        if ($value instanceof \DateTimeInterface) {
742
            $datetime = clone $value;
743
        } else {
744
            if (is_numeric($value)) {
745
                //Presumably timestamp
746
                $datetime = new DateTimeImmutable('now', $this->timezone);
747
                $datetime = $datetime->setTimestamp($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type string; however, parameter $timestamp of DateTimeImmutable::setTimestamp() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

747
                $datetime = $datetime->setTimestamp(/** @scrutinizer ignore-type */ $value);
Loading history...
748
            } else {
749
                $datetime = new DateTimeImmutable($value, $this->timezone);
750
            }
751
        }
752
753
        return match ($type) {
754
            'datetime', 'timestamp' => $datetime,
755
            'time' => $datetime->format(static::TIME_FORMAT),
756
            'date' => $datetime->format(static::DATE_FORMAT),
757
            default => $value
758
        };
759
    }
760
761
    protected static function isEnum(self $column): bool
762
    {
763
        return $column->getAbstractType() === 'enum';
764
    }
765
}
766