1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral Framework, Core Components |
4
|
|
|
* |
5
|
|
|
* @author Wolfy-J |
6
|
|
|
*/ |
7
|
|
|
namespace Spiral\Database\Drivers\Postgres\Schemas; |
8
|
|
|
|
9
|
|
|
use Spiral\Database\Entities\Driver; |
10
|
|
|
use Spiral\Database\Injections\Fragment; |
11
|
|
|
use Spiral\Database\Schemas\ColumnInterface; |
12
|
|
|
use Spiral\Database\Schemas\Prototypes\AbstractColumn; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* @todo investigate potential issue with entity non handling enum correctly when multiple |
16
|
|
|
* @todo column changes happen in one session (who the hell will do that?) |
17
|
|
|
*/ |
18
|
|
|
class PostgresColumn extends AbstractColumn |
19
|
|
|
{ |
20
|
|
|
/** |
21
|
|
|
* Default timestamp expression (driver specific). |
22
|
|
|
*/ |
23
|
|
|
const DATETIME_NOW = 'now()'; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Private state related values. |
27
|
|
|
*/ |
28
|
|
|
const EXCLUDE_FROM_COMPARE = [ |
29
|
|
|
'timezone', |
30
|
|
|
'constrained', |
31
|
|
|
'constrainName' |
32
|
|
|
]; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* {@inheritdoc} |
36
|
|
|
*/ |
37
|
|
|
protected $mapping = [ |
38
|
|
|
//Primary sequences |
39
|
|
|
'primary' => ['type' => 'serial', 'autoIncrement' => true, 'nullable' => false], |
40
|
|
|
'bigPrimary' => ['type' => 'bigserial', 'autoIncrement' => true, 'nullable' => false], |
41
|
|
|
|
42
|
|
|
//Enum type (mapped via method) |
43
|
|
|
'enum' => 'enum', |
44
|
|
|
|
45
|
|
|
//Logical types |
46
|
|
|
'boolean' => 'boolean', |
47
|
|
|
|
48
|
|
|
//Integer types (size can always be changed with size method), longInteger has method alias |
49
|
|
|
//bigInteger |
50
|
|
|
'integer' => 'integer', |
51
|
|
|
'tinyInteger' => 'smallint', |
52
|
|
|
'bigInteger' => 'bigint', |
53
|
|
|
|
54
|
|
|
//String with specified length (mapped via method) |
55
|
|
|
'string' => 'character varying', |
56
|
|
|
|
57
|
|
|
//Generic types |
58
|
|
|
'text' => 'text', |
59
|
|
|
'tinyText' => 'text', |
60
|
|
|
'longText' => 'text', |
61
|
|
|
|
62
|
|
|
//Real types |
63
|
|
|
'double' => 'double precision', |
64
|
|
|
'float' => 'real', |
65
|
|
|
|
66
|
|
|
//Decimal type (mapped via method) |
67
|
|
|
'decimal' => 'numeric', |
68
|
|
|
|
69
|
|
|
//Date and Time types |
70
|
|
|
'datetime' => 'timestamp without time zone', |
71
|
|
|
'date' => 'date', |
72
|
|
|
'time' => 'time without time zone', |
73
|
|
|
'timestamp' => 'timestamp without time zone', |
74
|
|
|
|
75
|
|
|
//Binary types |
76
|
|
|
'binary' => 'bytea', |
77
|
|
|
'tinyBinary' => 'bytea', |
78
|
|
|
'longBinary' => 'bytea', |
79
|
|
|
|
80
|
|
|
//Additional types |
81
|
|
|
'json' => 'text', |
82
|
|
|
]; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* {@inheritdoc} |
86
|
|
|
*/ |
87
|
|
|
protected $reverseMapping = [ |
88
|
|
|
'primary' => ['serial'], |
89
|
|
|
'bigPrimary' => ['bigserial'], |
90
|
|
|
'enum' => ['enum'], |
91
|
|
|
'boolean' => ['boolean'], |
92
|
|
|
'integer' => ['int', 'integer', 'int4'], |
93
|
|
|
'tinyInteger' => ['smallint'], |
94
|
|
|
'bigInteger' => ['bigint', 'int8'], |
95
|
|
|
'string' => ['character varying', 'character'], |
96
|
|
|
'text' => ['text'], |
97
|
|
|
'double' => ['double precision'], |
98
|
|
|
'float' => ['real', 'money'], |
99
|
|
|
'decimal' => ['numeric'], |
100
|
|
|
'date' => ['date'], |
101
|
|
|
'time' => ['time', 'time with time zone', 'time without time zone'], |
102
|
|
|
'timestamp' => ['timestamp', 'timestamp with time zone', 'timestamp without time zone'], |
103
|
|
|
'binary' => ['bytea'], |
104
|
|
|
'json' => ['text'], |
105
|
|
|
]; |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Field is auto incremental. |
109
|
|
|
* |
110
|
|
|
* @var bool |
111
|
|
|
*/ |
112
|
|
|
protected $autoIncrement = false; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Indication that column have enum constrain. |
116
|
|
|
* |
117
|
|
|
* @var bool |
118
|
|
|
*/ |
119
|
|
|
protected $constrained = false; |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Name of enum constraint associated with field. |
123
|
|
|
* |
124
|
|
|
* @var string |
125
|
|
|
*/ |
126
|
|
|
protected $constrainName = ''; |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* {@inheritdoc} |
130
|
|
|
*/ |
131
|
|
|
public function getConstraints(): array |
132
|
|
|
{ |
133
|
|
|
$constraints = parent::getConstraints(); |
134
|
|
|
|
135
|
|
|
if ($this->constrained) { |
136
|
|
|
$constraints[] = $this->constrainName; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
return $constraints; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* {@inheritdoc} |
144
|
|
|
*/ |
145
|
|
|
public function abstractType(): string |
146
|
|
|
{ |
147
|
|
|
if (!empty($this->enumValues)) { |
148
|
|
|
return 'enum'; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
return parent::abstractType(); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* {@inheritdoc} |
156
|
|
|
*/ |
157
|
|
|
public function primary(): AbstractColumn |
158
|
|
|
{ |
159
|
|
|
if (!empty($this->type) && $this->type != 'serial') { |
160
|
|
|
//Change type of already existed column (we can't use "serial" alias here) |
161
|
|
|
$this->type = 'integer'; |
162
|
|
|
|
163
|
|
|
return $this; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
return $this->setType('primary'); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* {@inheritdoc} |
171
|
|
|
*/ |
172
|
|
|
public function bigPrimary(): AbstractColumn |
173
|
|
|
{ |
174
|
|
|
if (!empty($this->type) && $this->type != 'bigserial') { |
175
|
|
|
//Change type of already existed column (we can't use "serial" alias here) |
176
|
|
|
$this->type = 'bigint'; |
177
|
|
|
|
178
|
|
|
return $this; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
return $this->setType('bigPrimary'); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* {@inheritdoc} |
186
|
|
|
*/ |
187
|
|
View Code Duplication |
public function enum($values): AbstractColumn |
|
|
|
|
188
|
|
|
{ |
189
|
|
|
$this->enumValues = array_map('strval', is_array($values) ? $values : func_get_args()); |
190
|
|
|
|
191
|
|
|
$this->type = 'character'; |
192
|
|
|
foreach ($this->enumValues as $value) { |
193
|
|
|
$this->size = max((int)$this->size, strlen($value)); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
return $this; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* {@inheritdoc} |
201
|
|
|
*/ |
202
|
|
|
public function sqlStatement(Driver $driver): string |
203
|
|
|
{ |
204
|
|
|
$statement = parent::sqlStatement($driver); |
205
|
|
|
|
206
|
|
|
if ($this->abstractType() != 'enum') { |
207
|
|
|
//Nothing special |
208
|
|
|
return $statement; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
//We have add constraint for enum type |
212
|
|
|
$enumValues = []; |
213
|
|
|
foreach ($this->enumValues as $value) { |
214
|
|
|
$enumValues[] = $driver->quote($value); |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
$constrain = $driver->identifier($this->enumConstraint()); |
218
|
|
|
$column = $driver->identifier($this->getName()); |
219
|
|
|
$values = implode(', ', $enumValues); |
220
|
|
|
|
221
|
|
|
return "{$statement} CONSTRAINT {$constrain} CHECK ($column IN ({$values}))"; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Generate set of operations need to change column. |
226
|
|
|
* |
227
|
|
|
* @param Driver $driver |
228
|
|
|
* @param AbstractColumn $initial |
229
|
|
|
* |
230
|
|
|
* @return array |
231
|
|
|
*/ |
232
|
|
|
public function alterOperations(Driver $driver, AbstractColumn $initial): array |
233
|
|
|
{ |
234
|
|
|
$operations = []; |
235
|
|
|
|
236
|
|
|
//To simplify comparation |
237
|
|
|
$currentType = [$this->type, $this->size, $this->precision, $this->scale]; |
238
|
|
|
$initialType = [$initial->type, $initial->size, $initial->precision, $initial->scale]; |
239
|
|
|
|
240
|
|
|
$identifier = $driver->identifier($this->getName()); |
241
|
|
|
|
242
|
|
|
/* |
243
|
|
|
* This block defines column type and all variations. |
244
|
|
|
*/ |
245
|
|
|
if ($currentType != $initialType) { |
246
|
|
|
if ($this->abstractType() == 'enum') { |
247
|
|
|
//Getting longest value |
248
|
|
|
$enumSize = $this->size; |
249
|
|
|
foreach ($this->enumValues as $value) { |
250
|
|
|
$enumSize = max($enumSize, strlen($value)); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
$type = "ALTER COLUMN {$identifier} TYPE character($enumSize)"; |
254
|
|
|
$operations[] = $type; |
255
|
|
|
} else { |
256
|
|
|
$type = "ALTER COLUMN {$identifier} TYPE {$this->type}"; |
257
|
|
|
|
258
|
|
|
if (!empty($this->size)) { |
259
|
|
|
$type .= "($this->size)"; |
260
|
|
|
} elseif (!empty($this->precision)) { |
261
|
|
|
$type .= "($this->precision, $this->scale)"; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
//Required to perform cross conversion |
265
|
|
|
$operations[] = "{$type} USING {$identifier}::{$this->type}"; |
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
//Dropping enum constrain before any operation |
270
|
|
|
if ($initial->abstractType() == 'enum' && $this->constrained) { |
271
|
|
|
$operations[] = 'DROP CONSTRAINT ' . $driver->identifier($this->enumConstraint()); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
//Default value set and dropping |
275
|
|
|
if ($initial->defaultValue != $this->defaultValue) { |
276
|
|
|
if (is_null($this->defaultValue)) { |
277
|
|
|
$operations[] = "ALTER COLUMN {$identifier} DROP DEFAULT"; |
278
|
|
|
} else { |
279
|
|
|
$operations[] = "ALTER COLUMN {$identifier} SET DEFAULT {$this->quoteDefault($driver)}"; |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
//Nullable option |
284
|
|
|
if ($initial->nullable != $this->nullable) { |
285
|
|
|
$operations[] = "ALTER COLUMN {$identifier} " . (!$this->nullable ? 'SET' : 'DROP') . ' NOT NULL'; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
if ($this->abstractType() == 'enum') { |
289
|
|
|
$enumValues = []; |
290
|
|
|
foreach ($this->enumValues as $value) { |
291
|
|
|
$enumValues[] = $driver->quote($value); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
$operations[] = "ADD CONSTRAINT {$driver->identifier($this->enumConstraint())} " |
295
|
|
|
. "CHECK ({$identifier} IN (" . implode(', ', $enumValues) . '))'; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
return $operations; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* {@inheritdoc} |
303
|
|
|
*/ |
304
|
|
|
protected function quoteEnum(Driver $driver): string |
305
|
|
|
{ |
306
|
|
|
//Postgres enums are just constrained strings |
307
|
|
|
return '(' . $this->size . ')'; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Get/generate name for enum constraint. |
312
|
|
|
* |
313
|
|
|
* @return string |
314
|
|
|
*/ |
315
|
|
View Code Duplication |
private function enumConstraint(): string |
|
|
|
|
316
|
|
|
{ |
317
|
|
|
if (empty($this->constrainName)) { |
318
|
|
|
$this->constrainName = $this->table . '_' . $this->getName() . '_enum_' . uniqid(); |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
return $this->constrainName; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Normalize default value. |
326
|
|
|
*/ |
327
|
|
|
private function normalizeDefault() |
328
|
|
|
{ |
329
|
|
|
if ($this->hasDefaultValue()) { |
330
|
|
|
if (preg_match("/^'?(.*?)'?::(.+)/", $this->defaultValue, $matches)) { |
331
|
|
|
//In database: 'value'::TYPE |
332
|
|
|
$this->defaultValue = $matches[1]; |
333
|
|
|
} elseif ($this->type == 'bit') { |
334
|
|
|
$this->defaultValue = bindec( |
335
|
|
|
substr($this->defaultValue, 2, strpos($this->defaultValue, '::') - 3) |
336
|
|
|
); |
337
|
|
|
} elseif ($this->type == 'boolean') { |
338
|
|
|
$this->defaultValue = (strtolower($this->defaultValue) == 'true'); |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* @param string $table Table name. |
345
|
|
|
* @param array $schema |
346
|
|
|
* @param Driver $driver Postgres columns are bit more complex. |
347
|
|
|
* |
348
|
|
|
* @return PostgresColumn |
349
|
|
|
*/ |
350
|
|
|
public static function createInstance(string $table, array $schema, Driver $driver): self |
351
|
|
|
{ |
352
|
|
|
$column = new self($table, $schema['column_name'], $driver->getTimezone()); |
353
|
|
|
|
354
|
|
|
$column->type = $schema['data_type']; |
355
|
|
|
$column->defaultValue = $schema['column_default']; |
356
|
|
|
$column->nullable = $schema['is_nullable'] == 'YES'; |
357
|
|
|
|
358
|
|
|
if ( |
359
|
|
|
in_array($column->type, ['int', 'bigint', 'integer']) |
360
|
|
|
&& preg_match('/nextval(.*)/', $column->defaultValue) |
361
|
|
|
) { |
362
|
|
|
$column->type = ($column->type == 'bigint' ? 'bigserial' : 'serial'); |
363
|
|
|
$column->autoIncrement = true; |
364
|
|
|
|
365
|
|
|
$column->defaultValue = new Fragment($column->defaultValue); |
366
|
|
|
|
367
|
|
|
return $column; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
if (strpos($column->type, 'char') !== false && $schema['character_maximum_length']) { |
371
|
|
|
$column->size = $schema['character_maximum_length']; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
View Code Duplication |
if ($column->type == 'numeric') { |
|
|
|
|
375
|
|
|
$column->precision = $schema['numeric_precision']; |
376
|
|
|
$column->scale = $schema['numeric_scale']; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
if ($column->type == 'USER-DEFINED' && $schema['typtype'] == 'e') { |
380
|
|
|
$column->type = $schema['typname']; |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Attention, this is not default spiral enum type emulated via CHECK. This is real |
384
|
|
|
* Postgres enum type. |
385
|
|
|
*/ |
386
|
|
|
self::resolveEnum($driver, $column); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
if (strpos($column->type, 'char') !== false && !empty($column->size)) { |
390
|
|
|
//Potential enum with manually created constraint (check in) |
391
|
|
|
self::resolveConstrains($driver, $schema['tableOID'], $column); |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
$column->normalizeDefault(); |
395
|
|
|
|
396
|
|
|
return $column; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* Resolving enum constrain and converting it into proper enum values set. |
401
|
|
|
* |
402
|
|
|
* @param Driver $driver |
403
|
|
|
* @param string|int $tableOID |
404
|
|
|
* @param PostgresColumn $column |
405
|
|
|
*/ |
406
|
|
|
private static function resolveConstrains(Driver $driver, $tableOID, PostgresColumn $column) |
407
|
|
|
{ |
408
|
|
|
$query = "SELECT conname, consrc FROM pg_constraint WHERE conrelid = ? AND contype = 'c' AND (consrc LIKE ? OR consrc LIKE ?)"; |
409
|
|
|
|
410
|
|
|
$constraints = $driver->query($query, [ |
411
|
|
|
$tableOID, |
412
|
|
|
'(' . $column->name . '%', |
413
|
|
|
'("' . $column->name . '%' |
414
|
|
|
]); |
415
|
|
|
|
416
|
|
|
foreach ($constraints as $constraint) { |
417
|
|
|
if (preg_match('/ARRAY\[([^\]]+)\]/', $constraint['consrc'], $matches)) { |
418
|
|
|
$enumValues = explode(',', $matches[1]); |
419
|
|
View Code Duplication |
foreach ($enumValues as &$value) { |
|
|
|
|
420
|
|
|
if (preg_match("/^'?(.*?)'?::(.+)/", trim($value), $matches)) { |
421
|
|
|
//In database: 'value'::TYPE |
422
|
|
|
$value = $matches[1]; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
unset($value); |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
$column->enumValues = $enumValues; |
429
|
|
|
$column->constrainName = $constraint['conname']; |
430
|
|
|
$column->constrained = true; |
431
|
|
|
} |
432
|
|
|
} |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* Resolve native ENUM type if presented. |
437
|
|
|
* |
438
|
|
|
* @param Driver $driver |
439
|
|
|
* @param PostgresColumn $column |
440
|
|
|
*/ |
441
|
|
|
private static function resolveEnum(Driver $driver, PostgresColumn $column) |
442
|
|
|
{ |
443
|
|
|
$range = $driver->query('SELECT enum_range(NULL::' . $column->type . ')')->fetchColumn(0); |
444
|
|
|
|
445
|
|
|
$column->enumValues = explode(',', substr($range, 1, -1)); |
446
|
|
|
|
447
|
|
|
if (!empty($column->defaultValue)) { |
448
|
|
|
//In database: 'value'::enumType |
449
|
|
|
$column->defaultValue = substr( |
450
|
|
|
$column->defaultValue, |
451
|
|
|
1, |
452
|
|
|
strpos($column->defaultValue, $column->type) - 4 |
453
|
|
|
); |
454
|
|
|
} |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* {@inheritdoc} |
459
|
|
|
*/ |
460
|
|
|
public function compare(ColumnInterface $initial): bool |
461
|
|
|
{ |
462
|
|
|
if (parent::compare($initial)) { |
463
|
|
|
return true; |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
if ( |
467
|
|
|
in_array($this->abstractType(), ['primary', 'bigPrimary']) |
468
|
|
|
&& $initial->getDefaultValue() != $this->getDefaultValue() |
469
|
|
|
) { |
470
|
|
|
//PG adds default values to primary keys |
471
|
|
|
return true; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
return false; |
475
|
|
|
} |
476
|
|
|
} |
477
|
|
|
|
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.