1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral Framework. |
4
|
|
|
* |
5
|
|
|
* @license MIT |
6
|
|
|
* @author Anton Titov (Wolfy-J) |
7
|
|
|
*/ |
8
|
|
|
namespace Spiral\Database\Drivers\Postgres\Schemas; |
9
|
|
|
|
10
|
|
|
use Spiral\Database\Entities\Schemas\AbstractColumn; |
11
|
|
|
use Spiral\Database\Injections\Fragment; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Postgres column schema. |
15
|
|
|
*/ |
16
|
|
|
class ColumnSchema extends AbstractColumn |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* {@inheritdoc} |
20
|
|
|
*/ |
21
|
|
|
protected $mapping = [ |
22
|
|
|
//Primary sequences |
23
|
|
|
'primary' => ['type' => 'serial', 'autoIncrement' => true, 'nullable' => false], |
24
|
|
|
'bigPrimary' => ['type' => 'bigserial', 'autoIncrement' => true, 'nullable' => false], |
25
|
|
|
|
26
|
|
|
//Enum type (mapped via method) |
27
|
|
|
'enum' => 'enum', |
28
|
|
|
|
29
|
|
|
//Logical types |
30
|
|
|
'boolean' => 'boolean', |
31
|
|
|
|
32
|
|
|
//Integer types (size can always be changed with size method), longInteger has method alias |
33
|
|
|
//bigInteger |
34
|
|
|
'integer' => 'integer', |
35
|
|
|
'tinyInteger' => 'smallint', |
36
|
|
|
'bigInteger' => 'bigint', |
37
|
|
|
|
38
|
|
|
//String with specified length (mapped via method) |
39
|
|
|
'string' => 'character varying', |
40
|
|
|
|
41
|
|
|
//Generic types |
42
|
|
|
'text' => 'text', |
43
|
|
|
'tinyText' => 'text', |
44
|
|
|
'longText' => 'text', |
45
|
|
|
|
46
|
|
|
//Real types |
47
|
|
|
'double' => 'double precision', |
48
|
|
|
'float' => 'real', |
49
|
|
|
|
50
|
|
|
//Decimal type (mapped via method) |
51
|
|
|
'decimal' => 'numeric', |
52
|
|
|
|
53
|
|
|
//Date and Time types |
54
|
|
|
'datetime' => 'timestamp without time zone', |
55
|
|
|
'date' => 'date', |
56
|
|
|
'time' => 'time', |
57
|
|
|
'timestamp' => 'timestamp without time zone', |
58
|
|
|
|
59
|
|
|
//Binary types |
60
|
|
|
'binary' => 'bytea', |
61
|
|
|
'tinyBinary' => 'bytea', |
62
|
|
|
'longBinary' => 'bytea', |
63
|
|
|
|
64
|
|
|
//Additional types |
65
|
|
|
'json' => 'json' |
66
|
|
|
]; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* {@inheritdoc} |
70
|
|
|
*/ |
71
|
|
|
protected $reverseMapping = [ |
72
|
|
|
'primary' => ['serial'], |
73
|
|
|
'bigPrimary' => ['bigserial'], |
74
|
|
|
'enum' => ['enum'], |
75
|
|
|
'boolean' => ['boolean'], |
76
|
|
|
'integer' => ['int', 'integer', 'int4'], |
77
|
|
|
'tinyInteger' => ['smallint'], |
78
|
|
|
'bigInteger' => ['bigint', 'int8'], |
79
|
|
|
'string' => ['character varying', 'character'], |
80
|
|
|
'text' => ['text'], |
81
|
|
|
'double' => ['double precision'], |
82
|
|
|
'float' => ['real', 'money'], |
83
|
|
|
'decimal' => ['numeric'], |
84
|
|
|
'date' => ['date'], |
85
|
|
|
'time' => ['time', 'time with time zone', 'time without time zone'], |
86
|
|
|
'timestamp' => ['timestamp', 'timestamp with time zone', 'timestamp without time zone'], |
87
|
|
|
'binary' => ['bytea'], |
88
|
|
|
'json' => ['json'] |
89
|
|
|
]; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Field is auto incremental. |
93
|
|
|
* |
94
|
|
|
* @var bool |
95
|
|
|
*/ |
96
|
|
|
protected $autoIncrement = false; |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Name of enum constraint associated with field. |
100
|
|
|
* |
101
|
|
|
* @var string |
102
|
|
|
*/ |
103
|
|
|
protected $enumConstraint = ''; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* {@inheritdoc} |
107
|
|
|
*/ |
108
|
|
|
public function getConstraints() |
109
|
|
|
{ |
110
|
|
|
$constraints = parent::getConstraints(); |
111
|
|
|
|
112
|
|
|
if (!empty($this->enumConstraint)) { |
113
|
|
|
$constraints[] = $this->enumConstraint; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
return $constraints; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* {@inheritdoc} |
121
|
|
|
*/ |
122
|
|
|
public function abstractType() |
123
|
|
|
{ |
124
|
|
|
if (!empty($this->enumValues)) { |
125
|
|
|
return 'enum'; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
return parent::abstractType(); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* {@inheritdoc} |
133
|
|
|
*/ |
134
|
|
|
public function primary() |
135
|
|
|
{ |
136
|
|
|
$this->autoIncrement = true; |
137
|
|
|
|
138
|
|
|
//Changing type of already created primary key (we can't use "serial" alias here) |
139
|
|
|
if (!empty($this->type) && $this->type != 'serial') { |
140
|
|
|
$this->type = 'integer'; |
141
|
|
|
|
142
|
|
|
return $this; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
return parent::primary(); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* {@inheritdoc} |
150
|
|
|
*/ |
151
|
|
|
public function bigPrimary() |
152
|
|
|
{ |
153
|
|
|
$this->autoIncrement = true; |
154
|
|
|
|
155
|
|
|
//Changing type of already created primary key (we can't use "serial" alias here) |
156
|
|
|
if (!empty($this->type) && $this->type != 'bigserial') { |
157
|
|
|
$this->type = 'bigint'; |
158
|
|
|
|
159
|
|
|
return $this; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
return parent::bigPrimary(); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* {@inheritdoc} |
167
|
|
|
*/ |
168
|
|
View Code Duplication |
public function enum($values) |
|
|
|
|
169
|
|
|
{ |
170
|
|
|
$this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args()); |
171
|
|
|
|
172
|
|
|
$this->type = 'character'; |
173
|
|
|
foreach ($this->enumValues as $value) { |
174
|
|
|
$this->size = max((int)$this->size, strlen($value)); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
return $this; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* {@inheritdoc} |
182
|
|
|
*/ |
183
|
|
|
public function sqlStatement() |
184
|
|
|
{ |
185
|
|
|
$statement = parent::sqlStatement(); |
186
|
|
|
|
187
|
|
|
if ($this->abstractType() != 'enum') { |
188
|
|
|
return $statement; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
//We have add constraint for enum type |
192
|
|
|
$enumValues = []; |
193
|
|
|
foreach ($this->enumValues as $value) { |
194
|
|
|
$enumValues[] = $this->table->driver()->getPDO()->quote($value); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
return "$statement CONSTRAINT {$this->enumConstraint(true, true)} " |
198
|
|
|
. "CHECK ({$this->getName(true)} IN (" . join(', ', $enumValues) . "))"; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Generate set of altering operations should be applied to column to change it's type, size, |
203
|
|
|
* default value or null flag. |
204
|
|
|
* |
205
|
|
|
* @param AbstractColumn $original |
206
|
|
|
* @return array |
207
|
|
|
*/ |
208
|
|
|
public function alteringOperations(AbstractColumn $original) |
209
|
|
|
{ |
210
|
|
|
$operations = []; |
211
|
|
|
|
212
|
|
|
$typeDefinition = [$this->type, $this->size, $this->precision, $this->scale]; |
213
|
|
|
$originalType = [$original->type, $original->size, $original->precision, $original->scale]; |
214
|
|
|
|
215
|
|
|
if ($typeDefinition != $originalType) { |
216
|
|
|
if ($this->abstractType() == 'enum') { |
217
|
|
|
//Getting longest value |
218
|
|
|
$enumSize = $this->size; |
219
|
|
|
foreach ($this->enumValues as $value) { |
220
|
|
|
$enumSize = max($enumSize, strlen($value)); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
$type = "ALTER COLUMN {$this->getName(true)} TYPE character($enumSize)"; |
224
|
|
|
$operations[] = $type; |
225
|
|
|
} else { |
226
|
|
|
$type = "ALTER COLUMN {$this->getName(true)} TYPE {$this->type}"; |
227
|
|
|
|
228
|
|
|
if (!empty($this->size)) { |
229
|
|
|
$type .= "($this->size)"; |
230
|
|
|
} elseif (!empty($this->precision)) { |
231
|
|
|
$type .= "($this->precision, $this->scale)"; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
//Required to perform cross conversion |
235
|
|
|
$operations[] = "{$type} USING {$this->getName(true)}::{$this->type}"; |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
if ($original->abstractType() == 'enum' && !empty($this->enumConstraint)) { |
240
|
|
|
$operations[] = 'DROP CONSTRAINT ' . $this->enumConstraint(true); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
if ($original->defaultValue != $this->defaultValue) { |
244
|
|
|
if (is_null($this->defaultValue)) { |
245
|
|
|
$operations[] = "ALTER COLUMN {$this->getName(true)} DROP DEFAULT"; |
246
|
|
|
} else { |
247
|
|
|
$operations[] = "ALTER COLUMN {$this->getName(true)} SET DEFAULT {$this->prepareDefault()}"; |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
if ($original->nullable != $this->nullable) { |
252
|
|
|
$operations[] = "ALTER COLUMN {$this->getName(true)} " |
253
|
|
|
. (!$this->nullable ? 'SET' : 'DROP') . " NOT NULL"; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
if ($this->abstractType() == 'enum') { |
257
|
|
|
$enumValues = []; |
258
|
|
|
foreach ($this->enumValues as $value) { |
259
|
|
|
$enumValues[] = $this->table->driver()->getPDO()->quote($value); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$operations[] = "ADD CONSTRAINT {$this->enumConstraint(true)} " |
263
|
|
|
. "CHECK ({$this->getName(true)} IN (" . join(', ', $enumValues) . "))"; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
return $operations; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* {@inheritdoc} |
271
|
|
|
*/ |
272
|
|
|
protected function resolveSchema($schema) |
273
|
|
|
{ |
274
|
|
|
$this->type = $schema['data_type']; |
275
|
|
|
$this->defaultValue = $schema['column_default']; |
276
|
|
|
$this->nullable = $schema['is_nullable'] == 'YES'; |
277
|
|
|
|
278
|
|
|
if ( |
279
|
|
|
in_array($this->type, ['int', 'bigint', 'integer']) |
280
|
|
|
&& preg_match("/nextval(.*)/", $this->defaultValue) |
281
|
|
|
) { |
282
|
|
|
$this->type = ($this->type == 'bigint' ? 'bigserial' : 'serial'); |
283
|
|
|
$this->autoIncrement = true; |
284
|
|
|
|
285
|
|
|
$this->defaultValue = new Fragment($this->defaultValue); |
286
|
|
|
|
287
|
|
|
return; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
if ( |
291
|
|
|
($this->type == 'character varying' || $this->type == 'character') |
292
|
|
|
&& $schema['character_maximum_length'] |
293
|
|
|
) { |
294
|
|
|
$this->size = $schema['character_maximum_length']; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
View Code Duplication |
if ($this->type == 'numeric') { |
|
|
|
|
298
|
|
|
$this->precision = $schema['numeric_precision']; |
299
|
|
|
$this->scale = $schema['numeric_scale']; |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* Attention, this is not default spiral enum type emulated via CHECK. This is real Postgres |
304
|
|
|
* enum type. |
305
|
|
|
*/ |
306
|
|
|
if ($this->type == 'USER-DEFINED' && $schema['typtype'] == 'e') { |
307
|
|
|
$this->type = $schema['typname']; |
308
|
|
|
$this->resolveNativeEnum(); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
//Potential enum with manually created constraint (check in) |
312
|
|
|
if ( |
313
|
|
|
($this->type == 'character' || $this->type == 'character varying') |
314
|
|
|
&& !empty($this->size) |
315
|
|
|
) { |
316
|
|
|
$this->checkCheckConstrain($schema['tableOID']); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
$this->normalizeDefault(); |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* {@inheritdoc} |
324
|
|
|
*/ |
325
|
|
|
protected function prepareEnum() |
326
|
|
|
{ |
327
|
|
|
return '(' . $this->size . ')'; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* Get name of enum constraint. |
332
|
|
|
* |
333
|
|
|
* @param bool $quote |
334
|
|
|
* @param bool $temporary If true enumConstraint identifier will be generated only for visual |
335
|
|
|
* purposes only. |
336
|
|
|
* @return string |
337
|
|
|
*/ |
338
|
|
|
private function enumConstraint($quote = false, $temporary = false) |
339
|
|
|
{ |
340
|
|
|
if (empty($this->enumConstraint)) { |
341
|
|
|
if ($temporary) { |
342
|
|
|
return $this->table->getName() . '_' . $this->getName() . '_enum'; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
$this->enumConstraint = $this->table->getName() . '_' . $this->getName() . '_enum_' . uniqid(); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
return $quote ? $this->table->driver()->identifier($this->enumConstraint) : $this->enumConstraint; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* Normalize default value. |
353
|
|
|
*/ |
354
|
|
|
private function normalizeDefault() |
355
|
|
|
{ |
356
|
|
|
if ($this->hasDefaultValue()) { |
357
|
|
|
if (preg_match("/^'?(.*?)'?::(.+)/", $this->defaultValue, $matches)) { |
358
|
|
|
//In database: 'value'::TYPE |
|
|
|
|
359
|
|
|
$this->defaultValue = $matches[1]; |
360
|
|
|
} elseif ($this->type == 'bit') { |
361
|
|
|
$this->defaultValue = bindec( |
362
|
|
|
substr($this->defaultValue, 2, strpos($this->defaultValue, '::') - 3) |
363
|
|
|
); |
364
|
|
|
} elseif ($this->type == 'boolean') { |
365
|
|
|
$this->defaultValue = (strtolower($this->defaultValue) == 'true'); |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* Resolve native enum type. |
372
|
|
|
**/ |
373
|
|
|
private function resolveNativeEnum() |
374
|
|
|
{ |
375
|
|
|
$range = $this->table->driver()->query( |
376
|
|
|
'SELECT enum_range(NULL::' . $this->type . ')' |
377
|
|
|
)->fetchColumn(0); |
378
|
|
|
|
379
|
|
|
$this->enumValues = explode(',', substr($range, 1, -1)); |
380
|
|
|
|
381
|
|
|
if (!empty($this->defaultValue)) { |
382
|
|
|
//In database: 'value'::enumType |
|
|
|
|
383
|
|
|
$this->defaultValue = substr( |
384
|
|
|
$this->defaultValue, |
385
|
|
|
1, |
386
|
|
|
strpos($this->defaultValue, $this->type) - 4 |
387
|
|
|
); |
388
|
|
|
} |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* Check if column was declared with check constrain. I love name of this method. |
393
|
|
|
* |
394
|
|
|
* @param string $tableOID |
395
|
|
|
*/ |
396
|
|
|
private function checkCheckConstrain($tableOID) |
397
|
|
|
{ |
398
|
|
|
$query = "SELECT conname, consrc FROM pg_constraint " |
399
|
|
|
. "WHERE conrelid = ? AND contype = 'c' AND (consrc LIKE ? OR consrc LIKE ?)"; |
400
|
|
|
|
401
|
|
|
$constraints = $this->table->driver()->query( |
402
|
|
|
$query, [$tableOID, '(' . $this->name . '%', '("' . $this->name . '%',] |
403
|
|
|
); |
404
|
|
|
|
405
|
|
View Code Duplication |
foreach ($constraints as $constraint) { |
|
|
|
|
406
|
|
|
if (preg_match('/ARRAY\[([^\]]+)\]/', $constraint['consrc'], $matches)) { |
407
|
|
|
$enumValues = explode(',', $matches[1]); |
408
|
|
|
foreach ($enumValues as &$value) { |
409
|
|
|
if (preg_match("/^'?(.*?)'?::(.+)/", trim($value), $matches)) { |
410
|
|
|
//In database: 'value'::TYPE |
|
|
|
|
411
|
|
|
$value = $matches[1]; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
unset($value); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
$this->enumValues = $enumValues; |
418
|
|
|
$this->enumConstraint = $constraint['conname']; |
419
|
|
|
} |
420
|
|
|
} |
421
|
|
|
} |
422
|
|
|
} |
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.