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

SQLServerColumn::quoteDefault()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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