Completed
Branch feature/pre-split (713d19)
by Anton
03:04
created

PostgresColumn   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 430
Duplicated Lines 7.21 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 31
loc 430
rs 6.5957
c 0
b 0
f 0
wmc 56
lcom 1
cbo 4

13 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 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 quoteEnum() 0 5 1

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
 * 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\Prototypes\AbstractColumn;
12
13
/**
14
 * @todo investigate potential issue with entity non handling enum correctly when multiple
15
 * @todo column changes happen in one session (who the hell will do that?)
16
 */
17
class PostgresColumn extends AbstractColumn
18
{
19
    /**
20
     * Default timestamp expression (driver specific).
21
     */
22
    const DATETIME_NOW = 'now()';
23
24
    /**
25
     * {@inheritdoc}
26
     */
27
    protected $mapping = [
28
        //Primary sequences
29
        'primary'     => ['type' => 'serial', 'autoIncrement' => true, 'nullable' => false],
30
        'bigPrimary'  => ['type' => 'bigserial', 'autoIncrement' => true, 'nullable' => false],
31
32
        //Enum type (mapped via method)
33
        'enum'        => 'enum',
34
35
        //Logical types
36
        'boolean'     => 'boolean',
37
38
        //Integer types (size can always be changed with size method), longInteger has method alias
39
        //bigInteger
40
        'integer'     => 'integer',
41
        'tinyInteger' => 'smallint',
42
        'bigInteger'  => 'bigint',
43
44
        //String with specified length (mapped via method)
45
        'string'      => 'character varying',
46
47
        //Generic types
48
        'text'        => 'text',
49
        'tinyText'    => 'text',
50
        'longText'    => 'text',
51
52
        //Real types
53
        'double'      => 'double precision',
54
        'float'       => 'real',
55
56
        //Decimal type (mapped via method)
57
        'decimal'     => 'numeric',
58
59
        //Date and Time types
60
        'datetime'    => 'timestamp without time zone',
61
        'date'        => 'date',
62
        'time'        => 'time without time zone',
63
        'timestamp'   => 'timestamp without time zone',
64
65
        //Binary types
66
        'binary'      => 'bytea',
67
        'tinyBinary'  => 'bytea',
68
        'longBinary'  => 'bytea',
69
70
        //Additional types
71
        'json'        => 'json',
72
    ];
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    protected $reverseMapping = [
78
        'primary'     => ['serial'],
79
        'bigPrimary'  => ['bigserial'],
80
        'enum'        => ['enum'],
81
        'boolean'     => ['boolean'],
82
        'integer'     => ['int', 'integer', 'int4'],
83
        'tinyInteger' => ['smallint'],
84
        'bigInteger'  => ['bigint', 'int8'],
85
        'string'      => ['character varying', 'character'],
86
        'text'        => ['text'],
87
        'double'      => ['double precision'],
88
        'float'       => ['real', 'money'],
89
        'decimal'     => ['numeric'],
90
        'date'        => ['date'],
91
        'time'        => ['time', 'time with time zone', 'time without time zone'],
92
        'timestamp'   => ['timestamp', 'timestamp with time zone', 'timestamp without time zone'],
93
        'binary'      => ['bytea'],
94
        'json'        => ['json'],
95
    ];
96
97
    /**
98
     * Field is auto incremental.
99
     *
100
     * @var bool
101
     */
102
    protected $autoIncrement = false;
103
104
    /**
105
     * Indication that column have enum constrain.
106
     *
107
     * @var bool
108
     */
109
    protected $constrained = false;
110
111
    /**
112
     * Name of enum constraint associated with field.
113
     *
114
     * @var string
115
     */
116
    protected $constrainName = '';
117
118
    /**
119
     * {@inheritdoc}
120
     */
121
    public function getConstraints(): array
122
    {
123
        $constraints = parent::getConstraints();
124
125
        if ($this->constrained) {
126
            $constraints[] = $this->constrainName;
127
        }
128
129
        return $constraints;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function abstractType(): string
136
    {
137
        if (!empty($this->enumValues)) {
138
            return 'enum';
139
        }
140
141
        return parent::abstractType();
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147
    public function primary(): AbstractColumn
148
    {
149
        if (!empty($this->type) && $this->type != 'serial') {
150
            //Change type of already existed column (we can't use "serial" alias here)
151
            $this->type = 'integer';
152
153
            return $this;
154
        }
155
156
        return $this->setType('primary');
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function bigPrimary(): AbstractColumn
163
    {
164
        if (!empty($this->type) && $this->type != 'bigserial') {
165
            //Change type of already existed column (we can't use "serial" alias here)
166
            $this->type = 'bigint';
167
168
            return $this;
169
        }
170
171
        return $this->setType('bigPrimary');
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 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...
178
    {
179
        $this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args());
180
181
        $this->type = 'character';
182
        foreach ($this->enumValues as $value) {
183
            $this->size = max((int)$this->size, strlen($value));
184
        }
185
186
        return $this;
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function sqlStatement(Driver $driver): string
193
    {
194
        $statement = parent::sqlStatement($driver);
195
196
        if ($this->abstractType() != 'enum') {
197
            //Nothing special
198
            return $statement;
199
        }
200
201
        //We have add constraint for enum type
202
        $enumValues = [];
203
        foreach ($this->enumValues as $value) {
204
            $enumValues[] = $driver->quote($value);
205
        }
206
207
        $constrain = $driver->identifier($this->enumConstraint());
208
        $column = $driver->identifier($this->getName());
209
        $values = implode(', ', $enumValues);
210
211
        return "{$statement} CONSTRAINT {$constrain} CHECK ($column IN ({$values}))";
212
    }
213
214
    /**
215
     * Generate set of operations need to change column.
216
     *
217
     * @param Driver         $driver
218
     * @param AbstractColumn $initial
219
     *
220
     * @return array
221
     */
222
    public function alterOperations(Driver $driver, AbstractColumn $initial): array
223
    {
224
        $operations = [];
225
226
        //To simplify comparation
227
        $currentType = [$this->type, $this->size, $this->precision, $this->scale];
228
        $initialType = [$initial->type, $initial->size, $initial->precision, $initial->scale];
229
230
        $identifier = $driver->identifier($this->getName());
231
232
        /*
233
         * This block defines column type and all variations.
234
         */
235
        if ($currentType != $initialType) {
236
            if ($this->abstractType() == 'enum') {
237
                //Getting longest value
238
                $enumSize = $this->size;
239
                foreach ($this->enumValues as $value) {
240
                    $enumSize = max($enumSize, strlen($value));
241
                }
242
243
                $type = "ALTER COLUMN {$identifier} TYPE character($enumSize)";
244
                $operations[] = $type;
245
            } else {
246
                $type = "ALTER COLUMN {$identifier} TYPE {$this->type}";
247
248
                if (!empty($this->size)) {
249
                    $type .= "($this->size)";
250
                } elseif (!empty($this->precision)) {
251
                    $type .= "($this->precision, $this->scale)";
252
                }
253
254
                //Required to perform cross conversion
255
                $operations[] = "{$type} USING {$identifier}::{$this->type}";
256
            }
257
        }
258
259
        //Dropping enum constrain before any operation
260
        if ($initial->abstractType() == 'enum' && $this->constrained) {
261
            $operations[] = 'DROP CONSTRAINT ' . $driver->identifier($this->enumConstraint());
262
        }
263
264
        //Default value set and dropping
265
        if ($initial->defaultValue != $this->defaultValue) {
266
            if (is_null($this->defaultValue)) {
267
                $operations[] = "ALTER COLUMN {$identifier} DROP DEFAULT";
268
            } else {
269
                $operations[] = "ALTER COLUMN {$identifier} SET DEFAULT {$this->quoteDefault($driver)}";
270
            }
271
        }
272
273
        //Nullable option
274
        if ($initial->nullable != $this->nullable) {
275
            $operations[] = "ALTER COLUMN {$identifier} " . (!$this->nullable ? 'SET' : 'DROP') . ' NOT NULL';
276
        }
277
278
        if ($this->abstractType() == 'enum') {
279
            $enumValues = [];
280
            foreach ($this->enumValues as $value) {
281
                $enumValues[] = $driver->quote($value);
282
            }
283
284
            $operations[] = "ADD CONSTRAINT {$driver->identifier($this->enumConstraint())} "
285
                . "CHECK ({$identifier} IN (" . implode(', ', $enumValues) . '))';
286
        }
287
288
        return $operations;
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294
    protected function quoteEnum(Driver $driver): string
295
    {
296
        //Postgres enums are just constrained strings
297
        return '(' . $this->size . ')';
298
    }
299
300
    /**
301
     * Get/generate name for enum constraint.
302
     *
303
     * @return string
304
     */
305 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...
306
    {
307
        if (empty($this->constrainName)) {
308
            $this->constrainName = $this->table . '_' . $this->getName() . '_enum_' . uniqid();
309
        }
310
311
        return $this->constrainName;
312
    }
313
314
    /**
315
     * Normalize default value.
316
     */
317
    private function normalizeDefault()
318
    {
319
        if ($this->hasDefaultValue()) {
320
            if (preg_match("/^'?(.*?)'?::(.+)/", $this->defaultValue, $matches)) {
321
                //In database: 'value'::TYPE
322
                $this->defaultValue = $matches[1];
323
            } elseif ($this->type == 'bit') {
324
                $this->defaultValue = bindec(
325
                    substr($this->defaultValue, 2, strpos($this->defaultValue, '::') - 3)
326
                );
327
            } elseif ($this->type == 'boolean') {
328
                $this->defaultValue = (strtolower($this->defaultValue) == 'true');
329
            }
330
        }
331
    }
332
333
    /**
334
     * @param string $table  Table name.
335
     * @param array  $schema
336
     * @param Driver $driver Postgres columns are bit more complex.
337
     *
338
     * @return PostgresColumn
339
     */
340
    public static function createInstance(string $table, array $schema, Driver $driver): self
341
    {
342
        $column = new self($table, $schema['column_name']);
343
344
        $column->type = $schema['data_type'];
345
        $column->defaultValue = $schema['column_default'];
346
        $column->nullable = $schema['is_nullable'] == 'YES';
347
348
        if (
349
            in_array($column->type, ['int', 'bigint', 'integer'])
350
            && preg_match('/nextval(.*)/', $column->defaultValue)
351
        ) {
352
            $column->type = ($column->type == 'bigint' ? 'bigserial' : 'serial');
353
            $column->autoIncrement = true;
354
355
            $column->defaultValue = new Fragment($column->defaultValue);
356
357
            return $column;
358
        }
359
360
        if (strpos($column->type, 'char') !== false && $schema['character_maximum_length']) {
361
            $column->size = $schema['character_maximum_length'];
362
        }
363
364 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...
365
            $column->precision = $schema['numeric_precision'];
366
            $column->scale = $schema['numeric_scale'];
367
        }
368
369
        if ($column->type == 'USER-DEFINED' && $schema['typtype'] == 'e') {
370
            $column->type = $schema['typname'];
371
372
            /**
373
             * Attention, this is not default spiral enum type emulated via CHECK. This is real
374
             * Postgres enum type.
375
             */
376
            self::resolveEnum($driver, $column);
377
        }
378
379
        if (strpos($column->type, 'char') !== false && !empty($column->size)) {
380
            //Potential enum with manually created constraint (check in)
381
            self::resolveConstrains($driver, $schema['tableOID'], $column);
382
        }
383
384
        $column->normalizeDefault();
385
386
        return $column;
387
    }
388
389
    /**
390
     * Resolving enum constrain and converting it into proper enum values set.
391
     *
392
     * @param Driver         $driver
393
     * @param string|int     $tableOID
394
     * @param PostgresColumn $column
395
     */
396
    private static function resolveConstrains(Driver $driver, $tableOID, PostgresColumn $column)
397
    {
398
        $query = "SELECT conname, consrc FROM pg_constraint WHERE conrelid = ? AND contype = 'c' AND (consrc LIKE ? OR consrc LIKE ?)";
399
400
        $constraints = $driver->query($query, [
401
            $tableOID,
402
            '(' . $column->name . '%',
403
            '("' . $column->name . '%'
404
        ]);
405
406
        foreach ($constraints as $constraint) {
407
            if (preg_match('/ARRAY\[([^\]]+)\]/', $constraint['consrc'], $matches)) {
408
                $enumValues = explode(',', $matches[1]);
409 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...
410
                    if (preg_match("/^'?(.*?)'?::(.+)/", trim($value), $matches)) {
411
                        //In database: 'value'::TYPE
412
                        $value = $matches[1];
413
                    }
414
415
                    unset($value);
416
                }
417
418
                $column->enumValues = $enumValues;
419
                $column->constrainName = $constraint['conname'];
420
                $column->constrained = true;
421
            }
422
        }
423
    }
424
425
    /**
426
     * Resolve native ENUM type if presented.
427
     *
428
     * @param Driver         $driver
429
     * @param PostgresColumn $column
430
     */
431
    private static function resolveEnum(Driver $driver, PostgresColumn $column)
432
    {
433
        $range = $driver->query('SELECT enum_range(NULL::' . $column->type . ')')->fetchColumn(0);
434
435
        $column->enumValues = explode(',', substr($range, 1, -1));
436
437
        if (!empty($column->defaultValue)) {
438
            //In database: 'value'::enumType
439
            $column->defaultValue = substr(
440
                $column->defaultValue,
441
                1,
442
                strpos($column->defaultValue, $column->type) - 4
443
            );
444
        }
445
    }
446
}