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

ColumnSchema::defaultConstrain()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 10
Ratio 100 %

Importance

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