Completed
Branch develop (c2aa4c)
by Anton
05:17
created

ColumnSchema::alteringOperations()   C

Complexity

Conditions 14
Paths 180

Size

Total Lines 60
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 60
rs 5.7378
cc 14
eloc 35
nc 180
nop 1

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
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
namespace Spiral\Database\Drivers\Postgres\Schemas;
9
10
use Spiral\Database\Entities\Schemas\AbstractColumn;
11
use Spiral\Database\Injections\Fragment;
12
13
/**
14
 * Postgres column schema.
15
 */
16
class ColumnSchema extends AbstractColumn
17
{
18
    /**
19
     * {@inheritdoc}
20
     */
21
    protected $mapping = [
22
        //Primary sequences
23
        'primary'     => ['type' => 'serial', 'autoIncrement' => true, 'nullable' => false],
24
        'bigPrimary'  => ['type' => 'bigserial', 'autoIncrement' => true, 'nullable' => false],
25
26
        //Enum type (mapped via method)
27
        'enum'        => 'enum',
28
29
        //Logical types
30
        'boolean'     => 'boolean',
31
32
        //Integer types (size can always be changed with size method), longInteger has method alias
33
        //bigInteger
34
        'integer'     => 'integer',
35
        'tinyInteger' => 'smallint',
36
        'bigInteger'  => 'bigint',
37
38
        //String with specified length (mapped via method)
39
        'string'      => 'character varying',
40
41
        //Generic types
42
        'text'        => 'text',
43
        'tinyText'    => 'text',
44
        'longText'    => 'text',
45
46
        //Real types
47
        'double'      => 'double precision',
48
        'float'       => 'real',
49
50
        //Decimal type (mapped via method)
51
        'decimal'     => 'numeric',
52
53
        //Date and Time types
54
        'datetime'    => 'timestamp without time zone',
55
        'date'        => 'date',
56
        'time'        => 'time',
57
        'timestamp'   => 'timestamp without time zone',
58
59
        //Binary types
60
        'binary'      => 'bytea',
61
        'tinyBinary'  => 'bytea',
62
        'longBinary'  => 'bytea',
63
64
        //Additional types
65
        'json'        => 'json'
66
    ];
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    protected $reverseMapping = [
72
        'primary'     => ['serial'],
73
        'bigPrimary'  => ['bigserial'],
74
        'enum'        => ['enum'],
75
        'boolean'     => ['boolean'],
76
        'integer'     => ['int', 'integer', 'int4'],
77
        'tinyInteger' => ['smallint'],
78
        'bigInteger'  => ['bigint', 'int8'],
79
        'string'      => ['character varying', 'character'],
80
        'text'        => ['text'],
81
        'double'      => ['double precision'],
82
        'float'       => ['real', 'money'],
83
        'decimal'     => ['numeric'],
84
        'date'        => ['date'],
85
        'time'        => ['time', 'time with time zone', 'time without time zone'],
86
        'timestamp'   => ['timestamp', 'timestamp with time zone', 'timestamp without time zone'],
87
        'binary'      => ['bytea'],
88
        'json'        => ['json']
89
    ];
90
91
    /**
92
     * Field is auto incremental.
93
     *
94
     * @var bool
95
     */
96
    protected $autoIncrement = false;
97
98
    /**
99
     * Name of enum constraint associated with field.
100
     *
101
     * @var string
102
     */
103
    protected $enumConstraint = '';
104
105
    /**
106
     * {@inheritdoc}
107
     */
108
    public function getConstraints()
109
    {
110
        $constraints = parent::getConstraints();
111
112
        if (!empty($this->enumConstraint)) {
113
            $constraints[] = $this->enumConstraint;
114
        }
115
116
        return $constraints;
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122
    public function abstractType()
123
    {
124
        if (!empty($this->enumValues)) {
125
            return 'enum';
126
        }
127
128
        return parent::abstractType();
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function primary()
135
    {
136
        $this->autoIncrement = true;
137
138
        //Changing type of already created primary key (we can't use "serial" alias here)
139
        if (!empty($this->type) && $this->type != 'serial') {
140
            $this->type = 'integer';
141
142
            return $this;
143
        }
144
145
        return parent::primary();
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151
    public function bigPrimary()
152
    {
153
        $this->autoIncrement = true;
154
155
        //Changing type of already created primary key (we can't use "serial" alias here)
156
        if (!empty($this->type) && $this->type != 'bigserial') {
157
            $this->type = 'bigint';
158
159
            return $this;
160
        }
161
162
        return parent::bigPrimary();
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168 View Code Duplication
    public function enum($values)
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...
169
    {
170
        $this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args());
171
172
        $this->type = 'character';
173
        foreach ($this->enumValues as $value) {
174
            $this->size = max((int)$this->size, strlen($value));
175
        }
176
177
        return $this;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183
    public function sqlStatement()
184
    {
185
        $statement = parent::sqlStatement();
186
187
        if ($this->abstractType() != 'enum') {
188
            return $statement;
189
        }
190
191
        //We have add constraint for enum type
192
        $enumValues = [];
193
        foreach ($this->enumValues as $value) {
194
            $enumValues[] = $this->table->driver()->getPDO()->quote($value);
195
        }
196
197
        return "$statement CONSTRAINT {$this->enumConstraint(true, true)} "
198
        . "CHECK ({$this->getName(true)} IN (" . join(', ', $enumValues) . "))";
199
    }
200
201
    /**
202
     * Generate set of altering operations should be applied to column to change it's type, size,
203
     * default value or null flag.
204
     *
205
     * @param AbstractColumn $original
206
     * @return array
207
     */
208
    public function alteringOperations(AbstractColumn $original)
209
    {
210
        $operations = [];
211
212
        $typeDefinition = [$this->type, $this->size, $this->precision, $this->scale];
213
        $originalType = [$original->type, $original->size, $original->precision, $original->scale];
214
215
        if ($typeDefinition != $originalType) {
216
            if ($this->abstractType() == 'enum') {
217
                //Getting longest value
218
                $enumSize = $this->size;
219
                foreach ($this->enumValues as $value) {
220
                    $enumSize = max($enumSize, strlen($value));
221
                }
222
223
                $type = "ALTER COLUMN {$this->getName(true)} TYPE character($enumSize)";
224
                $operations[] = $type;
225
            } else {
226
                $type = "ALTER COLUMN {$this->getName(true)} TYPE {$this->type}";
227
228
                if (!empty($this->size)) {
229
                    $type .= "($this->size)";
230
                } elseif (!empty($this->precision)) {
231
                    $type .= "($this->precision, $this->scale)";
232
                }
233
234
                //Required to perform cross conversion
235
                $operations[] = "{$type} USING {$this->getName(true)}::{$this->type}";
236
            }
237
        }
238
239
        if ($original->abstractType() == 'enum' && !empty($this->enumConstraint)) {
240
            $operations[] = 'DROP CONSTRAINT ' . $this->enumConstraint(true);
241
        }
242
243
        if ($original->defaultValue != $this->defaultValue) {
244
            if (is_null($this->defaultValue)) {
245
                $operations[] = "ALTER COLUMN {$this->getName(true)} DROP DEFAULT";
246
            } else {
247
                $operations[] = "ALTER COLUMN {$this->getName(true)} SET DEFAULT {$this->prepareDefault()}";
248
            }
249
        }
250
251
        if ($original->nullable != $this->nullable) {
252
            $operations[] = "ALTER COLUMN {$this->getName(true)} "
253
                . (!$this->nullable ? 'SET' : 'DROP') . " NOT NULL";
254
        }
255
256
        if ($this->abstractType() == 'enum') {
257
            $enumValues = [];
258
            foreach ($this->enumValues as $value) {
259
                $enumValues[] = $this->table->driver()->getPDO()->quote($value);
260
            }
261
262
            $operations[] = "ADD CONSTRAINT {$this->enumConstraint(true)} "
263
                . "CHECK ({$this->getName(true)} IN (" . join(', ', $enumValues) . "))";
264
        }
265
266
        return $operations;
267
    }
268
269
    /**
270
     * {@inheritdoc}
271
     */
272
    protected function resolveSchema($schema)
273
    {
274
        $this->type = $schema['data_type'];
275
        $this->defaultValue = $schema['column_default'];
276
        $this->nullable = $schema['is_nullable'] == 'YES';
277
278
        if (
279
            in_array($this->type, ['int', 'bigint', 'integer'])
280
            && preg_match("/nextval(.*)/", $this->defaultValue)
281
        ) {
282
            $this->type = ($this->type == 'bigint' ? 'bigserial' : 'serial');
283
            $this->autoIncrement = true;
284
285
            $this->defaultValue = new Fragment($this->defaultValue);
286
287
            return;
288
        }
289
290
        if (
291
            ($this->type == 'character varying' || $this->type == 'character')
292
            && $schema['character_maximum_length']
293
        ) {
294
            $this->size = $schema['character_maximum_length'];
295
        }
296
297 View Code Duplication
        if ($this->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...
298
            $this->precision = $schema['numeric_precision'];
299
            $this->scale = $schema['numeric_scale'];
300
        }
301
302
        /**
303
         * Attention, this is not default spiral enum type emulated via CHECK. This is real Postgres
304
         * enum type.
305
         */
306
        if ($this->type == 'USER-DEFINED' && $schema['typtype'] == 'e') {
307
            $this->type = $schema['typname'];
308
            $this->resolveNativeEnum();
309
        }
310
311
        //Potential enum with manually created constraint (check in)
312
        if (
313
            ($this->type == 'character' || $this->type == 'character varying')
314
            && !empty($this->size)
315
        ) {
316
            $this->checkCheckConstrain($schema['tableOID']);
317
        }
318
319
        $this->normalizeDefault();
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325
    protected function prepareEnum()
326
    {
327
        return '(' . $this->size . ')';
328
    }
329
330
    /**
331
     * Get name of enum constraint.
332
     *
333
     * @param bool $quote
334
     * @param bool $temporary If true enumConstraint identifier will be generated only for visual
335
     *                        purposes only.
336
     * @return string
337
     */
338
    private function enumConstraint($quote = false, $temporary = false)
339
    {
340
        if (empty($this->enumConstraint)) {
341
            if ($temporary) {
342
                return $this->table->getName() . '_' . $this->getName() . '_enum';
343
            }
344
345
            $this->enumConstraint = $this->table->getName() . '_' . $this->getName() . '_enum_' . uniqid();
346
        }
347
348
        return $quote ? $this->table->driver()->identifier($this->enumConstraint) : $this->enumConstraint;
349
    }
350
351
    /**
352
     * Normalize default value.
353
     */
354
    private function normalizeDefault()
355
    {
356
        if ($this->hasDefaultValue()) {
357
            if (preg_match("/^'?(.*?)'?::(.+)/", $this->defaultValue, $matches)) {
358
                //In database: 'value'::TYPE
1 ignored issue
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
359
                $this->defaultValue = $matches[1];
360
            } elseif ($this->type == 'bit') {
361
                $this->defaultValue = bindec(
362
                    substr($this->defaultValue, 2, strpos($this->defaultValue, '::') - 3)
363
                );
364
            } elseif ($this->type == 'boolean') {
365
                $this->defaultValue = (strtolower($this->defaultValue) == 'true');
366
            }
367
        }
368
    }
369
370
    /**
371
     * Resolve native enum type.
372
     **/
373
    private function resolveNativeEnum()
374
    {
375
        $range = $this->table->driver()->query(
376
            'SELECT enum_range(NULL::' . $this->type . ')'
377
        )->fetchColumn(0);
378
379
        $this->enumValues = explode(',', substr($range, 1, -1));
380
381
        if (!empty($this->defaultValue)) {
382
            //In database: 'value'::enumType
1 ignored issue
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
383
            $this->defaultValue = substr(
384
                $this->defaultValue,
385
                1,
386
                strpos($this->defaultValue, $this->type) - 4
387
            );
388
        }
389
    }
390
391
    /**
392
     * Check if column was declared with check constrain. I love name of this method.
393
     *
394
     * @param string $tableOID
395
     */
396
    private function checkCheckConstrain($tableOID)
397
    {
398
        $query = "SELECT conname, consrc FROM pg_constraint "
399
            . "WHERE conrelid = ? AND contype = 'c' AND (consrc LIKE ? OR consrc LIKE ?)";
400
401
        $constraints = $this->table->driver()->query(
402
            $query, [$tableOID, '(' . $this->name . '%', '("' . $this->name . '%',]
403
        );
404
405 View Code Duplication
        foreach ($constraints as $constraint) {
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...
406
            if (preg_match('/ARRAY\[([^\]]+)\]/', $constraint['consrc'], $matches)) {
407
                $enumValues = explode(',', $matches[1]);
408
                foreach ($enumValues as &$value) {
409
                    if (preg_match("/^'?(.*?)'?::(.+)/", trim($value), $matches)) {
410
                        //In database: 'value'::TYPE
1 ignored issue
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
411
                        $value = $matches[1];
412
                    }
413
414
                    unset($value);
415
                }
416
417
                $this->enumValues = $enumValues;
418
                $this->enumConstraint = $constraint['conname'];
419
            }
420
        }
421
    }
422
}