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) |
|
|
|
|
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') { |
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
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 ( |
|
|
|
|
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)) { |
|
|
|
|
395
|
|
|
$this->defaultValue = substr($this->defaultValue, 1, -1); |
396
|
|
|
} |
397
|
|
|
|
398
|
|
View Code Duplication |
if ( |
|
|
|
|
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
|
|
|
} |
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.