Completed
Branch feature/pre-split (42159e)
by Anton
05:36
created

PostgresColumn   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 459
Duplicated Lines 6.75 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 31
loc 459
rs 6.0975
c 0
b 0
f 0
wmc 60
lcom 1
cbo 5

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getConstraints() 0 10 2
A abstractType() 0 8 2
A primary() 0 11 3
A bigPrimary() 0 11 3
A enum() 11 11 3
A sqlStatement() 0 21 3
C alterOperations() 0 68 14
A quoteEnum() 0 5 1
A enumConstraint() 8 8 2
B normalizeDefault() 0 15 5
C createInstance() 4 48 11
B resolveConstrains() 8 28 5
A resolveEnum() 0 15 2
A compare() 0 16 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PostgresColumn often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PostgresColumn, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Spiral Framework, Core Components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\Database\Drivers\Postgres\Schemas;
8
9
use Spiral\Database\Entities\Driver;
10
use Spiral\Database\Injections\Fragment;
11
use Spiral\Database\Schemas\ColumnInterface;
12
use Spiral\Database\Schemas\Prototypes\AbstractColumn;
13
14
/**
15
 * @todo investigate potential issue with entity non handling enum correctly when multiple
16
 * @todo column changes happen in one session (who the hell will do that?)
17
 */
18
class PostgresColumn extends AbstractColumn
19
{
20
    /**
21
     * Default timestamp expression (driver specific).
22
     */
23
    const DATETIME_NOW = 'now()';
24
25
    /**
26
     * Private state related values.
27
     */
28
    const EXCLUDE_FROM_COMPARE = [
29
        'timezone',
30
        'constrained',
31
        'constrainName'
32
    ];
33
34
    /**
35
     * {@inheritdoc}
36
     */
37
    protected $mapping = [
38
        //Primary sequences
39
        'primary'     => ['type' => 'serial', 'autoIncrement' => true, 'nullable' => false],
40
        'bigPrimary'  => ['type' => 'bigserial', 'autoIncrement' => true, 'nullable' => false],
41
42
        //Enum type (mapped via method)
43
        'enum'        => 'enum',
44
45
        //Logical types
46
        'boolean'     => 'boolean',
47
48
        //Integer types (size can always be changed with size method), longInteger has method alias
49
        //bigInteger
50
        'integer'     => 'integer',
51
        'tinyInteger' => '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
75
        //Binary types
76
        'binary'      => 'bytea',
77
        'tinyBinary'  => 'bytea',
78
        'longBinary'  => 'bytea',
79
80
        //Additional types
81
        'json'        => 'text',
82
    ];
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    protected $reverseMapping = [
88
        'primary'     => ['serial'],
89
        'bigPrimary'  => ['bigserial'],
90
        'enum'        => ['enum'],
91
        'boolean'     => ['boolean'],
92
        'integer'     => ['int', 'integer', 'int4'],
93
        'tinyInteger' => ['smallint'],
94
        'bigInteger'  => ['bigint', 'int8'],
95
        'string'      => ['character varying', 'character'],
96
        'text'        => ['text'],
97
        'double'      => ['double precision'],
98
        'float'       => ['real', 'money'],
99
        'decimal'     => ['numeric'],
100
        'date'        => ['date'],
101
        'time'        => ['time', 'time with time zone', 'time without time zone'],
102
        'timestamp'   => ['timestamp', 'timestamp with time zone', 'timestamp without time zone'],
103
        'binary'      => ['bytea'],
104
        'json'        => ['text'],
105
    ];
106
107
    /**
108
     * Field is auto incremental.
109
     *
110
     * @var bool
111
     */
112
    protected $autoIncrement = false;
113
114
    /**
115
     * Indication that column have enum constrain.
116
     *
117
     * @var bool
118
     */
119
    protected $constrained = false;
120
121
    /**
122
     * Name of enum constraint associated with field.
123
     *
124
     * @var string
125
     */
126
    protected $constrainName = '';
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function getConstraints(): array
132
    {
133
        $constraints = parent::getConstraints();
134
135
        if ($this->constrained) {
136
            $constraints[] = $this->constrainName;
137
        }
138
139
        return $constraints;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function abstractType(): string
146
    {
147
        if (!empty($this->enumValues)) {
148
            return 'enum';
149
        }
150
151
        return parent::abstractType();
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157
    public function primary(): AbstractColumn
158
    {
159
        if (!empty($this->type) && $this->type != 'serial') {
160
            //Change type of already existed column (we can't use "serial" alias here)
161
            $this->type = 'integer';
162
163
            return $this;
164
        }
165
166
        return $this->setType('primary');
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function bigPrimary(): AbstractColumn
173
    {
174
        if (!empty($this->type) && $this->type != 'bigserial') {
175
            //Change type of already existed column (we can't use "serial" alias here)
176
            $this->type = 'bigint';
177
178
            return $this;
179
        }
180
181
        return $this->setType('bigPrimary');
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187 View Code Duplication
    public function enum($values): AbstractColumn
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
188
    {
189
        $this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args());
190
191
        $this->type = 'character';
192
        foreach ($this->enumValues as $value) {
193
            $this->size = max((int)$this->size, strlen($value));
194
        }
195
196
        return $this;
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202
    public function sqlStatement(Driver $driver): string
203
    {
204
        $statement = parent::sqlStatement($driver);
205
206
        if ($this->abstractType() != 'enum') {
207
            //Nothing special
208
            return $statement;
209
        }
210
211
        //We have add constraint for enum type
212
        $enumValues = [];
213
        foreach ($this->enumValues as $value) {
214
            $enumValues[] = $driver->quote($value);
215
        }
216
217
        $constrain = $driver->identifier($this->enumConstraint());
218
        $column = $driver->identifier($this->getName());
219
        $values = implode(', ', $enumValues);
220
221
        return "{$statement} CONSTRAINT {$constrain} CHECK ($column IN ({$values}))";
222
    }
223
224
    /**
225
     * Generate set of operations need to change column.
226
     *
227
     * @param Driver         $driver
228
     * @param AbstractColumn $initial
229
     *
230
     * @return array
231
     */
232
    public function alterOperations(Driver $driver, AbstractColumn $initial): array
233
    {
234
        $operations = [];
235
236
        //To simplify comparation
237
        $currentType = [$this->type, $this->size, $this->precision, $this->scale];
238
        $initialType = [$initial->type, $initial->size, $initial->precision, $initial->scale];
239
240
        $identifier = $driver->identifier($this->getName());
241
242
        /*
243
         * This block defines column type and all variations.
244
         */
245
        if ($currentType != $initialType) {
246
            if ($this->abstractType() == 'enum') {
247
                //Getting longest value
248
                $enumSize = $this->size;
249
                foreach ($this->enumValues as $value) {
250
                    $enumSize = max($enumSize, strlen($value));
251
                }
252
253
                $type = "ALTER COLUMN {$identifier} TYPE character($enumSize)";
254
                $operations[] = $type;
255
            } else {
256
                $type = "ALTER COLUMN {$identifier} TYPE {$this->type}";
257
258
                if (!empty($this->size)) {
259
                    $type .= "($this->size)";
260
                } elseif (!empty($this->precision)) {
261
                    $type .= "($this->precision, $this->scale)";
262
                }
263
264
                //Required to perform cross conversion
265
                $operations[] = "{$type} USING {$identifier}::{$this->type}";
266
            }
267
        }
268
269
        //Dropping enum constrain before any operation
270
        if ($initial->abstractType() == 'enum' && $this->constrained) {
271
            $operations[] = 'DROP CONSTRAINT ' . $driver->identifier($this->enumConstraint());
272
        }
273
274
        //Default value set and dropping
275
        if ($initial->defaultValue != $this->defaultValue) {
276
            if (is_null($this->defaultValue)) {
277
                $operations[] = "ALTER COLUMN {$identifier} DROP DEFAULT";
278
            } else {
279
                $operations[] = "ALTER COLUMN {$identifier} SET DEFAULT {$this->quoteDefault($driver)}";
280
            }
281
        }
282
283
        //Nullable option
284
        if ($initial->nullable != $this->nullable) {
285
            $operations[] = "ALTER COLUMN {$identifier} " . (!$this->nullable ? 'SET' : 'DROP') . ' NOT NULL';
286
        }
287
288
        if ($this->abstractType() == 'enum') {
289
            $enumValues = [];
290
            foreach ($this->enumValues as $value) {
291
                $enumValues[] = $driver->quote($value);
292
            }
293
294
            $operations[] = "ADD CONSTRAINT {$driver->identifier($this->enumConstraint())} "
295
                . "CHECK ({$identifier} IN (" . implode(', ', $enumValues) . '))';
296
        }
297
298
        return $operations;
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    protected function quoteEnum(Driver $driver): string
305
    {
306
        //Postgres enums are just constrained strings
307
        return '(' . $this->size . ')';
308
    }
309
310
    /**
311
     * Get/generate name for enum constraint.
312
     *
313
     * @return string
314
     */
315 View Code Duplication
    private function enumConstraint(): string
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
316
    {
317
        if (empty($this->constrainName)) {
318
            $this->constrainName = $this->table . '_' . $this->getName() . '_enum_' . uniqid();
319
        }
320
321
        return $this->constrainName;
322
    }
323
324
    /**
325
     * Normalize default value.
326
     */
327
    private function normalizeDefault()
328
    {
329
        if ($this->hasDefaultValue()) {
330
            if (preg_match("/^'?(.*?)'?::(.+)/", $this->defaultValue, $matches)) {
331
                //In database: 'value'::TYPE
332
                $this->defaultValue = $matches[1];
333
            } elseif ($this->type == 'bit') {
334
                $this->defaultValue = bindec(
335
                    substr($this->defaultValue, 2, strpos($this->defaultValue, '::') - 3)
336
                );
337
            } elseif ($this->type == 'boolean') {
338
                $this->defaultValue = (strtolower($this->defaultValue) == 'true');
339
            }
340
        }
341
    }
342
343
    /**
344
     * @param string $table  Table name.
345
     * @param array  $schema
346
     * @param Driver $driver Postgres columns are bit more complex.
347
     *
348
     * @return PostgresColumn
349
     */
350
    public static function createInstance(string $table, array $schema, Driver $driver): self
351
    {
352
        $column = new self($table, $schema['column_name'], $driver->getTimezone());
353
354
        $column->type = $schema['data_type'];
355
        $column->defaultValue = $schema['column_default'];
356
        $column->nullable = $schema['is_nullable'] == 'YES';
357
358
        if (
359
            in_array($column->type, ['int', 'bigint', 'integer'])
360
            && preg_match('/nextval(.*)/', $column->defaultValue)
361
        ) {
362
            $column->type = ($column->type == 'bigint' ? 'bigserial' : 'serial');
363
            $column->autoIncrement = true;
364
365
            $column->defaultValue = new Fragment($column->defaultValue);
366
367
            return $column;
368
        }
369
370
        if (strpos($column->type, 'char') !== false && $schema['character_maximum_length']) {
371
            $column->size = $schema['character_maximum_length'];
372
        }
373
374 View Code Duplication
        if ($column->type == 'numeric') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
375
            $column->precision = $schema['numeric_precision'];
376
            $column->scale = $schema['numeric_scale'];
377
        }
378
379
        if ($column->type == 'USER-DEFINED' && $schema['typtype'] == 'e') {
380
            $column->type = $schema['typname'];
381
382
            /**
383
             * Attention, this is not default spiral enum type emulated via CHECK. This is real
384
             * Postgres enum type.
385
             */
386
            self::resolveEnum($driver, $column);
387
        }
388
389
        if (strpos($column->type, 'char') !== false && !empty($column->size)) {
390
            //Potential enum with manually created constraint (check in)
391
            self::resolveConstrains($driver, $schema['tableOID'], $column);
392
        }
393
394
        $column->normalizeDefault();
395
396
        return $column;
397
    }
398
399
    /**
400
     * Resolving enum constrain and converting it into proper enum values set.
401
     *
402
     * @param Driver         $driver
403
     * @param string|int     $tableOID
404
     * @param PostgresColumn $column
405
     */
406
    private static function resolveConstrains(Driver $driver, $tableOID, PostgresColumn $column)
407
    {
408
        $query = "SELECT conname, consrc FROM pg_constraint WHERE conrelid = ? AND contype = 'c' AND (consrc LIKE ? OR consrc LIKE ?)";
409
410
        $constraints = $driver->query($query, [
411
            $tableOID,
412
            '(' . $column->name . '%',
413
            '("' . $column->name . '%'
414
        ]);
415
416
        foreach ($constraints as $constraint) {
417
            if (preg_match('/ARRAY\[([^\]]+)\]/', $constraint['consrc'], $matches)) {
418
                $enumValues = explode(',', $matches[1]);
419 View Code Duplication
                foreach ($enumValues as &$value) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
420
                    if (preg_match("/^'?(.*?)'?::(.+)/", trim($value), $matches)) {
421
                        //In database: 'value'::TYPE
422
                        $value = $matches[1];
423
                    }
424
425
                    unset($value);
426
                }
427
428
                $column->enumValues = $enumValues;
429
                $column->constrainName = $constraint['conname'];
430
                $column->constrained = true;
431
            }
432
        }
433
    }
434
435
    /**
436
     * Resolve native ENUM type if presented.
437
     *
438
     * @param Driver         $driver
439
     * @param PostgresColumn $column
440
     */
441
    private static function resolveEnum(Driver $driver, PostgresColumn $column)
442
    {
443
        $range = $driver->query('SELECT enum_range(NULL::' . $column->type . ')')->fetchColumn(0);
444
445
        $column->enumValues = explode(',', substr($range, 1, -1));
446
447
        if (!empty($column->defaultValue)) {
448
            //In database: 'value'::enumType
449
            $column->defaultValue = substr(
450
                $column->defaultValue,
451
                1,
452
                strpos($column->defaultValue, $column->type) - 4
453
            );
454
        }
455
    }
456
457
    /**
458
     * {@inheritdoc}
459
     */
460
    public function compare(ColumnInterface $initial): bool
461
    {
462
        if (parent::compare($initial)) {
463
            return true;
464
        }
465
466
        if (
467
            in_array($this->abstractType(), ['primary', 'bigPrimary'])
468
            && $initial->getDefaultValue() != $this->getDefaultValue()
469
        ) {
470
            //PG adds default values to primary keys
471
            return true;
472
        }
473
474
        return false;
475
    }
476
}
477