Passed
Pull Request — 2.x (#86)
by Maxim
20:35 queued 03:53
created

PostgresColumn::bigPrimary()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 4.125

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 0
dl 0
loc 10
ccs 2
cts 4
cp 0.5
crap 4.125
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver\Postgres\Schema;
13
14
use Cycle\Database\Driver\DriverInterface;
15
use Cycle\Database\Exception\SchemaException;
16
use Cycle\Database\Injection\Fragment;
17
use Cycle\Database\Schema\AbstractColumn;
0 ignored issues
show
Bug introduced by
The type Cycle\Database\Schema\AbstractColumn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
19
class PostgresColumn extends AbstractColumn
20
{
21
    /**
22
     * Default timestamp expression (driver specific).
23
     */
24
    public const DATETIME_NOW = 'now()';
25
26
    /**
27
     * Private state related values.
28
     */
29
    public const EXCLUDE_FROM_COMPARE = [
30
        'userType',
31
        'timezone',
32
        'constrained',
33
        'constrainName',
34
    ];
35
36
    protected array $mapping = [
37
        //Primary sequences
38
        'primary'     => ['type' => 'serial', 'autoIncrement' => true, 'nullable' => false],
39
        'bigPrimary'  => ['type' => 'bigserial', 'autoIncrement' => true, 'nullable' => false],
40
41
        //Enum type (mapped via method)
42
        'enum'        => 'enum',
43
44
        //Logical types
45
        'boolean'     => 'boolean',
46
47
        //Integer types (size can always be changed with size method), longInteger has method alias
48
        //bigInteger
49
        'integer'     => 'integer',
50
        'tinyInteger' => 'smallint',
51
        'smallInteger'=> 'smallint',
52
        'bigInteger'  => 'bigint',
53
54
        //String with specified length (mapped via method)
55
        'string'      => 'character varying',
56
57
        //Generic types
58
        'text'        => 'text',
59
        'tinyText'    => 'text',
60
        'longText'    => 'text',
61
62
        //Real types
63
        'double'      => 'double precision',
64
        'float'       => 'real',
65
66
        //Decimal type (mapped via method)
67
        'decimal'     => 'numeric',
68
69
        //Date and Time types
70
        'datetime'    => 'timestamp without time zone',
71
        'date'        => 'date',
72
        'time'        => 'time without time zone',
73
        'timestamp'   => 'timestamp without time zone',
74
        'timestamptz' => 'timestamp with time zone',
75
76
        //Binary types
77
        'binary'      => 'bytea',
78
        'tinyBinary'  => 'bytea',
79
        'longBinary'  => 'bytea',
80
81
        //Additional types
82
        'json'        => 'text',
83
        'jsonb'       => 'jsonb',
84
        'uuid'        => 'uuid',
85
    ];
86
87
    protected array $reverseMapping = [
88
        'primary'     => ['serial'],
89
        'bigPrimary'  => ['bigserial'],
90
        'enum'        => ['enum'],
91
        'boolean'     => ['boolean'],
92
        'integer'     => ['int', 'integer', 'int4'],
93
        'tinyInteger' => ['smallint'],
94
        'smallInteger'=> ['smallint'],
95
        'bigInteger'  => ['bigint', 'int8'],
96
        'string'      => ['character varying', 'character'],
97
        'text'        => ['text'],
98
        'double'      => ['double precision'],
99
        'float'       => ['real', 'money'],
100
        'decimal'     => ['numeric'],
101
        'date'        => ['date'],
102
        'time'        => ['time', 'time with time zone', 'time without time zone'],
103
        'timestamp'   => ['timestamp', 'timestamp without time zone'],
104
        'timestamptz' => ['timestamp with time zone'],
105
        'binary'      => ['bytea'],
106
        'json'        => ['json'],
107
        'jsonb'       => ['jsonb'],
108
    ];
109
110
    /**
111
     * Field is auto incremental.
112
     */
113
    protected bool $autoIncrement = false;
114
115
    /**
116
     * Indication that column has enum constrain.
117
     */
118
    protected bool $constrained = false;
119
120
    /**
121
     * Name of enum constraint associated with field.
122 8
     */
123
    protected string $constrainName = '';
124 8
125
    public function getConstraints(): array
126 8
    {
127
        $constraints = parent::getConstraints();
128
129
        if ($this->constrained) {
130 8
            $constraints[] = $this->constrainName;
131
        }
132
133
        return $constraints;
134
    }
135
136 516
    /**
137
     * @psalm-return non-empty-string
138 516
     */
139
    public function getAbstractType(): string
140
    {
141 366
        return !empty($this->enumValues) ? 'enum' : parent::getAbstractType();
142
    }
143 366
144
    public function primary(): AbstractColumn
145
    {
146
        if (!empty($this->type) && $this->type !== 'serial') {
147
            //Change type of already existed column (we can't use "serial" alias here)
148
            $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...
149
150 366
            return $this;
151
        }
152
153 2
        return $this->type('primary');
154
    }
155 2
156
    public function bigPrimary(): AbstractColumn
157
    {
158
        if (!empty($this->type) && $this->type !== 'bigserial') {
159
            //Change type of already existed column (we can't use "serial" alias here)
160
            $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...
161
162 2
            return $this;
163
        }
164
165 156
        return $this->type('bigPrimary');
166
    }
167 156
168
    public function enum(string|array $values): AbstractColumn
169 156
    {
170 156
        $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...
171 156
172
        $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...
173
        foreach ($this->enumValues as $value) {
174 156
            $this->size = max((int)$this->size, \strlen($value));
0 ignored issues
show
Bug Best Practice introduced by
The property size does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
175
        }
176
177
        return $this;
178
    }
179
180 514
    /**
181
     * @psalm-return non-empty-string
182 514
     */
183
    public function sqlStatement(DriverInterface $driver): string
184 514
    {
185
        $statement = parent::sqlStatement($driver);
186 506
187
        if ($this->getAbstractType() !== 'enum') {
188
            //Nothing special
189
            return $statement;
190 154
        }
191 154
192 154
        //We have add constraint for enum type
193
        $enumValues = [];
194
        foreach ($this->enumValues as $value) {
195 154
            $enumValues[] = $driver->quote($value);
196 154
        }
197 154
198
        $constrain = $driver->identifier($this->enumConstraint());
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

198
        /** @scrutinizer ignore-call */ 
199
        $constrain = $driver->identifier($this->enumConstraint());
Loading history...
199 154
        $column = $driver->identifier($this->getName());
200
        $values = implode(', ', $enumValues);
201
202
        return "{$statement} CONSTRAINT {$constrain} CHECK ($column IN ({$values}))";
203
    }
204
205 40
    /**
206
     * Generate set of operations need to change column.
207 40
     */
208
    public function alterOperations(DriverInterface $driver, AbstractColumn $initial): array
209
    {
210 40
        $operations = [];
211 40
212
        //To simplify comparation
213 40
        $currentType = [$this->type, $this->size, $this->precision, $this->scale];
214
        $initialType = [$initial->type, $initial->size, $initial->precision, $initial->scale];
215
216
        $identifier = $driver->identifier($this->getName());
217
218 40
        /*
219 18
         * This block defines column type and all variations.
220
         */
221 2
        if ($currentType !== $initialType) {
222 2
            if ($this->getAbstractType() === 'enum') {
223 2
                //Getting longest value
224
                $enumSize = $this->size;
225
                foreach ($this->enumValues as $value) {
226 2
                    $enumSize = max($enumSize, strlen($value));
227
                }
228 16
229
                $operations[] = "ALTER COLUMN {$identifier} TYPE character varying($enumSize)";
230 16
            } else {
231 6
                $type = "ALTER COLUMN {$identifier} TYPE {$this->type}";
232 12
233
                if (!empty($this->size)) {
234
                    $type .= "($this->size)";
235
                } elseif (!empty($this->precision)) {
236
                    $type .= "($this->precision, $this->scale)";
237 16
                }
238
239
                //Required to perform cross conversion
240
                $operations[] = "{$type} USING {$identifier}::{$this->type}";
241
            }
242 40
        }
243 6
244
        //Dropping enum constrain before any operation
245
        if ($this->constrained && $initial->getAbstractType() === 'enum') {
246
            $operations[] = 'DROP CONSTRAINT ' . $driver->identifier($this->enumConstraint());
247 40
        }
248 10
249 2
        //Default value set and dropping
250
        if ($initial->defaultValue !== $this->defaultValue) {
251 8
            if ($this->defaultValue === null) {
252
                $operations[] = "ALTER COLUMN {$identifier} DROP DEFAULT";
253
            } else {
254
                $operations[] = "ALTER COLUMN {$identifier} SET DEFAULT {$this->quoteDefault($driver)}";
255
            }
256 40
        }
257 4
258
        //Nullable option
259
        if ($initial->nullable !== $this->nullable) {
260 40
            $operations[] = "ALTER COLUMN {$identifier} " . (!$this->nullable ? 'SET' : 'DROP') . ' NOT NULL';
261 6
        }
262 6
263 6
        if ($this->getAbstractType() === 'enum') {
264
            $enumValues = [];
265
            foreach ($this->enumValues as $value) {
266 6
                $enumValues[] = $driver->quote($value);
267 6
            }
268
269
            $operations[] = "ADD CONSTRAINT {$driver->identifier($this->enumConstraint())} "
270 40
                . "CHECK ({$identifier} IN (" . implode(', ', $enumValues) . '))';
271
        }
272
273
        return $operations;
274
    }
275
276
    /**
277
     * @psalm-param non-empty-string $table Table name.
278 516
     *
279
     * @param DriverInterface $driver Postgres columns are bit more complex.
280
     */
281
    public static function createInstance(
282
        string $table,
283 516
        array $schema,
284
        DriverInterface $driver
285 516
    ): self {
286 516
        $column = new self($table, $schema['column_name'], $driver->getTimezone());
287 516
288
        $column->type = $schema['data_type'];
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...
289
        $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...
290 516
        $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...
291 516
292 516
        if (
293
            \is_string($column->defaultValue)
294 368
            && \in_array($column->type, ['int', 'bigint', 'integer'])
295 368
            && preg_match('/nextval(.*)/', $column->defaultValue)
296
        ) {
297 368
            $column->type = ($column->type === 'bigint' ? 'bigserial' : 'serial');
298
            $column->autoIncrement = true;
299 368
300
            $column->defaultValue = new Fragment($column->defaultValue);
301
302 470
            return $column;
303 264
        }
304
305
        if ($schema['character_maximum_length'] !== null && str_contains($column->type, 'char')) {
306 470
            $column->size = (int) $schema['character_maximum_length'];
0 ignored issues
show
Bug Best Practice introduced by
The property size does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
307 14
        }
308 14
309
        if ($column->type === 'numeric') {
310
            $column->precision = (int) $schema['numeric_precision'];
0 ignored issues
show
Bug Best Practice introduced by
The property precision does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
311 470
            $column->scale = (int) $schema['numeric_scale'];
0 ignored issues
show
Bug Best Practice introduced by
The property scale does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
312 2
        }
313
314
        if ($column->type === 'USER-DEFINED' && $schema['typtype'] === 'e') {
315
            $column->type = $schema['typname'];
316
317
            /**
318 2
             * Attention, this is not default enum type emulated via CHECK.
319
             * This is real Postgres enum type.
320
             */
321 470
            self::resolveEnum($driver, $column);
322
        }
323 264
324
        if (!empty($column->size) && str_contains($column->type, 'char')) {
325
            //Potential enum with manually created constraint (check in)
326 470
            self::resolveConstrains($driver, $schema, $column);
327
        }
328 470
329
        $column->normalizeDefault();
330
331 516
        return $column;
332
    }
333 516
334 506
    public function compare(AbstractColumn $initial): bool
335
    {
336
        if (parent::compare($initial)) {
337
            return true;
338 370
        }
339 370
340
        return (bool) (
341
            in_array($this->getAbstractType(), ['primary', 'bigPrimary'])
342
            && $initial->getDefaultValue() != $this->getDefaultValue()
343
        )
344
            //PG adds default values to primary keys
345
346
347
348
         ;
349
    }
350
351 154
    public function datetime(int $size = 0): self
352
    {
353
        $this->type('datetime');
354 154
355
        if ($size !== 0) {
356
            ($size < 0 || $size > 6) && throw new SchemaException('Invalid datetime length value');
357
358
            $this->type = str_replace('timestamp', "timestamp ({$size})", $this->type);
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...
359
        }
360 156
361
        return $this;
362 156
    }
363 156
364
    /**
365
     * @psalm-return non-empty-string
366 156
     */
367
    protected function quoteEnum(DriverInterface $driver): string
368
    {
369
        //Postgres enums are just constrained strings
370
        return '(' . $this->size . ')';
371
    }
372 470
373
    /**
374 470
     * Get/generate name for enum constraint.
375 402
     */
376
    private function enumConstraint(): string
377
    {
378 214
        if (empty($this->constrainName)) {
379
            $this->constrainName = str_replace('.', '_', $this->table) . '_' . $this->getName() . '_enum_' . uniqid();
380 188
        }
381 160
382
        return $this->constrainName;
383
    }
384
385 160
    /**
386 138
     * Normalize default value.
387
     */
388
    private function normalizeDefault(): void
389 214
    {
390 214
        if (!$this->hasDefaultValue()) {
391 152
            return;
392
        }
393 152
394
        if (preg_match('/^\'?(.*?)\'?::(.+)/', $this->defaultValue, $matches)) {
395
            //In database: 'value'::TYPE
396 214
            $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...
397
        } elseif ($this->type === 'bit') {
398
            $this->defaultValue = bindec(
399
                substr($this->defaultValue, 2, strpos($this->defaultValue, '::') - 3)
400
            );
401 264
        } elseif ($this->type === 'boolean') {
402
            $this->defaultValue = (strtolower($this->defaultValue) === 'true');
403
        }
404
405
        $type = $this->getType();
406 264
        if ($type === self::FLOAT || $type === self::INT) {
407
            if (preg_match('/^\(?(.*?)\)?(?!::(.+))?$/', $this->defaultValue, $matches)) {
408
                //Negative numeric values
409 264
                $this->defaultValue = $matches[1];
410
            }
411
        }
412 264
    }
413 264
414
    /**
415
     * Resolving enum constrain and converting it into proper enum values set.
416
     */
417 264
    private static function resolveConstrains(
418 156
        DriverInterface $driver,
419 156
        array $schema,
420 156
        self $column
421 156
    ): void {
422
        $query = "SELECT conname, pg_get_constraintdef(oid) as consrc FROM pg_constraint
423 156
        WHERE conrelid = ? AND contype = 'c' AND conkey = ?";
424
425
        $constraints = $driver->query(
426 156
            $query,
427
            [
428 156
                $schema['tableOID'],
429
                '{' . $schema['dtd_identifier'] . '}',
430 156
            ]
431 156
        );
432 156
433
        foreach ($constraints as $constraint) {
434
            if (preg_match('/ARRAY\[([^\]]+)\]/', $constraint['consrc'], $matches)) {
435 264
                $enumValues = explode(',', $matches[1]);
436
                foreach ($enumValues as &$value) {
437
                    if (preg_match("/^'?(.*?)'?::(.+)/", trim($value, ' ()'), $matches)) {
438
                        //In database: 'value'::TYPE
439
                        $value = $matches[1];
440 2
                    }
441
442 2
                    unset($value);
443
                }
444 2
                unset($value);
445
446 2
                $column->enumValues = $enumValues;
0 ignored issues
show
Bug Best Practice introduced by
The property enumValues does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
447
                $column->constrainName = $constraint['conname'];
448
                $column->constrained = true;
449
            }
450
        }
451
    }
452
453
    /**
454 2
     * Resolve native ENUM type if presented.
455
     */
456
    private static function resolveEnum(DriverInterface $driver, self $column): void
457
    {
458
        $range = $driver->query('SELECT enum_range(NULL::' . $column->type . ')')->fetchColumn(0);
459
460
        $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...
461
462
        if (!empty($column->defaultValue)) {
463
            //In database: 'value'::enumType
464
            $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...
465
                $column->defaultValue,
466
                1,
467
                strpos($column->defaultValue, $column->type) - 4
468
            );
469
        }
470
    }
471
}
472