Passed
Pull Request — 2.x (#219)
by
unknown
18:41
created

PostgresColumn::getComment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
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
    private const SERIAL_TYPES = [
53
        'smallPrimary',
54
        'primary',
55
        'bigPrimary',
56
        'smallserial',
57
        'serial',
58
        'bigserial',
59
    ];
60
    protected const INTEGER_TYPES = ['int', 'bigint', 'integer', 'smallint'];
61
62
    /**
63
     * Default timestamp expression (driver specific).
64
     */
65
    public const DATETIME_NOW = 'now()';
66
67
    public const DATETIME_PRECISION = 6;
68
69
    /**
70
     * Private state related values.
71
     */
72
    public const EXCLUDE_FROM_COMPARE = [
73
        'userType',
74
        'timezone',
75
        'constrained',
76
        'constrainName',
77
        'attributes',
78
    ];
79
80
    protected const INTERVAL_TYPES = [
81
        'YEAR',
82
        'MONTH',
83
        'DAY',
84
        'HOUR',
85
        'MINUTE',
86
        'SECOND',
87
        'YEAR TO MONTH',
88
        'DAY TO HOUR',
89
        'DAY TO MINUTE',
90
        'DAY TO SECOND',
91
        'HOUR TO MINUTE',
92
        'HOUR TO SECOND',
93
        'MINUTE TO SECOND',
94
    ];
95
    protected const INTERVALS_WITH_ALLOWED_PRECISION = [
96
        'SECOND',
97
        'DAY TO SECOND',
98
        'HOUR TO SECOND',
99
        'MINUTE TO SECOND',
100
    ];
101
102
    protected array $aliases = [
103
        'int'            => 'integer',
104
        'smallint'       => 'smallInteger',
105
        'bigint'         => 'bigInteger',
106
        'incremental'    => 'primary',
107
        'bigIncremental' => 'bigPrimary',
108
        'bool'           => 'boolean',
109
        'blob'           => 'binary',
110
        'bitVarying'     => 'bit varying',
111
        'smallSerial'    => 'smallserial',
112
        'bigSerial'      => 'bigserial',
113
    ];
114
    protected array $mapping = [
115
        //Primary sequences
116
        'smallPrimary' => ['type' => 'smallserial', 'nullable' => false, 'isPrimary' => true],
117
        'primary'      => ['type' => 'serial', 'nullable' => false, 'isPrimary' => true],
118
        'bigPrimary'   => ['type' => 'bigserial', 'nullable' => false, 'isPrimary' => true],
119
120
        //Serial
121
        'smallserial' => ['type' => 'smallserial', 'nullable' => false],
122 8
        'serial'      => ['type' => 'serial', 'nullable' => false],
123
        'bigserial'   => ['type' => 'bigserial', 'nullable' => false],
124 8
125
        //Enum type (mapped via method)
126 8
        'enum'         => 'enum',
127
128
        //Logical types
129
        'boolean'      => 'boolean',
130 8
131
        //Integer types (size can always be changed with size method), longInteger has method alias
132
        //bigInteger
133
        'integer'      => 'integer',
134
        'tinyInteger'  => 'smallint',
135
        'smallInteger' => 'smallint',
136 516
        'bigInteger'   => 'bigint',
137
138 516
        //String with specified length (mapped via method)
139
        'string'       => ['type' => 'character varying', 'size' => 255],
140
141 366
        //Generic types
142
        'text'         => 'text',
143 366
        'mediumText'   => 'text',
144
        'tinyText'     => 'text',
145
        'longText'     => 'text',
146
147
        //Real types
148
        'double'       => 'double precision',
149
        'float'        => 'real',
150 366
151
        //Decimal type (mapped via method)
152
        'decimal'      => 'numeric',
153 2
154
        //Date and Time types
155 2
        'datetime'     => 'timestamp',
156
        'date'         => 'date',
157
        'time'         => 'time',
158
        'timetz'       => ['type' => 'time', 'withTimezone' => true],
159
        'timestamp'    => 'timestamp',
160
        'timestamptz'  => ['type' => 'timestamp', 'withTimezone' => true],
161
        'interval'     => 'interval',
162 2
163
        //Binary types
164
        'binary'       => 'bytea',
165 156
        'tinyBinary'   => 'bytea',
166
        'longBinary'   => 'bytea',
167 156
168
        //Bit-string
169 156
        'bit'          => ['type' => 'bit', 'size' => 1],
170 156
        'bit varying'  => 'bit varying',
171 156
172
        //Ranges
173
        'int4range'    => 'int4range',
174 156
        'int8range'    => 'int8range',
175
        'numrange'     => 'numrange',
176
        'tsrange'      => 'tsrange',
177
        'tstzrange'    => ['type' => 'tstzrange', 'withTimezone' => true],
178
        'daterange'    => 'daterange',
179
180 514
        //Additional types
181
        'json'         => 'json',
182 514
        'jsonb'        => 'jsonb',
183
        'uuid'         => 'uuid',
184 514
        'point'        => 'point',
185
        'line'         => 'line',
186 506
        'lseg'         => 'lseg',
187
        'box'          => 'box',
188
        'path'         => 'path',
189
        'polygon'      => 'polygon',
190 154
        'circle'       => 'circle',
191 154
        'cidr'         => 'cidr',
192 154
        'inet'         => 'inet',
193
        'macaddr'      => 'macaddr',
194
        'macaddr8'     => 'macaddr8',
195 154
        'tsvector'     => 'tsvector',
196 154
        'tsquery'      => 'tsquery',
197 154
    ];
198
    protected array $reverseMapping = [
199 154
        'smallPrimary' => [['type' => 'smallserial', 'isPrimary' => true]],
200
        'primary'      => [['type' => 'serial', 'isPrimary' => true]],
201
        'bigPrimary'   => [['type' => 'bigserial', 'isPrimary' => true]],
202
        'smallserial'  => [['type' => 'smallserial', 'isPrimary' => false]],
203
        'serial'       => [['type' => 'serial', 'isPrimary' => false]],
204
        'bigserial'    => [['type' => 'bigserial', 'isPrimary' => false]],
205 40
        'enum'         => ['enum'],
206
        'boolean'      => ['boolean'],
207 40
        'integer'      => ['int', 'integer', 'int4', 'int4range'],
208
        'tinyInteger'  => ['smallint'],
209
        'smallInteger' => ['smallint'],
210 40
        'bigInteger'   => ['bigint', 'int8', 'int8range'],
211 40
        'string'       => [
212
            'character varying',
213 40
            'character',
214
            'char',
215
            'point',
216
            'line',
217
            'lseg',
218 40
            'box',
219 18
            'path',
220
            'polygon',
221 2
            'circle',
222 2
            'cidr',
223 2
            'inet',
224
            'macaddr',
225
            'macaddr8',
226 2
            'tsvector',
227
            'tsquery',
228 16
        ],
229
        'text'         => ['text'],
230 16
        'double'       => ['double precision'],
231 6
        'float'        => ['real', 'money'],
232 12
        'decimal'      => ['numeric', 'numrange'],
233
        'date'         => ['date', 'daterange'],
234
        'time'         => [['type' => 'time', 'withTimezone' => false]],
235
        'timetz'       => [['type' => 'time', 'withTimezone' => true]],
236
        'timestamp'    => [
237 16
            ['type' => 'timestamp', 'withTimezone' => false],
238
            ['type' => 'tsrange', 'withTimezone' => false],
239
        ],
240
        'timestamptz'  => [
241
            ['type' => 'timestamp', 'withTimezone' => true],
242 40
            ['type' => 'tstzrange', 'withTimezone' => true],
243 6
        ],
244
        'binary'       => ['bytea'],
245
        'json'         => ['json'],
246
        'jsonb'        => ['jsonb'],
247 40
        'interval'     => ['interval'],
248 10
        'bit'          => ['bit', 'bit varying'],
249 2
    ];
250
251 8
    #[ColumnAttribute([
252
        'character varying',
253
        'bit',
254
        'bit varying',
255
        'datetime',
256 40
        'time',
257 4
        'timetz',
258
        'timestamp',
259
        'timestamptz',
260 40
    ])]
261 6
    protected int $size = 0;
262 6
263 6
    /**
264
     * Field is auto incremental.
265
     *
266 6
     * @deprecated since v2.5.0
267 6
     */
268
    protected bool $autoIncrement = false;
269
270 40
    /**
271
     * Indication that column has enum constrain.
272
     */
273
    protected bool $constrained = false;
274
275
    /**
276
     * Name of enum constraint associated with field.
277
     */
278 516
    protected string $constrainName = '';
279
280
    #[ColumnAttribute(['timestamp', 'time', 'timestamptz', 'timetz', 'tsrange', 'tstzrange'])]
281
    protected bool $withTimezone = false;
282
283 516
    #[ColumnAttribute(['interval'])]
284
    protected ?string $intervalType = null;
285 516
286 516
    #[ColumnAttribute(['numeric'])]
287 516
    protected int $precision = 0;
288
289
    #[ColumnAttribute(['numeric'])]
290 516
    protected int $scale = 0;
291 516
292 516
    /**
293
     * Column comment.
294 368
     */
295 368
    #[ColumnAttribute]
296
    protected string $comment = '';
297 368
298
    /**
299 368
     * Internal field to determine if the serial is PK.
300
     */
301
    protected bool $isPrimary = false;
302 470
303 264
    /**
304
     * @psalm-param non-empty-string $table Table name.
305
     *
306 470
     * @param DriverInterface $driver Postgres columns are bit more complex.
307 14
     */
308 14
    public static function createInstance(
309
        string $table,
310
        array $schema,
311 470
        DriverInterface $driver,
312 2
    ): self {
313
        $column = new self($table, $schema['column_name'], $driver->getTimezone());
314
315
        $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...
316
            $schema['typname'] === 'timestamp' || $schema['typname'] === 'timestamptz' => 'timestamp',
317
            $schema['typname'] === 'date' => 'date',
318 2
            $schema['typname'] === 'time' || $schema['typname'] === 'timetz' => 'time',
319
            \in_array($schema['typname'], ['_varchar', '_text', '_bpchar'], true) => 'string[]',
320
            \str_starts_with($schema['typname'], '_int') => 'integer[]',
321 470
            $schema['typname'] === '_numeric', \str_starts_with($schema['typname'], '_float') => 'float[]',
322
            default => $schema['data_type'],
323 264
        };
324
325
        $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...
326 470
        $column->comment = (string) $schema['description'];
327
        $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...
328 470
329
        if (
330
            \is_string($column->defaultValue)
331 516
            && \in_array($column->type, self::INTEGER_TYPES)
332
            && \preg_match('/nextval(.*)/', $column->defaultValue)
333 516
        ) {
334 506
            $column->type = match (true) {
335
                $column->type === 'bigint' => 'bigserial',
336
                $column->type === 'smallint' => 'smallserial',
337
                default => 'serial',
338 370
            };
339 370
            $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

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

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