PostgresColumn::compare()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 9
ccs 0
cts 0
cp 0
crap 12
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\Injection\Fragment;
15
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...
16
use Cycle\Database\Driver\DriverInterface;
17
use Cycle\Database\Exception\SchemaException;
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
 * @method $this comment(string $value)
48
 */
49
class PostgresColumn extends AbstractColumn
50
{
51
    private const WITH_TIMEZONE = 'with time zone';
52
    private const WITHOUT_TIMEZONE = 'without time zone';
53
    private const SERIAL_TYPES = [
54
        'smallPrimary',
55
        'primary',
56
        'bigPrimary',
57
        'smallserial',
58
        'serial',
59
        'bigserial',
60
    ];
61
    protected const INTEGER_TYPES = ['int', 'bigint', 'integer', 'smallint'];
62
63
    /**
64
     * Default timestamp expression (driver specific).
65
     */
66
    public const DATETIME_NOW = 'now()';
67
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
    protected const INTERVALS_WITH_ALLOWED_PRECISION = [
97
        'SECOND',
98
        'DAY TO SECOND',
99
        'HOUR TO SECOND',
100
        'MINUTE TO SECOND',
101
    ];
102
103
    protected array $aliases = [
104
        'int'            => 'integer',
105
        'smallint'       => 'smallInteger',
106
        'bigint'         => 'bigInteger',
107
        'incremental'    => 'primary',
108
        'bigIncremental' => 'bigPrimary',
109
        'bool'           => 'boolean',
110
        'blob'           => 'binary',
111
        'bitVarying'     => 'bit varying',
112
        'smallSerial'    => 'smallserial',
113
        'bigSerial'      => 'bigserial',
114
    ];
115
    protected array $mapping = [
116
        //Primary sequences
117
        'smallPrimary' => ['type' => 'smallserial', 'nullable' => false, 'isPrimary' => true],
118
        'primary'      => ['type' => 'serial', 'nullable' => false, 'isPrimary' => true],
119
        'bigPrimary'   => ['type' => 'bigserial', 'nullable' => false, 'isPrimary' => true],
120
121
        //Serial
122 8
        'smallserial' => ['type' => 'smallserial', 'nullable' => false],
123
        'serial'      => ['type' => 'serial', 'nullable' => false],
124 8
        'bigserial'   => ['type' => 'bigserial', 'nullable' => false],
125
126 8
        //Enum type (mapped via method)
127
        'enum'         => 'enum',
128
129
        //Logical types
130 8
        'boolean'      => 'boolean',
131
132
        //Integer types (size can always be changed with size method), longInteger has method alias
133
        //bigInteger
134
        'integer'      => 'integer',
135
        'tinyInteger'  => 'smallint',
136 516
        'smallInteger' => 'smallint',
137
        'bigInteger'   => 'bigint',
138 516
139
        //String with specified length (mapped via method)
140
        'string'       => ['type' => 'character varying', 'size' => 255],
141 366
142
        //Generic types
143 366
        'text'         => 'text',
144
        'mediumText'   => 'text',
145
        'tinyText'     => 'text',
146
        'longText'     => 'text',
147
148
        //Real types
149
        'double'       => 'double precision',
150 366
        'float'        => 'real',
151
152
        //Decimal type (mapped via method)
153 2
        'decimal'      => 'numeric',
154
155 2
        //Date and Time types
156
        'datetime'     => 'timestamp',
157
        'date'         => 'date',
158
        'time'         => 'time',
159
        'timetz'       => ['type' => 'time', 'withTimezone' => true],
160
        'timestamp'    => 'timestamp',
161
        'timestamptz'  => ['type' => 'timestamp', 'withTimezone' => true],
162 2
        'interval'     => 'interval',
163
164
        //Binary types
165 156
        'binary'       => 'bytea',
166
        'tinyBinary'   => 'bytea',
167 156
        'longBinary'   => 'bytea',
168
169 156
        //Bit-string
170 156
        'bit'          => ['type' => 'bit', 'size' => 1],
171 156
        'bit varying'  => 'bit varying',
172
173
        //Ranges
174 156
        'int4range'    => 'int4range',
175
        'int8range'    => 'int8range',
176
        'numrange'     => 'numrange',
177
        'tsrange'      => 'tsrange',
178
        'tstzrange'    => ['type' => 'tstzrange', 'withTimezone' => true],
179
        'daterange'    => 'daterange',
180 514
181
        //Additional types
182 514
        'json'         => 'json',
183
        'jsonb'        => 'jsonb',
184 514
        'snowflake'    => 'bigint',
185
        'ulid'         => ['type' => 'character varying', 'size' => 26],
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
    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
    #[ColumnAttribute([
255
        'character varying',
256 40
        'bit',
257 4
        'bit varying',
258
        'datetime',
259
        'time',
260 40
        'timetz',
261 6
        'timestamp',
262 6
        'timestamptz',
263 6
    ])]
264
    protected int $size = 0;
265
266 6
    /**
267 6
     * Field is auto incremental.
268
     *
269
     * @deprecated since v2.5.0
270 40
     */
271
    protected bool $autoIncrement = false;
272
273
    /**
274
     * Indication that column has enum constrain.
275
     */
276
    protected bool $constrained = false;
277
278 516
    /**
279
     * Name of enum constraint associated with field.
280
     */
281
    protected string $constrainName = '';
282
283 516
    #[ColumnAttribute(['timestamp', 'time', 'timestamptz', 'timetz', 'tsrange', 'tstzrange'])]
284
    protected bool $withTimezone = false;
285 516
286 516
    #[ColumnAttribute(['interval'])]
287 516
    protected ?string $intervalType = null;
288
289
    #[ColumnAttribute(['numeric'])]
290 516
    protected int $precision = 0;
291 516
292 516
    #[ColumnAttribute(['numeric'])]
293
    protected int $scale = 0;
294 368
295 368
    /**
296
     * Column comment.
297 368
     */
298
    #[ColumnAttribute]
299 368
    protected string $comment = '';
300
301
    /**
302 470
     * Internal field to determine if the serial is PK.
303 264
     */
304
    protected bool $isPrimary = false;
305
306 470
    /**
307 14
     * @psalm-param non-empty-string $table Table name.
308 14
     *
309
     * @param DriverInterface $driver Postgres columns are bit more complex.
310
     */
311 470
    public static function createInstance(
312 2
        string $table,
313
        array $schema,
314
        DriverInterface $driver,
315
    ): self {
316
        $column = new self($table, $schema['column_name'], $driver->getTimezone());
317
318 2
        $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...
319
            $schema['typname'] === 'timestamp' || $schema['typname'] === 'timestamptz' => 'timestamp',
320
            $schema['typname'] === 'date' => 'date',
321 470
            $schema['typname'] === 'time' || $schema['typname'] === 'timetz' => 'time',
322
            \in_array($schema['typname'], ['_varchar', '_text', '_bpchar'], true) => 'string[]',
323 264
            \str_starts_with($schema['typname'], '_int') => 'integer[]',
324
            $schema['typname'] === '_numeric', \str_starts_with($schema['typname'], '_float') => 'float[]',
325
            default => $schema['data_type'],
326 470
        };
327
328 470
        $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...
329
        $column->comment = (string) $schema['description'];
330
        $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...
331 516
332
        if (
333 516
            \is_string($column->defaultValue)
334 506
            && \in_array($column->type, self::INTEGER_TYPES)
335
            && \preg_match('/nextval(.*)/', $column->defaultValue)
336
        ) {
337
            $column->type = match (true) {
338 370
                $column->type === 'bigint' => 'bigserial',
339 370
                $column->type === 'smallint' => 'smallserial',
340
                default => 'serial',
341
            };
342
            $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

342
            /** @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...
343
344
            $column->defaultValue = new Fragment($column->defaultValue);
345
346
            if ($schema['is_primary']) {
347
                $column->isPrimary = true;
348
            }
349
350
            return $column;
351 154
        }
352
353
        if ($schema['character_maximum_length'] !== null && \str_contains($column->type, 'char')) {
354 154
            $column->size = (int) $schema['character_maximum_length'];
355
        }
356
357
        if ($column->type === 'numeric') {
358
            $column->precision = (int) $schema['numeric_precision'];
359
            $column->scale = (int) $schema['numeric_scale'];
360 156
        }
361
362 156
        if ($column->type === 'USER-DEFINED' && $schema['typtype'] === 'e') {
363 156
            $column->type = $schema['typname'];
364
365
            /**
366 156
             * Attention, this is not default enum type emulated via CHECK.
367
             * This is real Postgres enum type.
368
             */
369
            self::resolveEnum($driver, $column);
370
        }
371
372 470
        if ($column->type === 'timestamp' || $column->type === 'time' || $column->type === 'interval') {
373
            $column->size = (int) $schema['datetime_precision'];
374 470
        }
375 402
376
        if (
377
            $schema['typname'] === 'timestamptz' ||
378 214
            $schema['typname'] === 'timetz' ||
379
            $schema['typname'] === 'tstzrange'
380 188
        ) {
381 160
            $column->withTimezone = true;
382
        }
383
384
        if (!empty($column->size) && \str_contains($column->type, 'char')) {
385 160
            //Potential enum with manually created constraint (check in)
386 138
            self::resolveConstrains($driver, $schema, $column);
387
        }
388
389 214
        if ($column->type === 'interval' && \is_string($schema['interval_type'])) {
390 214
            $column->intervalType = \str_replace(\sprintf('(%s)', $column->size), '', $schema['interval_type']);
391 152
            if (!\in_array($column->intervalType, self::INTERVALS_WITH_ALLOWED_PRECISION, true)) {
392
                $column->size = 0;
393 152
            }
394
        }
395
396 214
        if (
397
            ($column->type === 'bit' || $column->type === 'bit varying') &&
398
            isset($schema['character_maximum_length'])
399
        ) {
400
            $column->size = (int) $schema['character_maximum_length'];
401 264
        }
402
403
        $column->normalizeDefault();
404
405
        return $column;
406 264
    }
407
408
    public function getConstraints(): array
409 264
    {
410
        $constraints = parent::getConstraints();
411
412 264
        if ($this->constrained) {
413 264
            $constraints[] = $this->constrainName;
414
        }
415
416
        return $constraints;
417 264
    }
418 156
419 156
    /**
420 156
     * @psalm-return non-empty-string
421 156
     */
422
    public function getAbstractType(): string
423 156
    {
424
        return !empty($this->enumValues) ? 'enum' : parent::getAbstractType();
425
    }
426 156
427
    public function smallPrimary(): AbstractColumn
428 156
    {
429
        if (!empty($this->type) && $this->type !== 'smallserial') {
430 156
            //Change type of already existed column (we can't use "serial" alias here)
431 156
            $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...
432 156
433
            return $this;
434
        }
435 264
436
        return $this->type('smallPrimary');
437
    }
438
439
    public function primary(): AbstractColumn
440 2
    {
441
        if (!empty($this->type) && $this->type !== 'serial') {
442 2
            //Change type of already existed column (we can't use "serial" alias here)
443
            $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...
444 2
445
            return $this;
446 2
        }
447
448
        return $this->type('primary');
449
    }
450
451
    public function bigPrimary(): AbstractColumn
452
    {
453
        if (!empty($this->type) && $this->type !== 'bigserial') {
454 2
            //Change type of already existed column (we can't use "serial" alias here)
455
            $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...
456
457
            return $this;
458
        }
459
460
        return $this->type('bigPrimary');
461
    }
462
463
    public function enum(string|array $values): AbstractColumn
464
    {
465
        $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...
466
467
        $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...
468
        foreach ($this->enumValues as $value) {
469
            $this->size = \max((int) $this->size, \strlen($value));
470
        }
471
472
        return $this;
473
    }
474
475
    public function interval(int $size = 6, ?string $intervalType = null): AbstractColumn
476
    {
477
        if ($intervalType !== null && !\in_array($intervalType, self::INTERVALS_WITH_ALLOWED_PRECISION, true)) {
478
            $size = 0;
479
        }
480
481
        $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...
482
        $this->size = $size;
483
        $this->intervalType = $intervalType;
484
485
        return $this;
486
    }
487
488
    /**
489
     * @psalm-return non-empty-string
490
     */
491
    public function sqlStatement(DriverInterface $driver): string
492
    {
493
        $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

493
        $statement = [$driver->/** @scrutinizer ignore-call */ identifier($this->name), $this->type];
Loading history...
494
495
        if ($this->intervalType !== null && $this->getAbstractType() === 'interval') {
496
            if (!\in_array($this->intervalType, self::INTERVAL_TYPES, true)) {
497
                throw new SchemaException(\sprintf(
498
                    'Invalid interval type value. Valid values for interval type: `%s`.',
499
                    \implode('`, `', self::INTERVAL_TYPES),
500
                ));
501
            }
502
            $statement[] = $this->intervalType;
503
        }
504
505
        if ($this->getAbstractType() === 'enum') {
506
            //Enum specific column options
507
            if (!empty($enumDefinition = $this->quoteEnum($driver))) {
508
                $statement[] = $enumDefinition;
509
            }
510
        } elseif (!empty($this->precision)) {
511
            $statement[] = "({$this->precision}, {$this->scale})";
512
        } elseif (!empty($this->size) || $this->type === 'timestamp' || $this->type === 'time') {
513
            $statement[] = "({$this->size})";
514
        }
515
516
        if ($this->type === 'timestamp' || $this->type === 'time') {
517
            $statement[] = $this->withTimezone ? self::WITH_TIMEZONE : self::WITHOUT_TIMEZONE;
518
        }
519
520
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
521
522
        if ($this->defaultValue !== null) {
523
            $statement[] = "DEFAULT {$this->quoteDefault($driver)}";
524
        }
525
526
        $statement = \implode(' ', $statement);
527
528
        //We have to add constraint for enum type
529
        if ($this->getAbstractType() === 'enum') {
530
            $enumValues = [];
531
            foreach ($this->enumValues as $value) {
532
                $enumValues[] = $driver->quote($value);
533
            }
534
535
            $constrain = $driver->identifier($this->enumConstraint());
536
            $column = $driver->identifier($this->getName());
537
            $values = \implode(', ', $enumValues);
538
539
            return "{$statement} CONSTRAINT {$constrain} CHECK ($column IN ({$values}))";
540
        }
541
542
        //Nothing special
543
        return $statement;
544
    }
545
546
    /**
547
     * @psalm-return non-empty-string|null
548
     */
549
    public function commentOperation(DriverInterface $driver, PostgresColumn $initial): ?string
550
    {
551
        //Comment
552
        if ($initial->comment !== $this->comment) {
553
            return $this->createComment($driver);
554
        }
555
556
        return null;
557
    }
558
559
    /**
560
     * Generate set of operations need to change column.
561
     */
562
    public function alterOperations(DriverInterface $driver, AbstractColumn $initial): array
563
    {
564
        $operations = [];
565
566
        //To simplify comparation
567
        $currentType = [$this->type, $this->size, $this->precision, $this->scale];
568
        $initialType = [$initial->type, $initial->size, $initial->precision, $initial->scale];
569
570
        $identifier = $driver->identifier($this->getName());
571
572
        /*
573
         * This block defines column type and all variations.
574
         */
575
        if ($currentType !== $initialType) {
576
            if ($this->getAbstractType() === 'enum') {
577
                //Getting longest value
578
                $enumSize = $this->size;
579
                foreach ($this->enumValues as $value) {
580
                    $enumSize = \max($enumSize, \strlen($value));
581
                }
582
583
                $operations[] = "ALTER COLUMN {$identifier} TYPE character varying($enumSize)";
584
            } else {
585
                $type = "ALTER COLUMN {$identifier} TYPE {$this->type}";
586
587
                if (!empty($this->size)) {
588
                    $type .= "($this->size)";
589
                } elseif (!empty($this->precision)) {
590
                    $type .= "($this->precision, $this->scale)";
591
                }
592
593
                //Required to perform cross conversion
594
                $operations[] = "{$type} USING {$identifier}::{$this->type}";
595
            }
596
        }
597
598
        //Dropping enum constrain before any operation
599
        if ($this->constrained && $initial->getAbstractType() === 'enum') {
600
            $operations[] = 'DROP CONSTRAINT ' . $driver->identifier($this->enumConstraint());
601
        }
602
603
        //Default value set and dropping
604
        if ($initial->defaultValue !== $this->defaultValue) {
605
            if ($this->defaultValue === null) {
606
                $operations[] = "ALTER COLUMN {$identifier} DROP DEFAULT";
607
            } else {
608
                $operations[] = "ALTER COLUMN {$identifier} SET DEFAULT {$this->quoteDefault($driver)}";
609
            }
610
        }
611
612
        //Nullable option
613
        if ($initial->nullable !== $this->nullable) {
614
            $operations[] = "ALTER COLUMN {$identifier} " . (!$this->nullable ? 'SET' : 'DROP') . ' NOT NULL';
615
        }
616
617
        if ($this->getAbstractType() === 'enum') {
618
            $enumValues = [];
619
            foreach ($this->enumValues as $value) {
620
                $enumValues[] = $driver->quote($value);
621
            }
622
623
            $operations[] = "ADD CONSTRAINT {$driver->identifier($this->enumConstraint())} "
624
                . "CHECK ({$identifier} IN (" . \implode(', ', $enumValues) . '))';
625
        }
626
627
        return $operations;
628
    }
629
630
    public function compare(AbstractColumn $initial): bool
631
    {
632
        if (parent::compare($initial)) {
633
            return true;
634
        }
635
636
        return (bool) (
637
            \in_array($this->getAbstractType(), self::SERIAL_TYPES, true)
638
            && $initial->getDefaultValue() != $this->getDefaultValue()
639
        );
640
    }
641
642
    public function getComment(): string
643
    {
644
        return $this->comment;
645
    }
646
647
    /**
648
     * @psalm-return non-empty-string
649
     */
650
    public function createComment(DriverInterface $driver): string
651
    {
652
        $tableName = $driver->identifier($this->getTable());
653
        $identifier = $driver->identifier($this->getName());
654
655
        return "COMMENT ON COLUMN {$tableName}.{$identifier} IS " . $driver->quote($this->comment);
656
    }
657
658
    protected static function isJson(AbstractColumn $column): bool
659
    {
660
        return $column->getAbstractType() === 'json' || $column->getAbstractType() === 'jsonb';
661
    }
662
663
    /**
664
     * @psalm-return non-empty-string
665
     */
666
    protected function quoteEnum(DriverInterface $driver): string
667
    {
668
        //Postgres enums are just constrained strings
669
        return '(' . $this->size . ')';
670
    }
671
672
    /**
673
     * Resolving enum constrain and converting it into proper enum values set.
674
     */
675
    private static function resolveConstrains(
676
        DriverInterface $driver,
677
        array $schema,
678
        self $column,
679
    ): void {
680
        $query = "SELECT conname, pg_get_constraintdef(oid) as consrc FROM pg_constraint
681
        WHERE conrelid = ? AND contype = 'c' AND conkey = ?";
682
683
        $constraints = $driver->query(
684
            $query,
685
            [
686
                $schema['tableOID'],
687
                '{' . $schema['dtd_identifier'] . '}',
688
            ],
689
        );
690
691
        foreach ($constraints as $constraint) {
692
            $values = static::parseEnumValues($constraint['consrc']);
693
694
            if ($values !== []) {
695
                $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...
696
                $column->constrainName = $constraint['conname'];
697
                $column->constrained = true;
698
            }
699
        }
700
    }
701
702
    /**
703
     * Resolve native ENUM type if presented.
704
     */
705
    private static function resolveEnum(DriverInterface $driver, self $column): void
706
    {
707
        $range = $driver->query('SELECT enum_range(NULL::' . $column->type . ')')->fetchColumn(0);
708
709
        $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...
710
711
        if (!empty($column->defaultValue)) {
712
            //In database: 'value'::enumType
713
            $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...
714
                $column->defaultValue,
715
                1,
716
                \strpos($column->defaultValue, $column->type) - 4,
717
            );
718
        }
719
    }
720
721
    private static function parseEnumValues(string $constraint): array
722
    {
723
        if (\preg_match('/ARRAY\[([^\]]+)\]/', $constraint, $matches)) {
724
            $enumValues = \explode(',', $matches[1]);
725
            foreach ($enumValues as &$value) {
726
                if (\preg_match("/^'?([a-zA-Z0-9_]++)'?::([a-zA-Z0-9_]++)/", \trim($value, ' ()'), $matches)) {
727
                    //In database: 'value'::TYPE
728
                    $value = $matches[1];
729
                }
730
731
                unset($value);
732
            }
733
            unset($value);
734
735
            return $enumValues;
736
        }
737
738
        $pattern = '/CHECK \\(\\(\\([a-zA-Z0-9_]++\\)::([a-z]++) = \'([a-zA-Z0-9_]++)\'::([a-z]++)\\)\\)/i';
739
        if (\preg_match($pattern, $constraint, $matches) && !empty($matches[2])) {
740
            return [$matches[2]];
741
        }
742
743
        return [];
744
    }
745
746
    /**
747
     * Get/generate name for enum constraint.
748
     */
749
    private function enumConstraint(): string
750
    {
751
        if (empty($this->constrainName)) {
752
            $this->constrainName = \str_replace('.', '_', $this->table) . '_' . $this->getName() . '_enum_' . \uniqid();
753
        }
754
755
        return $this->constrainName;
756
    }
757
758
    /**
759
     * Normalize default value.
760
     */
761
    private function normalizeDefault(): void
762
    {
763
        if (!$this->hasDefaultValue()) {
764
            return;
765
        }
766
767
        if (\preg_match('/^\'?(.*?)\'?::(.+)/', $this->defaultValue, $matches)) {
768
            //In database: 'value'::TYPE
769
            $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...
770
        } elseif ($this->type === 'bit') {
771
            $this->defaultValue = \bindec(
772
                \substr($this->defaultValue, 2, \strpos($this->defaultValue, '::') - 3),
773
            );
774
        } elseif ($this->type === 'boolean') {
775
            $this->defaultValue = (\strtolower($this->defaultValue) === 'true');
776
        }
777
778
        $type = $this->getType();
779
        if ($type === self::FLOAT || $type === self::INT) {
780
            if (\preg_match('/^\(?(.*?)\)?(?!::(.+))?$/', $this->defaultValue, $matches)) {
781
                //Negative numeric values
782
                $this->defaultValue = $matches[1];
783
            }
784
        }
785
    }
786
}
787