Passed
Push — 2.x ( 63acb6...208149 )
by Maxim
17:56
created

PostgresColumn::isJson()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 6
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\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'       => ['type' => 'character varying', 'size' => 255],
143 366
144
        //Generic types
145
        'text'         => 'text',
146
        'mediumText'   => 'text',
147
        'tinyText'     => 'text',
148
        'longText'     => 'text',
149
150 366
        //Real types
151
        'double'       => 'double precision',
152
        'float'        => 'real',
153 2
154
        //Decimal type (mapped via method)
155 2
        'decimal'      => 'numeric',
156
157
        //Date and Time types
158
        'datetime'     => 'timestamp',
159
        'date'         => 'date',
160
        'time'         => 'time',
161
        'timetz'       => ['type' => 'time', 'withTimezone' => true],
162 2
        'timestamp'    => 'timestamp',
163
        'timestamptz'  => ['type' => 'timestamp', 'withTimezone' => true],
164
        'interval'     => 'interval',
165 156
166
        //Binary types
167 156
        'binary'       => 'bytea',
168
        'tinyBinary'   => 'bytea',
169 156
        'longBinary'   => 'bytea',
170 156
171 156
        //Bit-string
172
        'bit'          => ['type' => 'bit', 'size' => 1],
173
        'bit varying'  => 'bit varying',
174 156
175
        //Ranges
176
        'int4range'    => 'int4range',
177
        'int8range'    => 'int8range',
178
        'numrange'     => 'numrange',
179
        'tsrange'      => 'tsrange',
180 514
        'tstzrange'    => ['type' => 'tstzrange', 'withTimezone' => true],
181
        'daterange'    => 'daterange',
182 514
183
        //Additional types
184 514
        'json'         => 'json',
185
        'jsonb'        => 'jsonb',
186 506
        'uuid'         => 'uuid',
187
        'point'        => 'point',
188
        'line'         => 'line',
189
        'lseg'         => 'lseg',
190 154
        'box'          => 'box',
191 154
        'path'         => 'path',
192 154
        'polygon'      => 'polygon',
193
        'circle'       => 'circle',
194
        'cidr'         => 'cidr',
195 154
        'inet'         => 'inet',
196 154
        'macaddr'      => 'macaddr',
197 154
        'macaddr8'     => 'macaddr8',
198
        'tsvector'     => 'tsvector',
199 154
        'tsquery'      => 'tsquery',
200
    ];
201
202
    protected array $reverseMapping = [
203
        'smallPrimary' => [['type' => 'smallserial', 'isPrimary' => true]],
204
        'primary'      => [['type' => 'serial', 'isPrimary' => true]],
205 40
        'bigPrimary'   => [['type' => 'bigserial', 'isPrimary' => true]],
206
        'smallserial'  => [['type' => 'smallserial', 'isPrimary' => false]],
207 40
        'serial'       => [['type' => 'serial', 'isPrimary' => false]],
208
        'bigserial'    => [['type' => 'bigserial', 'isPrimary' => false]],
209
        'enum'         => ['enum'],
210 40
        'boolean'      => ['boolean'],
211 40
        'integer'      => ['int', 'integer', 'int4', 'int4range'],
212
        'tinyInteger'  => ['smallint'],
213 40
        'smallInteger' => ['smallint'],
214
        'bigInteger'   => ['bigint', 'int8', 'int8range'],
215
        'string'       => [
216
            'character varying',
217
            'character',
218 40
            'char',
219 18
            'point',
220
            'line',
221 2
            'lseg',
222 2
            'box',
223 2
            'path',
224
            'polygon',
225
            'circle',
226 2
            'cidr',
227
            'inet',
228 16
            'macaddr',
229
            'macaddr8',
230 16
            'tsvector',
231 6
            'tsquery',
232 12
        ],
233
        'text'         => ['text'],
234
        'double'       => ['double precision'],
235
        'float'        => ['real', 'money'],
236
        'decimal'      => ['numeric', 'numrange'],
237 16
        'date'         => ['date', 'daterange'],
238
        'time'         => [['type' => 'time', 'withTimezone' => false]],
239
        'timetz'       => [['type' => 'time', 'withTimezone' => true]],
240
        'timestamp'    => [
241
            ['type' => 'timestamp', 'withTimezone' => false],
242 40
            ['type' => 'tsrange', 'withTimezone' => false],
243 6
        ],
244
        'timestamptz'  => [
245
            ['type' => 'timestamp', 'withTimezone' => true],
246
            ['type' => 'tstzrange', 'withTimezone' => true],
247 40
        ],
248 10
        'binary'       => ['bytea'],
249 2
        'json'         => ['json'],
250
        'jsonb'        => ['jsonb'],
251 8
        'interval'     => ['interval'],
252
        'bit'          => ['bit', 'bit varying'],
253
    ];
254
255
    #[ColumnAttribute([
256 40
        'character varying',
257 4
        'bit',
258
        'bit varying',
259
        'datetime',
260 40
        'time',
261 6
        'timetz',
262 6
        'timestamp',
263 6
        'timestamptz',
264
    ])]
265
    protected int $size = 0;
266 6
267 6
    /**
268
     * Field is auto incremental.
269
     *
270 40
     * @deprecated since v2.5.0
271
     */
272
    protected bool $autoIncrement = false;
273
274
    /**
275
     * Indication that column has enum constrain.
276
     */
277
    protected bool $constrained = false;
278 516
279
    /**
280
     * Name of enum constraint associated with field.
281
     */
282
    protected string $constrainName = '';
283 516
284
    #[ColumnAttribute(['timestamp', 'time', 'timestamptz', 'timetz', 'tsrange', 'tstzrange'])]
285 516
    protected bool $withTimezone = false;
286 516
287 516
    #[ColumnAttribute(['interval'])]
288
    protected ?string $intervalType = null;
289
290 516
    #[ColumnAttribute(['numeric'])]
291 516
    protected int $precision = 0;
292 516
293
    #[ColumnAttribute(['numeric'])]
294 368
    protected int $scale = 0;
295 368
296
    /**
297 368
     * Internal field to determine if the serial is PK.
298
     */
299 368
    protected bool $isPrimary = false;
300
301
    public function getConstraints(): array
302 470
    {
303 264
        $constraints = parent::getConstraints();
304
305
        if ($this->constrained) {
306 470
            $constraints[] = $this->constrainName;
307 14
        }
308 14
309
        return $constraints;
310
    }
311 470
312 2
    /**
313
     * @psalm-return non-empty-string
314
     */
315
    public function getAbstractType(): string
316
    {
317
        return !empty($this->enumValues) ? 'enum' : parent::getAbstractType();
318 2
    }
319
320
    public function smallPrimary(): AbstractColumn
321 470
    {
322
        if (!empty($this->type) && $this->type !== 'smallserial') {
323 264
            //Change type of already existed column (we can't use "serial" alias here)
324
            $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...
325
326 470
            return $this;
327
        }
328 470
329
        return $this->type('smallPrimary');
330
    }
331 516
332
    public function primary(): AbstractColumn
333 516
    {
334 506
        if (!empty($this->type) && $this->type !== 'serial') {
335
            //Change type of already existed column (we can't use "serial" alias here)
336
            $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...
337
338 370
            return $this;
339 370
        }
340
341
        return $this->type('primary');
342
    }
343
344
    public function bigPrimary(): AbstractColumn
345
    {
346
        if (!empty($this->type) && $this->type !== 'bigserial') {
347
            //Change type of already existed column (we can't use "serial" alias here)
348
            $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...
349
350
            return $this;
351 154
        }
352
353
        return $this->type('bigPrimary');
354 154
    }
355
356
    public function enum(string|array $values): AbstractColumn
357
    {
358
        $this->enumValues = array_map('strval', \is_array($values) ? $values : \func_get_args());
0 ignored issues
show
introduced by
The condition is_array($values) is always true.
Loading history...
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...
359
360 156
        $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...
361
        foreach ($this->enumValues as $value) {
362 156
            $this->size = max((int)$this->size, \strlen($value));
363 156
        }
364
365
        return $this;
366 156
    }
367
368
    public function interval(int $size = 6, ?string $intervalType = null): AbstractColumn
369
    {
370
        if ($intervalType !== null && !\in_array($intervalType, self::INTERVALS_WITH_ALLOWED_PRECISION, true)) {
371
            $size = 0;
372 470
        }
373
374 470
        $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...
375 402
        $this->size = $size;
376
        $this->intervalType = $intervalType;
377
378 214
        return $this;
379
    }
380 188
381 160
    /**
382
     * @psalm-return non-empty-string
383
     */
384
    public function sqlStatement(DriverInterface $driver): string
385 160
    {
386 138
        $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

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

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