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
        'snowflake'   => 'bigint',
102
        'ulid'        => ['type' => 'varchar', 'size' => 26],
103
        'uuid'        => ['type' => 'varchar', 'size' => 36],
104
    ];
105
    protected array $reverseMapping = [
106
        'primary'     => [['type' => 'int', 'identity' => true]],
107
        'bigPrimary'  => [['type' => 'bigint', 'identity' => true]],
108
        'enum'        => ['enum'],
109
        'boolean'     => ['bit'],
110
        'integer'     => ['int'],
111
        'tinyInteger' => ['tinyint'],
112
        'smallInteger' => ['smallint'],
113
        'bigInteger'  => ['bigint'],
114
        'text'        => [['type' => 'varchar', 'size' => 0]],
115
        'string'      => ['varchar', 'char'],
116
        'double'      => ['float'],
117
        'float'       => ['real'],
118
        'decimal'     => ['decimal'],
119
        'timestamp'   => ['datetime'],
120
        'date'        => ['date'],
121
        'time'        => ['time'],
122
        'binary'      => ['varbinary'],
123
    ];
124
125
    #[ColumnAttribute(['varchar', 'datetime2', 'varbinary'])]
126
    protected int $size = 0;
127 44
128
    /**
129 44
     * Field is table identity.
130
     */
131 44
    protected bool $identity = false;
132 16
133
    protected bool $constrainedDefault = false;
134
135 44
    /**
136 6
     * Name of default constraint.
137
     */
138
    protected string $defaultConstraint = '';
139 44
140
    protected bool $constrainedEnum = false;
141
142 480
    /**
143
     * Name of enum constraint.
144 480
     */
145
    protected string $enumConstraint = '';
146
147 152
    /**
148
     * @psalm-param non-empty-string $table Table name.
149 152
     *
150 152
     * @param DriverInterface $driver SQLServer columns are bit more complex.
151
     */
152 152
    public static function createInstance(
153 152
        string $table,
154 152
        array $schema,
155
        DriverInterface $driver,
156
    ): self {
157 152
        $column = new self($table, $schema['COLUMN_NAME'], $driver->getTimezone());
158
159
        $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...
160
        $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...
161
        $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...
162
163
        $column->identity = (bool) $schema['is_identity'];
164
165
        $column->size = (int) $schema['CHARACTER_MAXIMUM_LENGTH'];
166 480
        if ($column->size === -1) {
167
            $column->size = 0;
168 480
        }
169 152
170
        if ($column->type === 'decimal') {
171
            $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...
172 480
            $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...
173
        }
174 480
175 14
        if ($column->type === 'datetime2') {
176 470
            $column->size = (int) $schema['DATETIME_PRECISION'];
177 266
        }
178 446
179 224
        //Normalizing default value
180
        $column->normalizeDefault();
181
182 480
        /*
183 334
        * We have to fetch all column constrains cos default and enum check will be included into
184
        * them, plus column drop is not possible without removing all constraints.
185
        */
186 480
187
        if (!empty($schema['default_object_id'])) {
188 480
            //Looking for default constrain id
189 212
            $column->defaultConstraint = $driver->query(
190
                'SELECT [name] FROM [sys].[default_constraints] WHERE [object_id] = ?',
191
                [
192 480
                    $schema['default_object_id'],
193
                ],
194
            )->fetchColumn();
195
196
            if (!empty($column->defaultConstraint)) {
197
                $column->constrainedDefault = true;
198
            }
199
        }
200
201 38
        //Potential enum
202
        if ($column->type === 'varchar' && !empty($column->size)) {
203 38
            self::resolveEnum($driver, $schema, $column);
204
        }
205 38
206 38
        return $column;
207 38
    }
208 38
209 38
    public function getConstraints(): array
210 38
    {
211
        $constraints = parent::getConstraints();
212
213 38
        if ($this->constrainedDefault) {
214 38
            $constraints[] = $this->defaultConstraint;
215 38
        }
216 38
217 38
        if ($this->constrainedEnum) {
218 38
            $constraints[] = $this->enumConstraint;
219
        }
220
221 38
        return $constraints;
222 20
    }
223
224
    public function getAbstractType(): string
225
    {
226
        return !empty($this->enumValues) ? 'enum' : parent::getAbstractType();
227
    }
228
229
    public function enum(mixed $values): AbstractColumn
230
    {
231
        $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...
232 20
        \sort($this->enumValues);
233
234 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...
235 10
        foreach ($this->enumValues as $value) {
236 12
            $this->size = \max((int) $this->size, \strlen($value));
237 2
        }
238 10
239
        return $this;
240
    }
241
242 20
    public function datetime(int $size = 0, mixed ...$attributes): self
243
    {
244
        $size === 0 ? $this->type('datetime') : $this->type('datetime2');
245
        $this->fillAttributes($attributes);
246
247 38
        ($size < 0 || $size > static::DATETIME_PRECISION) && throw new SchemaException(
248 14
            \sprintf('Invalid %s precision value.', $this->getAbstractType()),
249 14
        );
250 14
        $this->size = $size;
251
252
        return $this;
253
    }
254 38
255 4
    /**
256
     * @param bool $withEnum When true enum constrain will be included into definition. Set to false
257
     *                       if you want to create constrain separately.
258 38
     *
259
     * @psalm-return non-empty-string
260
     */
261
    public function sqlStatement(DriverInterface $driver, bool $withEnum = true): string
262
    {
263
        if ($withEnum && $this->getAbstractType() === 'enum') {
264
            return "{$this->sqlStatement($driver, false)} {$this->enumStatement($driver)}";
265
        }
266 480
267
        $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

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