Passed
Push — 2.x ( 416953...6a87a6 )
by Aleksei
29:18 queued 09:24
created

PostgresColumn::sqlStatement()   F

Complexity

Conditions 17
Paths 361

Size

Total Lines 53
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 306

Importance

Changes 0
Metric Value
cc 17
eloc 29
nc 361
nop 1
dl 0
loc 53
ccs 0
cts 0
cp 0
crap 306
rs 2.3708
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        'uuid'         => 'uuid',
185
        'point'        => 'point',
186 506
        'line'         => 'line',
187
        'lseg'         => 'lseg',
188
        'box'          => 'box',
189
        'path'         => 'path',
190 154
        'polygon'      => 'polygon',
191 154
        'circle'       => 'circle',
192 154
        'cidr'         => 'cidr',
193
        'inet'         => 'inet',
194
        'macaddr'      => 'macaddr',
195 154
        'macaddr8'     => 'macaddr8',
196 154
        'tsvector'     => 'tsvector',
197 154
        'tsquery'      => 'tsquery',
198
    ];
199 154
    protected array $reverseMapping = [
200
        'smallPrimary' => [['type' => 'smallserial', 'isPrimary' => true]],
201
        'primary'      => [['type' => 'serial', 'isPrimary' => true]],
202
        'bigPrimary'   => [['type' => 'bigserial', 'isPrimary' => true]],
203
        'smallserial'  => [['type' => 'smallserial', 'isPrimary' => false]],
204
        'serial'       => [['type' => 'serial', 'isPrimary' => false]],
205 40
        'bigserial'    => [['type' => 'bigserial', 'isPrimary' => false]],
206
        'enum'         => ['enum'],
207 40
        'boolean'      => ['boolean'],
208
        'integer'      => ['int', 'integer', 'int4', 'int4range'],
209
        'tinyInteger'  => ['smallint'],
210 40
        'smallInteger' => ['smallint'],
211 40
        'bigInteger'   => ['bigint', 'int8', 'int8range'],
212
        'string'       => [
213 40
            'character varying',
214
            'character',
215
            'char',
216
            'point',
217
            'line',
218 40
            'lseg',
219 18
            'box',
220
            'path',
221 2
            'polygon',
222 2
            'circle',
223 2
            'cidr',
224
            'inet',
225
            'macaddr',
226 2
            'macaddr8',
227
            'tsvector',
228 16
            'tsquery',
229
        ],
230 16
        'text'         => ['text'],
231 6
        'double'       => ['double precision'],
232 12
        'float'        => ['real', 'money'],
233
        'decimal'      => ['numeric', 'numrange'],
234
        'date'         => ['date', 'daterange'],
235
        'time'         => [['type' => 'time', 'withTimezone' => false]],
236
        'timetz'       => [['type' => 'time', 'withTimezone' => true]],
237 16
        'timestamp'    => [
238
            ['type' => 'timestamp', 'withTimezone' => false],
239
            ['type' => 'tsrange', 'withTimezone' => false],
240
        ],
241
        'timestamptz'  => [
242 40
            ['type' => 'timestamp', 'withTimezone' => true],
243 6
            ['type' => 'tstzrange', 'withTimezone' => true],
244
        ],
245
        'binary'       => ['bytea'],
246
        'json'         => ['json'],
247 40
        'jsonb'        => ['jsonb'],
248 10
        'interval'     => ['interval'],
249 2
        'bit'          => ['bit', 'bit varying'],
250
    ];
251 8
252
    #[ColumnAttribute([
253
        'character varying',
254
        'bit',
255
        'bit varying',
256 40
        'datetime',
257 4
        'time',
258
        'timetz',
259
        'timestamp',
260 40
        'timestamptz',
261 6
    ])]
262 6
    protected int $size = 0;
263 6
264
    /**
265
     * Field is auto incremental.
266 6
     *
267 6
     * @deprecated since v2.5.0
268
     */
269
    protected bool $autoIncrement = false;
270 40
271
    /**
272
     * Indication that column has enum constrain.
273
     */
274
    protected bool $constrained = false;
275
276
    /**
277
     * Name of enum constraint associated with field.
278 516
     */
279
    protected string $constrainName = '';
280
281
    #[ColumnAttribute(['timestamp', 'time', 'timestamptz', 'timetz', 'tsrange', 'tstzrange'])]
282
    protected bool $withTimezone = false;
283 516
284
    #[ColumnAttribute(['interval'])]
285 516
    protected ?string $intervalType = null;
286 516
287 516
    #[ColumnAttribute(['numeric'])]
288
    protected int $precision = 0;
289
290 516
    #[ColumnAttribute(['numeric'])]
291 516
    protected int $scale = 0;
292 516
293
    /**
294 368
     * Column comment.
295 368
     */
296
    #[ColumnAttribute]
297 368
    protected string $comment = '';
298
299 368
    /**
300
     * Internal field to determine if the serial is PK.
301
     */
302 470
    protected bool $isPrimary = false;
303 264
304
    /**
305
     * @psalm-param non-empty-string $table Table name.
306 470
     *
307 14
     * @param DriverInterface $driver Postgres columns are bit more complex.
308 14
     */
309
    public static function createInstance(
310
        string $table,
311 470
        array $schema,
312 2
        DriverInterface $driver,
313
    ): self {
314
        $column = new self($table, $schema['column_name'], $driver->getTimezone());
315
316
        $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...
317
            $schema['typname'] === 'timestamp' || $schema['typname'] === 'timestamptz' => 'timestamp',
318 2
            $schema['typname'] === 'date' => 'date',
319
            $schema['typname'] === 'time' || $schema['typname'] === 'timetz' => 'time',
320
            \in_array($schema['typname'], ['_varchar', '_text', '_bpchar'], true) => 'string[]',
321 470
            \str_starts_with($schema['typname'], '_int') => 'integer[]',
322
            $schema['typname'] === '_numeric', \str_starts_with($schema['typname'], '_float') => 'float[]',
323 264
            default => $schema['data_type'],
324
        };
325
326 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...
327
        $column->comment = (string) $schema['description'];
328 470
        $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...
329
330
        if (
331 516
            \is_string($column->defaultValue)
332
            && \in_array($column->type, self::INTEGER_TYPES)
333 516
            && \preg_match('/nextval(.*)/', $column->defaultValue)
334 506
        ) {
335
            $column->type = match (true) {
336
                $column->type === 'bigint' => 'bigserial',
337
                $column->type === 'smallint' => 'smallserial',
338 370
                default => 'serial',
339 370
            };
340
            $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

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

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