Passed
Pull Request — 2.x (#91)
by Aleksei
18:20
created

SQLServerColumn::datetime()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 4
nc 6
nop 1
dl 0
loc 9
ccs 4
cts 4
cp 1
crap 4
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
18
class SQLServerColumn extends AbstractColumn
19
{
20
    /**
21
     * Default datetime value.
22
     */
23
    public const DATETIME_DEFAULT = '1970-01-01T00:00:00';
24
25
    /**
26
     * Default timestamp expression (driver specific).
27
     */
28
    public const DATETIME_NOW = 'getdate()';
29
30
    /**
31
     * Private state related values.
32
     */
33
    public const EXCLUDE_FROM_COMPARE = [
34
        'userType',
35
        'timezone',
36
        'constrainedDefault',
37
        'defaultConstraint',
38
        'constrainedEnum',
39
        'enumConstraint',
40
    ];
41
42
    protected array $mapping = [
43
        //Primary sequences
44
        'primary'     => ['type' => 'int', 'identity' => true, 'nullable' => false],
45
        'bigPrimary'  => ['type' => 'bigint', 'identity' => true, 'nullable' => false],
46
47
        //Enum type (mapped via method)
48
        'enum'        => 'enum',
49
50
        //Logical types
51
        'boolean'     => 'bit',
52
53
        //Integer types (size can always be changed with size method), longInteger has method alias
54
        //bigInteger
55
        'integer'     => 'int',
56
        'tinyInteger' => 'tinyint',
57
        'smallInteger'=> 'smallint',
58
        'bigInteger'  => 'bigint',
59
60
        //String with specified length (mapped via method)
61
        'string'      => 'varchar',
62
63
        //Generic types
64
        'text'        => ['type' => 'varchar', 'size' => 0],
65
        'tinyText'    => ['type' => 'varchar', 'size' => 0],
66
        'longText'    => ['type' => 'varchar', 'size' => 0],
67
68
        //Real types
69
        'double'      => 'float',
70
        'float'       => 'real',
71
72
        //Decimal type (mapped via method)
73
        'decimal'     => 'decimal',
74
75
        //Date and Time types
76
        'datetime'    => 'datetime',
77
        'datetime2'   => 'datetime2',
78
        'date'        => 'date',
79
        'time'        => 'time',
80
        'timestamp'   => 'datetime',
81
82
        //Binary types
83
        'binary'      => ['type' => 'varbinary', 'size' => 0],
84
        'tinyBinary'  => ['type' => 'varbinary', 'size' => 0],
85
        'longBinary'  => ['type' => 'varbinary', 'size' => 0],
86
87
        //Additional types
88
        'json'        => ['type' => 'varchar', 'size' => 0],
89
        'uuid'        => ['type' => 'varchar', 'size' => 36],
90
    ];
91
92
    protected array $reverseMapping = [
93
        'primary'     => [['type' => 'int', 'identity' => true]],
94
        'bigPrimary'  => [['type' => 'bigint', 'identity' => true]],
95
        'enum'        => ['enum'],
96
        'boolean'     => ['bit'],
97
        'integer'     => ['int'],
98
        'tinyInteger' => ['tinyint'],
99
        'smallInteger'=> ['smallint'],
100
        'bigInteger'  => ['bigint'],
101
        'text'        => [['type' => 'varchar', 'size' => 0]],
102
        'string'      => ['varchar', 'char'],
103
        'double'      => ['float'],
104
        'float'       => ['real'],
105
        'decimal'     => ['decimal'],
106
        'timestamp'   => ['datetime'],
107
        'date'        => ['date'],
108
        'time'        => ['time'],
109
        'binary'      => ['varbinary'],
110
    ];
111
112
    /**
113
     * Field is table identity.
114
     */
115
    protected bool $identity = false;
116
117
    protected bool $constrainedDefault = false;
118
119
    /**
120
     * Name of default constraint.
121
     */
122
    protected string $defaultConstraint = '';
123
124
    protected bool $constrainedEnum = false;
125
126
    /**
127 44
     * Name of enum constraint.
128
     */
129 44
    protected string $enumConstraint = '';
130
131 44
    public function getConstraints(): array
132 16
    {
133
        $constraints = parent::getConstraints();
134
135 44
        if ($this->constrainedDefault) {
136 6
            $constraints[] = $this->defaultConstraint;
137
        }
138
139 44
        if ($this->constrainedEnum) {
140
            $constraints[] = $this->enumConstraint;
141
        }
142 480
143
        return $constraints;
144 480
    }
145
146
    public function getAbstractType(): string
147 152
    {
148
        return !empty($this->enumValues) ? 'enum' : parent::getAbstractType();
149 152
    }
150 152
151
    public function enum(mixed $values): AbstractColumn
152 152
    {
153 152
        $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...
154 152
        sort($this->enumValues);
155
156
        $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...
157 152
        foreach ($this->enumValues as $value) {
158
            $this->size = max((int)$this->size, strlen($value));
0 ignored issues
show
Bug Best Practice introduced by
The property size does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
159
        }
160
161
        return $this;
162
    }
163
164
    public function datetime(int $size = 0): self
165
    {
166 480
        ($size < 0 || $size > 7) && throw new SchemaException('Invalid datetime length value');
167
168 480
        $size === 0 ? $this->type('datetime') : $this->type('datetime2');
169 152
170
        $this->size = $size;
0 ignored issues
show
Bug Best Practice introduced by
The property size does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
171
172 480
        return $this;
173
    }
174 480
175 14
    /**
176 470
     * @param bool $withEnum When true enum constrain will be included into definition. Set to false
177 266
     *                       if you want to create constrain separately.
178 446
     *
179 224
     * @psalm-return non-empty-string
180
     */
181
    public function sqlStatement(DriverInterface $driver, bool $withEnum = true): string
182 480
    {
183 334
        if ($withEnum && $this->getAbstractType() === 'enum') {
184
            return "{$this->sqlStatement($driver, false)} {$this->enumStatement($driver)}";
185
        }
186 480
187
        $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

187
        $statement = [$driver->/** @scrutinizer ignore-call */ identifier($this->getName()), $this->type];
Loading history...
188 480
189 212
        if (!empty($this->precision)) {
190
            $statement[] = "({$this->precision}, {$this->scale})";
191
        } elseif (!empty($this->size)) {
192 480
            $statement[] = "({$this->size})";
193
        } elseif ($this->type === 'varchar' || $this->type === 'varbinary') {
194
            $statement[] = '(max)';
195
        }
196
197
        if ($this->identity) {
198
            $statement[] = 'IDENTITY(1,1)';
199
        }
200
201 38
        $statement[] = $this->nullable ? 'NULL' : 'NOT NULL';
202
203 38
        if ($this->hasDefaultValue()) {
204
            $statement[] = "DEFAULT {$this->quoteDefault($driver)}";
205 38
        }
206 38
207 38
        return implode(' ', $statement);
208 38
    }
209 38
210 38
    /**
211
     * Generate set of operations need to change column. We are expecting that column constrains
212
     * will be dropped separately.
213 38
     *
214 38
     * @return string[]
215 38
     */
216 38
    public function alterOperations(DriverInterface $driver, AbstractColumn $initial): array
217 38
    {
218 38
        $operations = [];
219
220
        $currentType = [
221 38
            $this->type,
222 20
            $this->size,
223
            $this->precision,
224
            $this->scale,
225
            $this->nullable,
226
        ];
227
228
        $initType = [
229
            $initial->type,
230
            $initial->size,
231
            $initial->precision,
232 20
            $initial->scale,
233
            $initial->nullable,
234 20
        ];
235 10
236 12
        if ($currentType !== $initType) {
237 2
            if ($this->getAbstractType() === 'enum') {
238 10
                //Getting longest value
239
                $enumSize = $this->size;
240
                foreach ($this->enumValues as $value) {
241
                    $enumSize = max($enumSize, strlen($value));
242 20
                }
243
244
                $type = "ALTER COLUMN {$driver->identifier($this->getName())} varchar($enumSize)";
245
                $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL');
246
            } else {
247 38
                $type = "ALTER COLUMN {$driver->identifier($this->getName())} {$this->type}";
248 14
249 14
                if (!empty($this->size)) {
250 14
                    $type .= "($this->size)";
251
                } elseif ($this->type === 'varchar' || $this->type === 'varbinary') {
252
                    $type .= '(max)';
253
                } elseif (!empty($this->precision)) {
254 38
                    $type .= "($this->precision, $this->scale)";
255 4
                }
256
257
                $operations[] = $type . ' ' . ($this->nullable ? 'NULL' : 'NOT NULL');
258 38
            }
259
        }
260
261
        //Constraint should be already removed it this moment (see doColumnChange in TableSchema)
262
        if ($this->hasDefaultValue()) {
263
            $operations[] = "ADD CONSTRAINT {$this->defaultConstrain()} "
264
                . "DEFAULT {$this->quoteDefault($driver)} "
265
                . "FOR {$driver->identifier($this->getName())}";
266 480
        }
267
268
        //Constraint should be already removed it this moment (see alterColumn in SQLServerHandler)
269
        if ($this->getAbstractType() === 'enum') {
270
            $operations[] = "ADD {$this->enumStatement($driver)}";
271 480
        }
272
273 480
        return $operations;
274 480
    }
275 480
276
    /**
277 480
     * @psalm-param non-empty-string $table Table name.
278
     *
279 480
     * @param DriverInterface $driver SQLServer columns are bit more complex.
280 480
     */
281 224
    public static function createInstance(
282
        string $table,
283
        array $schema,
284 480
        DriverInterface $driver
285 14
    ): self {
286 14
        $column = new self($table, $schema['COLUMN_NAME'], $driver->getTimezone());
287
288
        $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...
289
        $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...
290 480
        $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...
291
292
        $column->identity = (bool)$schema['is_identity'];
293
294
        $column->size = (int)$schema['CHARACTER_MAXIMUM_LENGTH'];
0 ignored issues
show
Bug Best Practice introduced by
The property size does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
295
        if ($column->size === -1) {
296
            $column->size = 0;
297 480
        }
298
299 212
        if ($column->type === 'decimal') {
300 212
            $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...
301
            $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...
302 212
        }
303
304 212
        if ($column->type === 'datetime2') {
305
            $column->size = (int) $schema['DATETIME_PRECISION'];
306 212
        }
307 212
308
        //Normalizing default value
309
        $column->normalizeDefault();
310
311
        /*
312 480
        * We have to fetch all column constrains cos default and enum check will be included into
313 266
        * them, plus column drop is not possible without removing all constraints.
314
        */
315
316 480
        if (!empty($schema['default_object_id'])) {
317
            //Looking for default constrain id
318
            $column->defaultConstraint = $driver->query(
319
                'SELECT [name] FROM [sys].[default_constraints] WHERE [object_id] = ?',
320
                [
321
                    $schema['default_object_id'],
322 212
                ]
323
            )->fetchColumn();
324 212
325 212
            if (!empty($column->defaultConstraint)) {
326 136
                $column->constrainedDefault = true;
327
            }
328
        }
329 212
330
        //Potential enum
331
        if ($column->type === 'varchar' && !empty($column->size)) {
332
            self::resolveEnum($driver, $schema, $column);
333
        }
334
335 152
        return $column;
336
    }
337 152
338 152
    /**
339
     * @psalm-return non-empty-string
340
     */
341 152
    protected function quoteDefault(DriverInterface $driver): string
342
    {
343
        $defaultValue = parent::quoteDefault($driver);
344
        if ($this->getAbstractType() === 'boolean') {
345
            $defaultValue = (string) ((int)$this->defaultValue);
346
        }
347 14
348
        return $defaultValue;
349 14
    }
350
351
    /**
352
     * Get/generate name of enum constraint.
353 14
     */
354
    protected function enumConstraint(): string
355
    {
356
        if (empty($this->enumConstraint)) {
357
            $this->enumConstraint = $this->table . '_' . $this->getName() . '_enum_' . uniqid();
358
        }
359
360
        return $this->enumConstraint;
361 152
    }
362
363 152
    /**
364 152
     * Get/generate name of default constrain.
365 152
     */
366
    protected function defaultConstrain(): string
367
    {
368 152
        if (empty($this->defaultConstraint)) {
369 152
            $this->defaultConstraint = $this->table . '_' . $this->getName() . '_default_' . uniqid();
370 152
        }
371
372 152
        return $this->defaultConstraint;
373
    }
374
375
    /**
376
     * In SQLServer we can emulate enums similar way as in Postgres via column constrain.
377
     *
378 480
     * @psalm-return non-empty-string
379
     */
380 480
    private function enumStatement(DriverInterface $driver): string
381 412
    {
382
        $enumValues = [];
383
        foreach ($this->enumValues as $value) {
384 212
            $enumValues[] = $driver->quote($value);
385
        }
386 212
387
        $constrain = $driver->identifier($this->enumConstraint());
388
        $column = $driver->identifier($this->getName());
389 212
        $enumValues = implode(', ', $enumValues);
390 178
391
        return "CONSTRAINT {$constrain} CHECK ({$column} IN ({$enumValues}))";
392
    }
393
394 212
    /**
395
     * Normalize default value.
396 154
     */
397 212
    private function normalizeDefault(): void
398
    {
399
        if (!$this->hasDefaultValue()) {
400
            return;
401 154
        }
402
403 212
        if ($this->defaultValue[0] === '(' && $this->defaultValue[strlen($this->defaultValue) - 1] === ')') {
404
            //Cut braces
405
            $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...
406
        }
407
408 266
        if (preg_match('/^[\'"].*?[\'"]$/', $this->defaultValue)) {
409
            $this->defaultValue = substr($this->defaultValue, 1, -1);
410
        }
411
412
        if (
413 266
            $this->getType() !== 'string'
414
            && (
415
                $this->defaultValue[0] === '('
416
                && $this->defaultValue[strlen($this->defaultValue) - 1] === ')'
417
            )
418 266
        ) {
419
            //Cut another braces
420 266
            $this->defaultValue = substr($this->defaultValue, 1, -1);
421 152
        }
422 152
    }
423
424 152
    /**
425
     * Resolve enum values if any.
426
     */
427
    private static function resolveEnum(
428 152
        DriverInterface $driver,
429 152
        array $schema,
430 152
        self $column
431
    ): void {
432
        $query = 'SELECT object_definition([o].[object_id]) AS [definition], '
433
            . "OBJECT_NAME([o].[object_id]) AS [name]\nFROM [sys].[objects] AS [o]\n"
434
            . "JOIN [sys].[sysconstraints] AS [c] ON [o].[object_id] = [c].[constid]\n"
435 152
            . "WHERE [type_desc] = 'CHECK_CONSTRAINT' AND [parent_object_id] = ? AND [c].[colid] = ?";
436 152
437
        $constraints = $driver->query($query, [$schema['object_id'], $schema['column_id']]);
438
439 266
        foreach ($constraints as $constraint) {
440
            $column->enumConstraint = $constraint['name'];
441
            $column->constrainedEnum = true;
442
443
            $name = preg_quote($driver->identifier($column->getName()));
444
445
            // we made some assumptions here...
446
            if (
447
                preg_match_all(
448
                    '/' . $name . '=[\']?([^\']+)[\']?/i',
449
                    $constraint['definition'],
450
                    $matches
451
                )
452
            ) {
453
                //Fetching enum values
454
                $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...
455
                sort($column->enumValues);
456
            }
457
        }
458
    }
459
}
460