Passed
Pull Request — 2.x (#138)
by Aleksei
18:44
created

PostgresColumn   F

Complexity

Total Complexity 99

Size/Duplication

Total Lines 662
Duplicated Lines 0 %

Test Coverage

Coverage 91.55%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 342
dl 0
loc 662
ccs 130
cts 142
cp 0.9155
rs 2
c 1
b 1
f 0
wmc 99

17 Methods

Rating   Name   Duplication   Size   Complexity  
A enum() 0 10 3
A getAbstractType() 0 3 2
A interval() 0 11 3
A primary() 0 10 3
A smallPrimary() 0 10 3
A compare() 0 9 3
C alterOperations() 0 66 14
A quoteEnum() 0 4 1
A getConstraints() 0 9 2
F sqlStatement() 0 53 17
A bigPrimary() 0 10 3
F createInstance() 0 91 26
A resolveEnum() 0 12 2
A enumConstraint() 0 7 2
B normalizeDefault() 0 22 8
A isJson() 0 3 2
A resolveConstrains() 0 32 5

How to fix   Complexity   

Complex Class

Complex classes like PostgresColumn often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PostgresColumn, and based on these observations, apply Extract Interface, too.

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\Postgres\Schema;
13
14
use Cycle\Database\Driver\DriverInterface;
15
use Cycle\Database\Exception\SchemaException;
16
use Cycle\Database\Injection\Fragment;
17
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...
18
use Cycle\Database\Schema\Attribute\ColumnAttribute;
19
20
/**
21
 * @method $this timestamptz(int $size = 0)
22
 * @method $this timetz()
23
 * @method $this bitVarying(int $size = 0)
24
 * @method $this bit(int $size = 1)
25
 * @method $this int4range()
26
 * @method $this int8range()
27
 * @method $this numrange()
28
 * @method $this tsrange()
29
 * @method $this tstzrange()
30
 * @method $this daterange()
31
 * @method $this point()
32
 * @method $this line()
33
 * @method $this lseg()
34
 * @method $this box()
35
 * @method $this path()
36
 * @method $this polygon()
37
 * @method $this circle()
38
 * @method $this cidr()
39
 * @method $this inet()
40
 * @method $this macaddr()
41
 * @method $this macaddr8()
42
 * @method $this tsvector()
43
 * @method $this tsquery()
44
 * @method $this smallSerial()
45
 * @method $this serial()
46
 * @method $this bigSerial()
47
 */
48
class PostgresColumn extends AbstractColumn
49
{
50
    private const WITH_TIMEZONE = 'with time zone';
51
    private const WITHOUT_TIMEZONE = 'without time zone';
52
53
    private const SERIAL_TYPES = [
54
        'smallPrimary',
55
        'primary',
56
        'bigPrimary',
57
        'smallserial',
58
        'serial',
59
        'bigserial',
60
    ];
61
62
    protected const INTEGER_TYPES = ['int', 'bigint', 'integer', 'smallint'];
63
64
    /**
65
     * Default timestamp expression (driver specific).
66
     */
67
    public const DATETIME_NOW = 'now()';
68
    public const DATETIME_PRECISION = 6;
69
70
    /**
71
     * Private state related values.
72
     */
73
    public const EXCLUDE_FROM_COMPARE = [
74
        'userType',
75
        'timezone',
76
        'constrained',
77
        'constrainName',
78
        'attributes',
79
    ];
80
81
    protected const INTERVAL_TYPES = [
82
        'YEAR',
83
        'MONTH',
84
        'DAY',
85
        'HOUR',
86
        'MINUTE',
87
        'SECOND',
88
        'YEAR TO MONTH',
89
        'DAY TO HOUR',
90
        'DAY TO MINUTE',
91
        'DAY TO SECOND',
92
        'HOUR TO MINUTE',
93
        'HOUR TO SECOND',
94
        'MINUTE TO SECOND',
95
    ];
96
97
    protected const INTERVALS_WITH_ALLOWED_PRECISION = [
98
        'SECOND',
99
        'DAY TO SECOND',
100
        'HOUR TO SECOND',
101
        'MINUTE TO SECOND',
102
    ];
103
104
    protected array $aliases = [
105
        'int'            => 'integer',
106
        'smallint'       => 'smallInteger',
107
        'bigint'         => 'bigInteger',
108
        'incremental'    => 'primary',
109
        'bigIncremental' => 'bigPrimary',
110
        'bool'           => 'boolean',
111
        'blob'           => 'binary',
112
        'bitVarying'     => 'bit varying',
113
        'smallSerial'    => 'smallserial',
114
        'bigSerial'      => 'bigserial',
115
    ];
116
117
    protected array $mapping = [
118
        //Primary sequences
119
        'smallPrimary' => ['type' => 'smallserial', 'nullable' => false, 'isPrimary' => true],
120
        'primary'      => ['type' => 'serial', 'nullable' => false, 'isPrimary' => true],
121
        'bigPrimary'   => ['type' => 'bigserial', 'nullable' => false, 'isPrimary' => true],
122 8
123
        //Serial
124 8
        'smallserial' => ['type' => 'smallserial', 'nullable' => false],
125
        'serial'      => ['type' => 'serial', 'nullable' => false],
126 8
        'bigserial'   => ['type' => 'bigserial', 'nullable' => false],
127
128
        //Enum type (mapped via method)
129
        'enum'         => 'enum',
130 8
131
        //Logical types
132
        'boolean'      => 'boolean',
133
134
        //Integer types (size can always be changed with size method), longInteger has method alias
135
        //bigInteger
136 516
        'integer'      => 'integer',
137
        'tinyInteger'  => 'smallint',
138 516
        'smallInteger' => 'smallint',
139
        'bigInteger'   => 'bigint',
140
141 366
        //String with specified length (mapped via method)
142
        'string'       => 'character varying',
143 366
144
        //Generic types
145
        'text'         => 'text',
146
        'tinyText'     => 'text',
147
        'longText'     => 'text',
148
149
        //Real types
150 366
        'double'       => 'double precision',
151
        'float'        => 'real',
152
153 2
        //Decimal type (mapped via method)
154
        'decimal'      => 'numeric',
155 2
156
        //Date and Time types
157
        'datetime'     => 'timestamp',
158
        'date'         => 'date',
159
        'time'         => 'time',
160
        'timetz'       => ['type' => 'time', 'withTimezone' => true],
161
        'timestamp'    => 'timestamp',
162 2
        'timestamptz'  => ['type' => 'timestamp', 'withTimezone' => true],
163
        'interval'     => 'interval',
164
165 156
        //Binary types
166
        'binary'       => 'bytea',
167 156
        'tinyBinary'   => 'bytea',
168
        'longBinary'   => 'bytea',
169 156
170 156
        //Bit-string
171 156
        'bit'          => ['type' => 'bit', 'size' => 1],
172
        'bit varying'  => 'bit varying',
173
174 156
        //Ranges
175
        'int4range'    => 'int4range',
176
        'int8range'    => 'int8range',
177
        'numrange'     => 'numrange',
178
        'tsrange'      => 'tsrange',
179
        'tstzrange'    => ['type' => 'tstzrange', 'withTimezone' => true],
180 514
        'daterange'    => 'daterange',
181
182 514
        //Additional types
183
        'json'         => 'json',
184 514
        'jsonb'        => 'jsonb',
185
        'uuid'         => 'uuid',
186 506
        'point'        => 'point',
187
        'line'         => 'line',
188
        'lseg'         => 'lseg',
189
        'box'          => 'box',
190 154
        'path'         => 'path',
191 154
        'polygon'      => 'polygon',
192 154
        'circle'       => 'circle',
193
        'cidr'         => 'cidr',
194
        'inet'         => 'inet',
195 154
        'macaddr'      => 'macaddr',
196 154
        'macaddr8'     => 'macaddr8',
197 154
        'tsvector'     => 'tsvector',
198
        'tsquery'      => 'tsquery',
199 154
    ];
200
201
    protected array $reverseMapping = [
202
        'smallPrimary' => [['type' => 'smallserial', 'isPrimary' => true]],
203
        'primary'      => [['type' => 'serial', 'isPrimary' => true]],
204
        'bigPrimary'   => [['type' => 'bigserial', 'isPrimary' => true]],
205 40
        'smallserial'  => [['type' => 'smallserial', 'isPrimary' => false]],
206
        'serial'       => [['type' => 'serial', 'isPrimary' => false]],
207 40
        'bigserial'    => [['type' => 'bigserial', 'isPrimary' => false]],
208
        'enum'         => ['enum'],
209
        'boolean'      => ['boolean'],
210 40
        'integer'      => ['int', 'integer', 'int4', 'int4range'],
211 40
        'tinyInteger'  => ['smallint'],
212
        'smallInteger' => ['smallint'],
213 40
        'bigInteger'   => ['bigint', 'int8', 'int8range'],
214
        'string'       => [
215
            'character varying',
216
            'character',
217
            'char',
218 40
            'point',
219 18
            'line',
220
            'lseg',
221 2
            'box',
222 2
            'path',
223 2
            'polygon',
224
            'circle',
225
            'cidr',
226 2
            'inet',
227
            'macaddr',
228 16
            'macaddr8',
229
            'tsvector',
230 16
            'tsquery',
231 6
        ],
232 12
        'text'         => ['text'],
233
        'double'       => ['double precision'],
234
        'float'        => ['real', 'money'],
235
        'decimal'      => ['numeric', 'numrange'],
236
        'date'         => ['date', 'daterange'],
237 16
        'time'         => [['type' => 'time', 'withTimezone' => false]],
238
        'timetz'       => [['type' => 'time', 'withTimezone' => true]],
239
        'timestamp'    => [
240
            ['type' => 'timestamp', 'withTimezone' => false],
241
            ['type' => 'tsrange', 'withTimezone' => false],
242 40
        ],
243 6
        'timestamptz'  => [
244
            ['type' => 'timestamp', 'withTimezone' => true],
245
            ['type' => 'tstzrange', 'withTimezone' => true],
246
        ],
247 40
        'binary'       => ['bytea'],
248 10
        'json'         => ['json'],
249 2
        'jsonb'        => ['jsonb'],
250
        'interval'     => ['interval'],
251 8
        'bit'          => ['bit', 'bit varying'],
252
    ];
253
254
    /**
255
     * Field is auto incremental.
256 40
     *
257 4
     * @deprecated since v2.5.0
258
     */
259
    protected bool $autoIncrement = false;
260 40
261 6
    /**
262 6
     * Indication that column has enum constrain.
263 6
     */
264
    protected bool $constrained = false;
265
266 6
    /**
267 6
     * Name of enum constraint associated with field.
268
     */
269
    protected string $constrainName = '';
270 40
271
    #[ColumnAttribute(['timestamp', 'time', 'timestamptz', 'timetz', 'tsrange', 'tstzrange'])]
272
    protected bool $withTimezone = false;
273
274
    #[ColumnAttribute(['interval'])]
275
    protected ?string $intervalType = null;
276
277
    /**
278 516
     * Internal field to determine if the serial is PK.
279
     */
280
    protected bool $isPrimary = false;
281
282
    public function getConstraints(): array
283 516
    {
284
        $constraints = parent::getConstraints();
285 516
286 516
        if ($this->constrained) {
287 516
            $constraints[] = $this->constrainName;
288
        }
289
290 516
        return $constraints;
291 516
    }
292 516
293
    /**
294 368
     * @psalm-return non-empty-string
295 368
     */
296
    public function getAbstractType(): string
297 368
    {
298
        return !empty($this->enumValues) ? 'enum' : parent::getAbstractType();
299 368
    }
300
301
    public function smallPrimary(): AbstractColumn
302 470
    {
303 264
        if (!empty($this->type) && $this->type !== 'smallserial') {
304
            //Change type of already existed column (we can't use "serial" alias here)
305
            $this->type = 'smallint';
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...
306 470
307 14
            return $this;
308 14
        }
309
310
        return $this->type('smallPrimary');
311 470
    }
312 2
313
    public function primary(): AbstractColumn
314
    {
315
        if (!empty($this->type) && $this->type !== 'serial') {
316
            //Change type of already existed column (we can't use "serial" alias here)
317
            $this->type = 'integer';
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...
318 2
319
            return $this;
320
        }
321 470
322
        return $this->type('primary');
323 264
    }
324
325
    public function bigPrimary(): AbstractColumn
326 470
    {
327
        if (!empty($this->type) && $this->type !== 'bigserial') {
328 470
            //Change type of already existed column (we can't use "serial" alias here)
329
            $this->type = 'bigint';
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...
330
331 516
            return $this;
332
        }
333 516
334 506
        return $this->type('bigPrimary');
335
    }
336
337
    public function enum(string|array $values): AbstractColumn
338 370
    {
339 370
        $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...
340
341
        $this->type = 'character varying';
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...
342
        foreach ($this->enumValues as $value) {
343
            $this->size = max((int)$this->size, \strlen($value));
0 ignored issues
show
Bug Best Practice introduced by
The property size does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
344
        }
345
346
        return $this;
347
    }
348
349
    public function interval(int $size = 6, ?string $intervalType = null): AbstractColumn
350
    {
351 154
        if ($intervalType !== null && !\in_array($intervalType, self::INTERVALS_WITH_ALLOWED_PRECISION, true)) {
352
            $size = 0;
353
        }
354 154
355
        $this->type = 'interval';
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...
356
        $this->size = $size;
0 ignored issues
show
Bug Best Practice introduced by
The property size does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
357
        $this->intervalType = $intervalType;
358
359
        return $this;
360 156
    }
361
362 156
    /**
363 156
     * @psalm-return non-empty-string
364
     */
365
    public function sqlStatement(DriverInterface $driver): string
366 156
    {
367
        $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

367
        $statement = [$driver->/** @scrutinizer ignore-call */ identifier($this->name), $this->type];
Loading history...
368
369
        if ($this->intervalType !== null && $this->getAbstractType() === 'interval') {
370
            if (!\in_array($this->intervalType, self::INTERVAL_TYPES, true)) {
371
                throw new SchemaException(\sprintf(
372 470
                    'Invalid interval type value. Valid values for interval type: `%s`.',
373
                    \implode('`, `', self::INTERVAL_TYPES)
374 470
                ));
375 402
            }
376
            $statement[] = $this->intervalType;
377
        }
378 214
379
        if ($this->getAbstractType() === 'enum') {
380 188
            //Enum specific column options
381 160
            if (!empty($enumDefinition = $this->quoteEnum($driver))) {
382
                $statement[] = $enumDefinition;
383
            }
384
        } elseif (!empty($this->precision)) {
385 160
            $statement[] = "({$this->precision}, {$this->scale})";
386 138
        } elseif (!empty($this->size) || $this->type === 'timestamp' || $this->type === 'time') {
387
            $statement[] = "({$this->size})";
388
        }
389 214
390 214
        if ($this->type === 'timestamp' || $this->type === 'time') {
391 152
            $statement[] = $this->withTimezone ? self::WITH_TIMEZONE : self::WITHOUT_TIMEZONE;
392
        }
393 152
394
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
395
396 214
        if ($this->defaultValue !== null) {
397
            $statement[] = "DEFAULT {$this->quoteDefault($driver)}";
398
        }
399
400
        $statement = \implode(' ', $statement);
401 264
402
        //We have to add constraint for enum type
403
        if ($this->getAbstractType() === 'enum') {
404
            $enumValues = [];
405
            foreach ($this->enumValues as $value) {
406 264
                $enumValues[] = $driver->quote($value);
407
            }
408
409 264
            $constrain = $driver->identifier($this->enumConstraint());
410
            $column = $driver->identifier($this->getName());
411
            $values = \implode(', ', $enumValues);
412 264
413 264
            return "{$statement} CONSTRAINT {$constrain} CHECK ($column IN ({$values}))";
414
        }
415
416
        //Nothing special
417 264
        return $statement;
418 156
    }
419 156
420 156
    /**
421 156
     * Generate set of operations need to change column.
422
     */
423 156
    public function alterOperations(DriverInterface $driver, AbstractColumn $initial): array
424
    {
425
        $operations = [];
426 156
427
        //To simplify comparation
428 156
        $currentType = [$this->type, $this->size, $this->precision, $this->scale];
429
        $initialType = [$initial->type, $initial->size, $initial->precision, $initial->scale];
430 156
431 156
        $identifier = $driver->identifier($this->getName());
432 156
433
        /*
434
         * This block defines column type and all variations.
435 264
         */
436
        if ($currentType !== $initialType) {
437
            if ($this->getAbstractType() === 'enum') {
438
                //Getting longest value
439
                $enumSize = $this->size;
440 2
                foreach ($this->enumValues as $value) {
441
                    $enumSize = max($enumSize, strlen($value));
442 2
                }
443
444 2
                $operations[] = "ALTER COLUMN {$identifier} TYPE character varying($enumSize)";
445
            } else {
446 2
                $type = "ALTER COLUMN {$identifier} TYPE {$this->type}";
447
448
                if (!empty($this->size)) {
449
                    $type .= "($this->size)";
450
                } elseif (!empty($this->precision)) {
451
                    $type .= "($this->precision, $this->scale)";
452
                }
453
454 2
                //Required to perform cross conversion
455
                $operations[] = "{$type} USING {$identifier}::{$this->type}";
456
            }
457
        }
458
459
        //Dropping enum constrain before any operation
460
        if ($this->constrained && $initial->getAbstractType() === 'enum') {
461
            $operations[] = 'DROP CONSTRAINT ' . $driver->identifier($this->enumConstraint());
462
        }
463
464
        //Default value set and dropping
465
        if ($initial->defaultValue !== $this->defaultValue) {
466
            if ($this->defaultValue === null) {
467
                $operations[] = "ALTER COLUMN {$identifier} DROP DEFAULT";
468
            } else {
469
                $operations[] = "ALTER COLUMN {$identifier} SET DEFAULT {$this->quoteDefault($driver)}";
470
            }
471
        }
472
473
        //Nullable option
474
        if ($initial->nullable !== $this->nullable) {
475
            $operations[] = "ALTER COLUMN {$identifier} " . (!$this->nullable ? 'SET' : 'DROP') . ' NOT NULL';
476
        }
477
478
        if ($this->getAbstractType() === 'enum') {
479
            $enumValues = [];
480
            foreach ($this->enumValues as $value) {
481
                $enumValues[] = $driver->quote($value);
482
            }
483
484
            $operations[] = "ADD CONSTRAINT {$driver->identifier($this->enumConstraint())} "
485
                . "CHECK ({$identifier} IN (" . implode(', ', $enumValues) . '))';
486
        }
487
488
        return $operations;
489
    }
490
491
    /**
492
     * @psalm-param non-empty-string $table Table name.
493
     *
494
     * @param DriverInterface $driver Postgres columns are bit more complex.
495
     */
496
    public static function createInstance(
497
        string $table,
498
        array $schema,
499
        DriverInterface $driver
500
    ): self {
501
        $column = new self($table, $schema['column_name'], $driver->getTimezone());
502
503
        $column->type = match (true) {
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...
504
            $schema['typname'] === 'timestamp' || $schema['typname'] === 'timestamptz' => 'timestamp',
505
            $schema['typname'] === 'date' => 'date',
506
            $schema['typname'] === 'time' || $schema['typname'] === 'timetz' => 'time',
507
            default => $schema['data_type']
508
        };
509
510
        $column->defaultValue = $schema['column_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...
511
        $column->nullable = $schema['is_nullable'] === '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...
512
513
        if (
514
            \is_string($column->defaultValue)
515
            && \in_array($column->type, self::INTEGER_TYPES)
516
            && \preg_match('/nextval(.*)/', $column->defaultValue)
517
        ) {
518
            $column->type = match (true) {
519
                $column->type === 'bigint' => 'bigserial',
520
                $column->type === 'smallint' => 'smallserial',
521
                default => 'serial'
522
            };
523
            $column->autoIncrement = true;
0 ignored issues
show
Deprecated Code introduced by
The property Cycle\Database\Driver\Po...sColumn::$autoIncrement has been deprecated: since v2.5.0 ( Ignorable by Annotation )

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

523
            /** @scrutinizer ignore-deprecated */ $column->autoIncrement = true;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
524
525
            $column->defaultValue = new Fragment($column->defaultValue);
526
527
            if ($schema['is_primary']) {
528
                $column->isPrimary = true;
529
            }
530
531
            return $column;
532
        }
533
534
        if ($schema['character_maximum_length'] !== null && str_contains($column->type, 'char')) {
535
            $column->size = (int) $schema['character_maximum_length'];
0 ignored issues
show
Bug Best Practice introduced by
The property size does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
536
        }
537
538
        if ($column->type === 'numeric') {
539
            $column->precision = (int) $schema['numeric_precision'];
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...
540
            $column->scale = (int) $schema['numeric_scale'];
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...
541
        }
542
543
        if ($column->type === 'USER-DEFINED' && $schema['typtype'] === 'e') {
544
            $column->type = $schema['typname'];
545
546
            /**
547
             * Attention, this is not default enum type emulated via CHECK.
548
             * This is real Postgres enum type.
549
             */
550
            self::resolveEnum($driver, $column);
551
        }
552
553
        if ($column->type === 'timestamp' || $column->type === 'time' || $column->type === 'interval') {
554
            $column->size = (int) $schema['datetime_precision'];
555
        }
556
557
        if (
558
            $schema['typname'] === 'timestamptz' ||
559
            $schema['typname'] === 'timetz' ||
560
            $schema['typname'] === 'tstzrange'
561
        ) {
562
            $column->withTimezone = true;
563
        }
564
565
        if (!empty($column->size) && str_contains($column->type, 'char')) {
566
            //Potential enum with manually created constraint (check in)
567
            self::resolveConstrains($driver, $schema, $column);
568
        }
569
570
        if ($column->type === 'interval' && \is_string($schema['interval_type'])) {
571
            $column->intervalType = \str_replace(\sprintf('(%s)', $column->size), '', $schema['interval_type']);
572
            if (!in_array($column->intervalType, self::INTERVALS_WITH_ALLOWED_PRECISION, true)) {
573
                $column->size = 0;
574
            }
575
        }
576
577
        if (
578
            ($column->type === 'bit' || $column->type === 'bit varying') &&
579
            isset($schema['character_maximum_length'])
580
        ) {
581
            $column->size = (int) $schema['character_maximum_length'];
582
        }
583
584
        $column->normalizeDefault();
585
586
        return $column;
587
    }
588
589
    public function compare(AbstractColumn $initial): bool
590
    {
591
        if (parent::compare($initial)) {
592
            return true;
593
        }
594
595
        return (bool) (
596
            \in_array($this->getAbstractType(), self::SERIAL_TYPES, true)
597
            && $initial->getDefaultValue() != $this->getDefaultValue()
598
        );
599
    }
600
601
    /**
602
     * @psalm-return non-empty-string
603
     */
604
    protected function quoteEnum(DriverInterface $driver): string
605
    {
606
        //Postgres enums are just constrained strings
607
        return '(' . $this->size . ')';
608
    }
609
610
    protected static function isJson(AbstractColumn $column): bool
611
    {
612
        return $column->getAbstractType() === 'json' || $column->getAbstractType() === 'jsonb';
613
    }
614
615
    /**
616
     * Get/generate name for enum constraint.
617
     */
618
    private function enumConstraint(): string
619
    {
620
        if (empty($this->constrainName)) {
621
            $this->constrainName = str_replace('.', '_', $this->table) . '_' . $this->getName() . '_enum_' . uniqid();
622
        }
623
624
        return $this->constrainName;
625
    }
626
627
    /**
628
     * Normalize default value.
629
     */
630
    private function normalizeDefault(): void
631
    {
632
        if (!$this->hasDefaultValue()) {
633
            return;
634
        }
635
636
        if (preg_match('/^\'?(.*?)\'?::(.+)/', $this->defaultValue, $matches)) {
637
            //In database: 'value'::TYPE
638
            $this->defaultValue = $matches[1];
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...
639
        } elseif ($this->type === 'bit') {
640
            $this->defaultValue = bindec(
641
                substr($this->defaultValue, 2, strpos($this->defaultValue, '::') - 3)
642
            );
643
        } elseif ($this->type === 'boolean') {
644
            $this->defaultValue = (strtolower($this->defaultValue) === 'true');
645
        }
646
647
        $type = $this->getType();
648
        if ($type === self::FLOAT || $type === self::INT) {
649
            if (preg_match('/^\(?(.*?)\)?(?!::(.+))?$/', $this->defaultValue, $matches)) {
650
                //Negative numeric values
651
                $this->defaultValue = $matches[1];
652
            }
653
        }
654
    }
655
656
    /**
657
     * Resolving enum constrain and converting it into proper enum values set.
658
     */
659
    private static function resolveConstrains(
660
        DriverInterface $driver,
661
        array $schema,
662
        self $column
663
    ): void {
664
        $query = "SELECT conname, pg_get_constraintdef(oid) as consrc FROM pg_constraint
665
        WHERE conrelid = ? AND contype = 'c' AND conkey = ?";
666
667
        $constraints = $driver->query(
668
            $query,
669
            [
670
                $schema['tableOID'],
671
                '{' . $schema['dtd_identifier'] . '}',
672
            ]
673
        );
674
675
        foreach ($constraints as $constraint) {
676
            if (preg_match('/ARRAY\[([^\]]+)\]/', $constraint['consrc'], $matches)) {
677
                $enumValues = explode(',', $matches[1]);
678
                foreach ($enumValues as &$value) {
679
                    if (preg_match("/^'?(.*?)'?::(.+)/", trim($value, ' ()'), $matches)) {
680
                        //In database: 'value'::TYPE
681
                        $value = $matches[1];
682
                    }
683
684
                    unset($value);
685
                }
686
                unset($value);
687
688
                $column->enumValues = $enumValues;
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...
689
                $column->constrainName = $constraint['conname'];
690
                $column->constrained = true;
691
            }
692
        }
693
    }
694
695
    /**
696
     * Resolve native ENUM type if presented.
697
     */
698
    private static function resolveEnum(DriverInterface $driver, self $column): void
699
    {
700
        $range = $driver->query('SELECT enum_range(NULL::' . $column->type . ')')->fetchColumn(0);
701
702
        $column->enumValues = explode(',', substr($range, 1, -1));
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...
703
704
        if (!empty($column->defaultValue)) {
705
            //In database: 'value'::enumType
706
            $column->defaultValue = substr(
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...
707
                $column->defaultValue,
708
                1,
709
                strpos($column->defaultValue, $column->type) - 4
710
            );
711
        }
712
    }
713
}
714