Passed
Pull Request — 2.x (#86)
by Maxim
20:35 queued 03:53
created

SQLServerColumn::datetime()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

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