Completed
Branch feature/pre-split (1fb805)
by Anton
03:35
created

SQLServerColumn::defaultConstrain()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 8
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 8
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\Database\Drivers\SQLServer\Schemas;
8
9
use Spiral\Database\Entities\Driver;
10
use Spiral\Database\Schemas\Prototypes\AbstractColumn;
11
12
/**
13
 * @todo investigate potential issue with entity non handling enum correctly when multiple
14
 * @todo column changes happen in one session (who the hell will do that?)
15
 */
16
class SQLServerColumn extends AbstractColumn
17
{
18
    /**
19
     * {@inheritdoc}
20
     */
21
    protected $mapping = [
22
        //Primary sequences
23
        'primary'     => ['type' => 'int', 'identity' => true, 'nullable' => false],
24
        'bigPrimary'  => ['type' => 'bigint', 'identity' => true, 'nullable' => false],
25
26
        //Enum type (mapped via method)
27
        'enum'        => 'enum',
28
29
        //Logical types
30
        'boolean'     => 'bit',
31
32
        //Integer types (size can always be changed with size method), longInteger has method alias
33
        //bigInteger
34
        'integer'     => 'int',
35
        'tinyInteger' => 'tinyint',
36
        'bigInteger'  => 'bigint',
37
38
        //String with specified length (mapped via method)
39
        'string'      => 'varchar',
40
41
        //Generic types
42
        'text'        => ['type' => 'varchar', 'size' => 0],
43
        'tinyText'    => ['type' => 'varchar', 'size' => 0],
44
        'longText'    => ['type' => 'varchar', 'size' => 0],
45
46
        //Real types
47
        'double'      => 'float',
48
        'float'       => 'real',
49
50
        //Decimal type (mapped via method)
51
        'decimal'     => 'decimal',
52
53
        //Date and Time types
54
        'datetime'    => 'datetime',
55
        'date'        => 'date',
56
        'time'        => 'time',
57
        'timestamp'   => 'datetime',
58
59
        //Binary types
60
        'binary'      => ['type' => 'varbinary', 'size' => 0],
61
        'tinyBinary'  => ['type' => 'varbinary', 'size' => 0],
62
        'longBinary'  => ['type' => 'varbinary', 'size' => 0],
63
64
        //Additional types
65
        'json'        => ['type' => 'varchar', 'size' => 0],
66
    ];
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    protected $reverseMapping = [
72
        'primary'     => [['type' => 'int', 'identity' => true]],
73
        'bigPrimary'  => [['type' => 'bigint', 'identity' => true]],
74
        'enum'        => ['enum'],
75
        'boolean'     => ['bit'],
76
        'integer'     => ['int'],
77
        'tinyInteger' => ['tinyint', 'smallint'],
78
        'bigInteger'  => ['bigint'],
79
        'text'        => [['type' => 'varchar', 'size' => 0]],
80
        'string'      => ['varchar', 'char'],
81
        'double'      => ['float'],
82
        'float'       => ['real'],
83
        'decimal'     => ['decimal'],
84
        'timestamp'   => ['datetime'],
85
        'date'        => ['date'],
86
        'time'        => ['time'],
87
        'binary'      => ['varbinary'],
88
    ];
89
90
    /**
91
     * Field is table identity.
92
     *
93
     * @var bool
94
     */
95
    protected $identity = false;
96
97
    /**
98
     * @var bool
99
     */
100
    protected $constrainedDefault = false;
101
102
    /**
103
     * Name of default constraint.
104
     *
105
     * @var string
106
     */
107
    protected $defaultConstraint = '';
108
109
    /**
110
     * @var bool
111
     */
112
    protected $constrainedEnum = false;
113
114
    /**
115
     * Name of enum constraint.
116
     *
117
     * @var string
118
     */
119
    protected $enumConstraint = '';
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function getConstraints(): array
125
    {
126
        $constraints = parent::getConstraints();
127
128
        if ($this->constrainedDefault) {
129
            $constraints[] = $this->defaultConstraint;
130
        }
131
132
        if ($this->constrainedEnum) {
133
            $constraints[] = $this->enumConstraint;
134
        }
135
136
        return $constraints;
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142
    public function abstractType(): string
143
    {
144
        if (!empty($this->enumValues)) {
145
            return 'enum';
146
        }
147
148
        return parent::abstractType();
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154 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...
155
    {
156
        $this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args());
157
        sort($this->enumValues);
158
159
        $this->type = 'varchar';
160
        foreach ($this->enumValues as $value) {
161
            $this->size = max((int)$this->size, strlen($value));
162
        }
163
164
        return $this;
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     *
170
     * @param bool $withEnum When true enum constrain will be included into definition. Set to false
171
     *                       if you want to create constrain separately.
172
     */
173
    public function sqlStatement(Driver $driver, bool $withEnum = true): string
174
    {
175
        if ($withEnum && $this->abstractType() == 'enum') {
176
            return "{$this->sqlStatement($driver, false)} {$this->enumStatement($driver)}";
177
        }
178
179
        $statement = [$driver->identifier($this->getName()), $this->type];
180
181
        if (!empty($this->precision)) {
182
            $statement[] = "({$this->precision}, {$this->scale})";
183
        } elseif (!empty($this->size)) {
184
            $statement[] = "({$this->size})";
185
        } elseif ($this->type == 'varchar' || $this->type == 'varbinary') {
186
            $statement[] = '(max)';
187
        }
188
189
        if ($this->identity) {
190
            $statement[] = 'IDENTITY(1,1)';
191
        }
192
193
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
194
195
        if ($this->hasDefaultValue()) {
196
            $statement[] = "DEFAULT {$this->prepareDefault($driver)}";
197
        }
198
199
        return implode(' ', $statement);
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    protected function prepareDefault(Driver $driver): string
206
    {
207
        $defaultValue = parent::prepareDefault($driver);
208
        if ($this->abstractType() == 'boolean') {
209
            $defaultValue = (int)$this->defaultValue;
210
        }
211
212
        return $defaultValue;
213
    }
214
215
    /**
216
     * Generate set of operations need to change column. We are expecting that column constrains
217
     * will be dropped separately.
218
     *
219
     * @param Driver         $driver
220
     * @param AbstractColumn $initial
221
     *
222
     * @return array
223
     */
224
    public function alterOperations(Driver $driver, AbstractColumn $initial): array
225
    {
226
        $operations = [];
227
228
        $currentType = [
229
            $this->type,
230
            $this->size,
231
            $this->precision,
232
            $this->scale,
233
            $this->nullable,
234
        ];
235
236
        $initType = [
237
            $initial->type,
238
            $initial->size,
239
            $initial->precision,
240
            $initial->scale,
241
            $initial->nullable,
242
        ];
243
244
        if ($currentType != $initType) {
245
            if ($this->abstractType() == 'enum') {
246
                //Getting longest value
247
                $enumSize = $this->size;
248
                foreach ($this->enumValues as $value) {
249
                    $enumSize = max($enumSize, strlen($value));
250
                }
251
252
                $type = "ALTER COLUMN {$driver->identifier($this->getName())} varchar($enumSize)";
253
                $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL');
254
            } else {
255
                $type = "ALTER COLUMN {$driver->identifier($this->getName())} {$this->type}";
256
257
                if (!empty($this->size)) {
258
                    $type .= "($this->size)";
259
                } elseif ($this->type == 'varchar' || $this->type == 'varbinary') {
260
                    $type .= '(max)';
261
                } elseif (!empty($this->precision)) {
262
                    $type .= "($this->precision, $this->scale)";
263
                }
264
265
                $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL');
266
            }
267
        }
268
269
        //Constraint should be already removed it this moment (see doColumnChange in TableSchema)
270
        if ($this->hasDefaultValue()) {
271
            $operations[] = "ADD CONSTRAINT {$this->defaultConstrain()} "
272
                . "DEFAULT {$this->prepareDefault($driver)} "
273
                . "FOR {$driver->identifier($this->getName())}";
274
        }
275
276
        //Constraint should be already removed it this moment (see alterColumn in SQLServerHandler)
277
        if ($this->abstractType() == 'enum') {
278
            $operations[] = "ADD {$this->enumStatement($driver)}";
279
        }
280
281
        return $operations;
282
    }
283
284
    /**
285
     * Get/generate name of enum constraint.
286
     *
287
     * @return string
288
     */
289 View Code Duplication
    protected function enumConstraint()
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...
290
    {
291
        if (empty($this->enumConstraint)) {
292
            $this->enumConstraint = $this->table . '_' . $this->getName() . '_enum_' . uniqid();
293
        }
294
295
        return $this->enumConstraint;
296
    }
297
298
    /**
299
     * Get/generate name of default constrain.
300
     *
301
     * @return string
302
     */
303 View Code Duplication
    protected function defaultConstrain(): 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...
304
    {
305
        if (empty($this->defaultConstraint)) {
306
            $this->defaultConstraint = $this->table . '_' . $this->getName() . '_default_' . uniqid();
307
        }
308
309
        return $this->defaultConstraint;
310
    }
311
312
    /**
313
     * In SQLServer we can emulate enums similar way as in Postgres via column constrain.
314
     *
315
     * @param Driver $driver
316
     *
317
     * @return string
318
     */
319
    private function enumStatement(Driver $driver): string
320
    {
321
        $enumValues = [];
322
        foreach ($this->enumValues as $value) {
323
            $enumValues[] = $driver->quote($value);
324
        }
325
326
        $constrain = $driver->identifier($this->enumConstraint());
327
        $column = $driver->identifier($this->getName());
328
        $enumValues = implode(', ', $enumValues);
329
330
        return "CONSTRAINT {$constrain} CHECK ({$column} IN ({$enumValues}))";
331
    }
332
333
    /**
334
     * Normalize default value.
335
     */
336
    private function normalizeDefault()
337
    {
338
        if (!$this->hasDefaultValue()) {
339
            return;
340
        }
341
342 View Code Duplication
        if ($this->defaultValue[0] == '(' && $this->defaultValue[strlen($this->defaultValue) - 1] == ')') {
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...
343
            //Cut braces
344
            $this->defaultValue = substr($this->defaultValue, 1, -1);
345
        }
346
347
        if (preg_match('/^[\'""].*?[\'"]$/', $this->defaultValue)) {
348
            $this->defaultValue = substr($this->defaultValue, 1, -1);
349
        }
350
351 View Code Duplication
        if (
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...
352
            $this->phpType() != 'string'
353
            && ($this->defaultValue[0] == '(' && $this->defaultValue[strlen($this->defaultValue) - 1] == ')')
354
        ) {
355
            //Cut another braces
356
            $this->defaultValue = substr($this->defaultValue, 1, -1);
357
        }
358
    }
359
360
    /**
361
     * @param string $table  Table name.
362
     * @param array  $schema
363
     * @param Driver $driver SQLServer columns are bit more complex.
364
     *
365
     * @return SQLServerColumn
366
     */
367
    public static function createInstance(string $table, array $schema, Driver $driver): self
368
    {
369
        $column = new self($table, $schema['COLUMN_NAME']);
370
371
        $column->type = $schema['DATA_TYPE'];
372
        $column->nullable = strtoupper($schema['IS_NULLABLE']) == 'YES';
373
        $column->defaultValue = $schema['COLUMN_DEFAULT'];
374
375
        $column->identity = (bool)$schema['is_identity'];
376
377
        $column->size = (int)$schema['CHARACTER_MAXIMUM_LENGTH'];
378
        if ($column->size == -1) {
379
            $column->size = 0;
380
        }
381
382 View Code Duplication
        if ($column->type == 'decimal') {
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...
383
            $column->precision = (int)$schema['NUMERIC_PRECISION'];
384
            $column->scale = (int)$schema['NUMERIC_SCALE'];
385
        }
386
387
        //Normalizing default value
388
        $column->normalizeDefault();
389
390
        /*
391
        * We have to fetch all column constrains cos default and enum check will be included into
392
        * them, plus column drop is not possible without removing all constraints.
393
        */
394
395
        if (!empty($schema['default_object_id'])) {
396
            //Looking for default constrain id
397
            $column->defaultConstraint = $driver->query(
398
                'SELECT [name] FROM [sys].[default_constraints] WHERE [object_id] = ?', [
399
                $schema['default_object_id'],
400
            ])->fetchColumn();
401
402
            if (!empty($column->defaultConstraint)) {
403
                $column->constrainedDefault = true;
404
            }
405
        }
406
407
        //Potential enum
408
        if ($column->type == 'varchar' && !empty($column->size)) {
409
            self::resolveEnum($driver, $schema, $column);
410
        }
411
412
        return $column;
413
    }
414
415
    /**
416
     * Resolve enum values if any.
417
     *
418
     * @param Driver          $driver
419
     * @param array           $schema
420
     * @param SQLServerColumn $column
421
     */
422
    private static function resolveEnum(Driver $driver, array $schema, SQLServerColumn $column)
423
    {
424
        $query = 'SELECT object_definition([o].[object_id]) AS [definition], '
425
            . "OBJECT_NAME([o].[object_id]) AS [name]\nFROM [sys].[objects] AS [o]\n"
426
            . "JOIN [sys].[sysconstraints] AS [c] ON [o].[object_id] = [c].[constid]\n"
427
            . "WHERE [type_desc] = 'CHECK_CONSTRAINT' AND [parent_object_id] = ? AND [c].[colid] = ?";
428
429
        $constraints = $driver->query($query, [$schema['object_id'], $schema['column_id']]);
430
431
        foreach ($constraints as $constraint) {
432
            $column->enumConstraint = $constraint['name'];
433
            $column->constrainedEnum = true;
434
435
            $name = preg_quote($driver->identifier($column->getName()));
436
437
            //We made some assumptions here...
438
            if (preg_match_all(
439
                '/' . $name . '=[\']?([^\']+)[\']?/i',
440
                $constraint['definition'],
441
                $matches
442
            )) {
443
                //Fetching enum values
444
                $column->enumValues = $matches[1];
445
                sort($column->enumValues);
446
            }
447
        }
448
    }
449
}