SQLServerColumn::defaultConstrain()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
ccs 3
cts 3
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver\SQLServer\Schema;
13
14
use Cycle\Database\Driver\DriverInterface;
15
use Cycle\Database\Exception\SchemaException;
16
use Cycle\Database\Schema\AbstractColumn;
0 ignored issues
show
Bug introduced by
The type Cycle\Database\Schema\AbstractColumn was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Cycle\Database\Schema\Attribute\ColumnAttribute;
18
19
class SQLServerColumn extends AbstractColumn
20
{
21
    /**
22
     * Default datetime value.
23
     */
24
    public const DATETIME_DEFAULT = '1970-01-01T00:00:00';
25
26
    /**
27
     * Default timestamp expression (driver specific).
28
     */
29
    public const DATETIME_NOW = 'getdate()';
30
31
    public const DATETIME_PRECISION = 7;
32
33
    /**
34
     * Private state related values.
35
     */
36
    public const EXCLUDE_FROM_COMPARE = [
37
        'userType',
38
        'timezone',
39
        'constrainedDefault',
40
        'defaultConstraint',
41
        'constrainedEnum',
42
        'enumConstraint',
43
        'attributes',
44
    ];
45
46
    protected array $aliases = [
47
        'int'       => 'integer',
48
        'smallint'  => 'smallInteger',
49
        'bigint'    => 'bigInteger',
50
        'bool'      => 'boolean',
51
        'varbinary' => 'binary',
52
    ];
53
    protected array $mapping = [
54
        //Primary sequences
55
        'primary'     => ['type' => 'int', 'identity' => true, 'nullable' => false],
56
        'bigPrimary'  => ['type' => 'bigint', 'identity' => true, 'nullable' => false],
57
58
        //Enum type (mapped via method)
59
        'enum'        => 'enum',
60
61
        //Logical types
62
        'boolean'     => 'bit',
63
64
        //Integer types (size can always be changed with size method), longInteger has method alias
65
        //bigInteger
66
        'integer'     => 'int',
67
        'tinyInteger' => 'tinyint',
68
        'smallInteger' => 'smallint',
69
        'bigInteger'  => 'bigint',
70
71
        //String with specified length (mapped via method)
72
        'string'      => ['type' => 'varchar', 'size' => 255],
73
74
        //Generic types
75
        'text'        => ['type' => 'varchar', 'size' => 0],
76
        'tinyText'    => ['type' => 'varchar', 'size' => 0],
77
        'mediumText'  => ['type' => 'varchar', 'size' => 0],
78
        'longText'    => ['type' => 'varchar', 'size' => 0],
79
80
        //Real types
81
        'double'      => 'float',
82
        'float'       => 'real',
83
84
        //Decimal type (mapped via method)
85
        'decimal'     => 'decimal',
86
87
        //Date and Time types
88
        'datetime'    => 'datetime',
89
        'datetime2'   => 'datetime2',
90
        'date'        => 'date',
91
        'time'        => 'time',
92
        'timestamp'   => 'datetime',
93
94
        //Binary types
95
        'binary'      => ['type' => 'varbinary', 'size' => 0],
96
        'tinyBinary'  => ['type' => 'varbinary', 'size' => 0],
97
        'longBinary'  => ['type' => 'varbinary', 'size' => 0],
98
99
        //Additional types
100
        'json'        => ['type' => 'varchar', 'size' => 0],
101
        'uuid'        => ['type' => 'varchar', 'size' => 36],
102
    ];
103
    protected array $reverseMapping = [
104
        'primary'     => [['type' => 'int', 'identity' => true]],
105
        'bigPrimary'  => [['type' => 'bigint', 'identity' => true]],
106
        'enum'        => ['enum'],
107
        'boolean'     => ['bit'],
108
        'integer'     => ['int'],
109
        'tinyInteger' => ['tinyint'],
110
        'smallInteger' => ['smallint'],
111
        'bigInteger'  => ['bigint'],
112
        'text'        => [['type' => 'varchar', 'size' => 0]],
113
        'string'      => ['varchar', 'char'],
114
        'double'      => ['float'],
115
        'float'       => ['real'],
116
        'decimal'     => ['decimal'],
117
        'timestamp'   => ['datetime'],
118
        'date'        => ['date'],
119
        'time'        => ['time'],
120
        'binary'      => ['varbinary'],
121
    ];
122
123
    #[ColumnAttribute(['varchar', 'datetime2', 'varbinary'])]
124
    protected int $size = 0;
125
126
    /**
127 44
     * Field is table identity.
128
     */
129 44
    protected bool $identity = false;
130
131 44
    protected bool $constrainedDefault = false;
132 16
133
    /**
134
     * Name of default constraint.
135 44
     */
136 6
    protected string $defaultConstraint = '';
137
138
    protected bool $constrainedEnum = false;
139 44
140
    /**
141
     * Name of enum constraint.
142 480
     */
143
    protected string $enumConstraint = '';
144 480
145
    /**
146
     * @psalm-param non-empty-string $table Table name.
147 152
     *
148
     * @param DriverInterface $driver SQLServer columns are bit more complex.
149 152
     */
150 152
    public static function createInstance(
151
        string $table,
152 152
        array $schema,
153 152
        DriverInterface $driver,
154 152
    ): self {
155
        $column = new self($table, $schema['COLUMN_NAME'], $driver->getTimezone());
156
157 152
        $column->type = $schema['DATA_TYPE'];
0 ignored issues
show
Bug Best Practice introduced by
The property type does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
158
        $column->nullable = \strtoupper($schema['IS_NULLABLE']) === 'YES';
0 ignored issues
show
Bug Best Practice introduced by
The property nullable does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
159
        $column->defaultValue = $schema['COLUMN_DEFAULT'];
0 ignored issues
show
Bug Best Practice introduced by
The property defaultValue does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
160
161
        $column->identity = (bool) $schema['is_identity'];
162
163
        $column->size = (int) $schema['CHARACTER_MAXIMUM_LENGTH'];
164
        if ($column->size === -1) {
165
            $column->size = 0;
166 480
        }
167
168 480
        if ($column->type === 'decimal') {
169 152
            $column->precision = (int) $schema['NUMERIC_PRECISION'];
0 ignored issues
show
Bug Best Practice introduced by
The property precision does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
170
            $column->scale = (int) $schema['NUMERIC_SCALE'];
0 ignored issues
show
Bug Best Practice introduced by
The property scale does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
171
        }
172 480
173
        if ($column->type === 'datetime2') {
174 480
            $column->size = (int) $schema['DATETIME_PRECISION'];
175 14
        }
176 470
177 266
        //Normalizing default value
178 446
        $column->normalizeDefault();
179 224
180
        /*
181
        * We have to fetch all column constrains cos default and enum check will be included into
182 480
        * them, plus column drop is not possible without removing all constraints.
183 334
        */
184
185
        if (!empty($schema['default_object_id'])) {
186 480
            //Looking for default constrain id
187
            $column->defaultConstraint = $driver->query(
188 480
                'SELECT [name] FROM [sys].[default_constraints] WHERE [object_id] = ?',
189 212
                [
190
                    $schema['default_object_id'],
191
                ],
192 480
            )->fetchColumn();
193
194
            if (!empty($column->defaultConstraint)) {
195
                $column->constrainedDefault = true;
196
            }
197
        }
198
199
        //Potential enum
200
        if ($column->type === 'varchar' && !empty($column->size)) {
201 38
            self::resolveEnum($driver, $schema, $column);
202
        }
203 38
204
        return $column;
205 38
    }
206 38
207 38
    public function getConstraints(): array
208 38
    {
209 38
        $constraints = parent::getConstraints();
210 38
211
        if ($this->constrainedDefault) {
212
            $constraints[] = $this->defaultConstraint;
213 38
        }
214 38
215 38
        if ($this->constrainedEnum) {
216 38
            $constraints[] = $this->enumConstraint;
217 38
        }
218 38
219
        return $constraints;
220
    }
221 38
222 20
    public function getAbstractType(): string
223
    {
224
        return !empty($this->enumValues) ? 'enum' : parent::getAbstractType();
225
    }
226
227
    public function enum(mixed $values): AbstractColumn
228
    {
229
        $this->enumValues = \array_map('strval', \is_array($values) ? $values : \func_get_args());
0 ignored issues
show
Bug Best Practice introduced by
The property enumValues does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
230
        \sort($this->enumValues);
231
232 20
        $this->type = 'varchar';
0 ignored issues
show
Bug Best Practice introduced by
The property type does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
233
        foreach ($this->enumValues as $value) {
234 20
            $this->size = \max((int) $this->size, \strlen($value));
235 10
        }
236 12
237 2
        return $this;
238 10
    }
239
240
    public function datetime(int $size = 0, mixed ...$attributes): self
241
    {
242 20
        $size === 0 ? $this->type('datetime') : $this->type('datetime2');
243
        $this->fillAttributes($attributes);
244
245
        ($size < 0 || $size > static::DATETIME_PRECISION) && throw new SchemaException(
246
            \sprintf('Invalid %s precision value.', $this->getAbstractType()),
247 38
        );
248 14
        $this->size = $size;
249 14
250 14
        return $this;
251
    }
252
253
    /**
254 38
     * @param bool $withEnum When true enum constrain will be included into definition. Set to false
255 4
     *                       if you want to create constrain separately.
256
     *
257
     * @psalm-return non-empty-string
258 38
     */
259
    public function sqlStatement(DriverInterface $driver, bool $withEnum = true): string
260
    {
261
        if ($withEnum && $this->getAbstractType() === 'enum') {
262
            return "{$this->sqlStatement($driver, false)} {$this->enumStatement($driver)}";
263
        }
264
265
        $statement = [$driver->identifier($this->getName()), $this->type];
0 ignored issues
show
Bug introduced by
The method identifier() does not exist on Cycle\Database\Driver\DriverInterface. It seems like you code against a sub-type of Cycle\Database\Driver\DriverInterface such as Cycle\Database\Driver\Driver. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

265
        $statement = [$driver->/** @scrutinizer ignore-call */ identifier($this->getName()), $this->type];
Loading history...
266 480
267
        if (!empty($this->precision)) {
268
            $statement[] = "({$this->precision}, {$this->scale})";
269
        } elseif (!empty($this->size)) {
270
            $statement[] = "({$this->size})";
271 480
        } elseif ($this->type === 'varchar' || $this->type === 'varbinary') {
272
            $statement[] = '(max)';
273 480
        }
274 480
275 480
        if ($this->identity) {
276
            $statement[] = 'IDENTITY(1,1)';
277 480
        }
278
279 480
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
280 480
281 224
        if ($this->hasDefaultValue()) {
282
            $statement[] = "DEFAULT {$this->quoteDefault($driver)}";
283
        }
284 480
285 14
        return \implode(' ', $statement);
286 14
    }
287
288
    /**
289
     * Generate set of operations need to change column. We are expecting that column constrains
290 480
     * will be dropped separately.
291
     *
292
     * @return string[]
293
     */
294
    public function alterOperations(DriverInterface $driver, AbstractColumn $initial): array
295
    {
296
        $operations = [];
297 480
298
        $currentType = [
299 212
            $this->type,
300 212
            $this->size,
301
            $this->precision,
302 212
            $this->scale,
303
            $this->nullable,
304 212
        ];
305
306 212
        $initType = [
307 212
            $initial->type,
308
            $initial->size,
309
            $initial->precision,
310
            $initial->scale,
311
            $initial->nullable,
312 480
        ];
313 266
314
        if ($currentType !== $initType) {
315
            if ($this->getAbstractType() === 'enum') {
316 480
                //Getting longest value
317
                $enumSize = $this->size;
318
                foreach ($this->enumValues as $value) {
319
                    $enumSize = \max($enumSize, \strlen($value));
320
                }
321
322 212
                $type = "ALTER COLUMN {$driver->identifier($this->getName())} varchar($enumSize)";
323
                $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL');
324 212
            } else {
325 212
                $type = "ALTER COLUMN {$driver->identifier($this->getName())} {$this->type}";
326 136
327
                if (!empty($this->size)) {
328
                    $type .= "($this->size)";
329 212
                } elseif ($this->type === 'varchar' || $this->type === 'varbinary') {
330
                    $type .= '(max)';
331
                } elseif (!empty($this->precision)) {
332
                    $type .= "($this->precision, $this->scale)";
333
                }
334
335 152
                $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL');
336
            }
337 152
        }
338 152
339
        //Constraint should be already removed it this moment (see doColumnChange in TableSchema)
340
        if ($this->hasDefaultValue()) {
341 152
            $operations[] = "ADD CONSTRAINT {$this->defaultConstrain()} "
342
                . "DEFAULT {$this->quoteDefault($driver)} "
343
                . "FOR {$driver->identifier($this->getName())}";
344
        }
345
346
        //Constraint should be already removed it this moment (see alterColumn in SQLServerHandler)
347 14
        if ($this->getAbstractType() === 'enum') {
348
            $operations[] = "ADD {$this->enumStatement($driver)}";
349 14
        }
350
351
        return $operations;
352
    }
353 14
354
    protected static function isJson(AbstractColumn $column): ?bool
355
    {
356
        // In SQL Server, we cannot determine if a column has a JSON type.
357
        return $column->getAbstractType() === 'text' ? null : false;
358
    }
359
360
    /**
361 152
     * @psalm-return non-empty-string
362
     */
363 152
    protected function quoteDefault(DriverInterface $driver): string
364 152
    {
365 152
        $defaultValue = parent::quoteDefault($driver);
366
        if ($this->getAbstractType() === 'boolean') {
367
            $defaultValue = (string) ((int) $this->defaultValue);
368 152
        }
369 152
370 152
        return $defaultValue;
371
    }
372 152
373
    /**
374
     * Get/generate name of enum constraint.
375
     */
376
    protected function enumConstraint(): string
377
    {
378 480
        if (empty($this->enumConstraint)) {
379
            $this->enumConstraint = $this->table . '_' . $this->getName() . '_enum_' . \uniqid();
380 480
        }
381 412
382
        return $this->enumConstraint;
383
    }
384 212
385
    /**
386 212
     * Get/generate name of default constrain.
387
     */
388
    protected function defaultConstrain(): string
389 212
    {
390 178
        if (empty($this->defaultConstraint)) {
391
            $this->defaultConstraint = $this->table . '_' . $this->getName() . '_default_' . \uniqid();
392
        }
393
394 212
        return $this->defaultConstraint;
395
    }
396 154
397 212
    /**
398
     * Resolve enum values if any.
399
     */
400
    private static function resolveEnum(
401 154
        DriverInterface $driver,
402
        array $schema,
403 212
        self $column,
404
    ): void {
405
        $query = 'SELECT object_definition([o].[object_id]) AS [definition], '
406
            . "OBJECT_NAME([o].[object_id]) AS [name]\nFROM [sys].[objects] AS [o]\n"
407
            . "JOIN [sys].[sysconstraints] AS [c] ON [o].[object_id] = [c].[constid]\n"
408 266
            . "WHERE [type_desc] = 'CHECK_CONSTRAINT' AND [parent_object_id] = ? AND [c].[colid] = ?";
409
410
        $constraints = $driver->query($query, [$schema['object_id'], $schema['column_id']]);
411
412
        foreach ($constraints as $constraint) {
413 266
            $column->enumConstraint = $constraint['name'];
414
            $column->constrainedEnum = true;
415
416
            $name = \preg_quote($driver->identifier($column->getName()));
417
418 266
            // we made some assumptions here...
419
            if (
420 266
                \preg_match_all(
421 152
                    '/' . $name . '=[\']?([^\']+)[\']?/i',
422 152
                    $constraint['definition'],
423
                    $matches,
424 152
                )
425
            ) {
426
                //Fetching enum values
427
                $column->enumValues = $matches[1];
0 ignored issues
show
Bug Best Practice introduced by
The property enumValues does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
428 152
                \sort($column->enumValues);
429 152
            }
430 152
        }
431
    }
432
433
    /**
434
     * In SQLServer we can emulate enums similar way as in Postgres via column constrain.
435 152
     *
436 152
     * @psalm-return non-empty-string
437
     */
438
    private function enumStatement(DriverInterface $driver): string
439 266
    {
440
        $enumValues = [];
441
        foreach ($this->enumValues as $value) {
442
            $enumValues[] = $driver->quote($value);
443
        }
444
445
        $constrain = $driver->identifier($this->enumConstraint());
446
        $column = $driver->identifier($this->getName());
447
        $enumValues = \implode(', ', $enumValues);
448
449
        return "CONSTRAINT {$constrain} CHECK ({$column} IN ({$enumValues}))";
450
    }
451
452
    /**
453
     * Normalize default value.
454
     */
455
    private function normalizeDefault(): void
456
    {
457
        if (!$this->hasDefaultValue()) {
458
            return;
459
        }
460
461
        if ($this->defaultValue[0] === '(' && $this->defaultValue[\strlen($this->defaultValue) - 1] === ')') {
462
            //Cut braces
463
            $this->defaultValue = \substr($this->defaultValue, 1, -1);
0 ignored issues
show
Bug Best Practice introduced by
The property defaultValue does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
464
        }
465
466
        if (\preg_match('/^[\'"].*?[\'"]$/', $this->defaultValue)) {
467
            $this->defaultValue = \substr($this->defaultValue, 1, -1);
468
        }
469
470
        if (
471
            $this->getType() !== 'string'
472
            && (
473
                $this->defaultValue[0] === '('
474
                && $this->defaultValue[\strlen($this->defaultValue) - 1] === ')'
475
            )
476
        ) {
477
            //Cut another braces
478
            $this->defaultValue = \substr($this->defaultValue, 1, -1);
479
        }
480
    }
481
}
482