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\Schema; |
13
|
|
|
|
14
|
|
|
use Cycle\Database\Driver\DriverInterface; |
15
|
|
|
use Cycle\Database\Driver\HandlerInterface; |
16
|
|
|
use Cycle\Database\Exception\DriverException; |
17
|
|
|
use Cycle\Database\Exception\HandlerException; |
18
|
|
|
use Cycle\Database\Exception\SchemaException; |
19
|
|
|
use Cycle\Database\TableInterface; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* AbstractTable class used to describe and manage state of specified table. It provides ability to |
23
|
|
|
* get table introspection, update table schema and automatically generate set of diff operations. |
24
|
|
|
* |
25
|
|
|
* Most of table operation like column, index or foreign key creation/altering will be applied when |
26
|
|
|
* save() method will be called. |
27
|
|
|
* |
28
|
|
|
* Column configuration shortcuts: |
29
|
|
|
* |
30
|
|
|
* @method AbstractColumn primary($column) |
31
|
|
|
* @method AbstractColumn bigPrimary($column) |
32
|
|
|
* @method AbstractColumn enum($column, array $values) |
33
|
|
|
* @method AbstractColumn string($column, $length = 255) |
34
|
|
|
* @method AbstractColumn decimal($column, $precision, $scale) |
35
|
|
|
* @method AbstractColumn boolean($column) |
36
|
|
|
* @method AbstractColumn integer($column) |
37
|
|
|
* @method AbstractColumn tinyInteger($column) |
38
|
|
|
* @method AbstractColumn smallInteger($column) |
39
|
|
|
* @method AbstractColumn bigInteger($column) |
40
|
|
|
* @method AbstractColumn text($column) |
41
|
|
|
* @method AbstractColumn tinyText($column) |
42
|
|
|
* @method AbstractColumn mediumText($column) |
43
|
|
|
* @method AbstractColumn longText($column) |
44
|
|
|
* @method AbstractColumn json($column) |
45
|
|
|
* @method AbstractColumn double($column) |
46
|
|
|
* @method AbstractColumn float($column) |
47
|
|
|
* @method AbstractColumn datetime($column, $size = 0) |
48
|
|
|
* @method AbstractColumn date($column) |
49
|
|
|
* @method AbstractColumn time($column) |
50
|
|
|
* @method AbstractColumn timestamp($column) |
51
|
|
|
* @method AbstractColumn binary($column) |
52
|
|
|
* @method AbstractColumn tinyBinary($column) |
53
|
|
|
* @method AbstractColumn longBinary($column) |
54
|
|
|
* @method AbstractColumn uuid($column) |
55
|
|
|
*/ |
56
|
|
|
abstract class AbstractTable implements TableInterface, ElementInterface |
57
|
|
|
{ |
58
|
|
|
/** |
59
|
|
|
* Table states. |
60
|
|
|
*/ |
61
|
|
|
public const STATUS_NEW = 0; |
62
|
|
|
|
63
|
|
|
public const STATUS_EXISTS = 1; |
64
|
|
|
public const STATUS_DECLARED_DROPPED = 2; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Initial table state. |
68
|
|
|
* |
69
|
|
|
* @internal |
70
|
|
|
*/ |
71
|
|
|
protected State $initial; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Currently defined table state. |
75
|
|
|
* |
76
|
|
|
* @internal |
77
|
|
|
*/ |
78
|
|
|
protected State $current; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Indication that table is exists and current schema is fetched from database. |
82
|
|
|
*/ |
83
|
|
|
private int $status = self::STATUS_NEW; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @param DriverInterface $driver Parent driver. |
87
|
|
|
* |
88
|
1974 |
|
* @psalm-param non-empty-string $name Table name, must include table prefix. |
89
|
|
|
* |
90
|
|
|
* @param string $prefix Database specific table prefix. Required for table renames. |
91
|
|
|
*/ |
92
|
|
|
public function __construct( |
93
|
|
|
protected DriverInterface $driver, |
94
|
1974 |
|
string $name, |
95
|
1974 |
|
private string $prefix, |
96
|
1974 |
|
) { |
97
|
|
|
//Initializing states |
98
|
1974 |
|
$prefixedName = $this->prefixTableName($name); |
99
|
1928 |
|
$this->initial = new State($prefixedName); |
100
|
|
|
$this->current = new State($prefixedName); |
101
|
|
|
|
102
|
1974 |
|
if ($this->driver->getSchemaHandler()->hasTable($this->getFullName())) { |
103
|
|
|
$this->status = self::STATUS_EXISTS; |
104
|
1928 |
|
} |
105
|
|
|
|
106
|
|
|
if ($this->exists()) { |
107
|
1974 |
|
//Initiating table schema |
108
|
1974 |
|
$this->initSchema($this->initial); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
$this->setState($this->initial); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
14 |
|
* Sanitize column expression for index name |
116
|
|
|
* |
117
|
14 |
|
* @psalm-param non-empty-string $column |
118
|
|
|
* |
119
|
|
|
* @psalm-return non-empty-string |
120
|
|
|
*/ |
121
|
|
|
public static function sanitizeColumnExpression(string $column): string |
122
|
|
|
{ |
123
|
|
|
return \preg_replace(['/\(/', '/\)/', '/ /'], '__', \strtolower($column)); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Get instance of associated driver. |
128
|
|
|
*/ |
129
|
|
|
public function getDriver(): DriverInterface |
130
|
|
|
{ |
131
|
|
|
return $this->driver; |
132
|
1948 |
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
1948 |
|
* Return database specific table prefix. |
136
|
1948 |
|
*/ |
137
|
|
|
public function getPrefix(): string |
138
|
|
|
{ |
139
|
|
|
return $this->prefix; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
public function getComparator(): ComparatorInterface |
143
|
|
|
{ |
144
|
|
|
return new Comparator($this->initial, $this->current); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
public function exists(): bool |
148
|
1950 |
|
{ |
149
|
|
|
// Declared as dropped != actually dropped |
150
|
1950 |
|
return $this->status === self::STATUS_EXISTS || $this->status === self::STATUS_DECLARED_DROPPED; |
151
|
1950 |
|
} |
152
|
1950 |
|
|
153
|
|
|
/** |
154
|
8 |
|
* Table status (see codes above). |
155
|
|
|
*/ |
156
|
|
|
public function getStatus(): int |
157
|
8 |
|
{ |
158
|
8 |
|
return $this->status; |
159
|
8 |
|
} |
160
|
8 |
|
|
161
|
8 |
|
/** |
162
|
8 |
|
* Sets table name. Use this function in combination with save to rename table. |
163
|
8 |
|
* |
164
|
|
|
* @psalm-param non-empty-string $name |
165
|
|
|
* |
166
|
|
|
* @psalm-return non-empty-string Prefixed table name. |
167
|
|
|
*/ |
168
|
|
|
public function setName(string $name): string |
169
|
|
|
{ |
170
|
112 |
|
$this->current->setName($this->prefixTableName($name)); |
171
|
|
|
|
172
|
112 |
|
return $this->getFullName(); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* @psalm-return non-empty-string |
177
|
|
|
*/ |
178
|
240 |
|
public function getName(): string |
179
|
|
|
{ |
180
|
240 |
|
return $this->getFullName(); |
181
|
|
|
} |
182
|
|
|
|
183
|
1950 |
|
/** |
184
|
|
|
* @psalm-return non-empty-string |
185
|
1950 |
|
*/ |
186
|
|
|
public function getFullName(): string |
187
|
|
|
{ |
188
|
1974 |
|
return $this->current->getName(); |
189
|
|
|
} |
190
|
|
|
|
191
|
1974 |
|
/** |
192
|
|
|
* Table name before rename. |
193
|
|
|
* |
194
|
|
|
* @psalm-return non-empty-string |
195
|
|
|
*/ |
196
|
|
|
public function getInitialName(): string |
197
|
112 |
|
{ |
198
|
|
|
return $this->initial->getName(); |
199
|
112 |
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Declare table as dropped, you have to sync table using "save" method in order to apply this |
203
|
|
|
* change. |
204
|
|
|
* |
205
|
|
|
* Attention, method will flush declared FKs to ensure that table express no dependecies. |
206
|
|
|
*/ |
207
|
|
|
public function declareDropped(): void |
208
|
132 |
|
{ |
209
|
|
|
$this->status === self::STATUS_NEW and throw new SchemaException('Unable to drop non existed table'); |
210
|
132 |
|
|
211
|
|
|
//Declaring as dropped |
212
|
132 |
|
$this->status = self::STATUS_DECLARED_DROPPED; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Set table primary keys. Operation can only be applied for newly created tables. Now every |
217
|
|
|
* database might support compound indexes. |
218
|
18 |
|
*/ |
219
|
|
|
public function setPrimaryKeys(array $columns): self |
220
|
18 |
|
{ |
221
|
|
|
//Originally i were forcing an exception when primary key were changed, now we should |
222
|
|
|
//force it when table will be synced |
223
|
|
|
|
224
|
|
|
//Updating primary keys in current state |
225
|
|
|
$this->current->setPrimaryKeys($columns); |
226
|
1974 |
|
|
227
|
|
|
return $this; |
228
|
1974 |
|
} |
229
|
|
|
|
230
|
|
|
public function getPrimaryKeys(): array |
231
|
|
|
{ |
232
|
|
|
return $this->current->getPrimaryKeys(); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
public function hasColumn(string $name): bool |
236
|
1930 |
|
{ |
237
|
|
|
return $this->current->hasColumn($name); |
238
|
1930 |
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* @return AbstractColumn[] |
242
|
|
|
*/ |
243
|
|
|
public function getColumns(): array |
244
|
|
|
{ |
245
|
|
|
return $this->current->getColumns(); |
246
|
|
|
} |
247
|
1938 |
|
|
248
|
|
|
public function hasIndex(array $columns = []): bool |
249
|
1938 |
|
{ |
250
|
|
|
return $this->current->hasIndex($columns); |
251
|
|
|
} |
252
|
1930 |
|
|
253
|
1930 |
|
/** |
254
|
|
|
* @return AbstractIndex[] |
255
|
|
|
*/ |
256
|
|
|
public function getIndexes(): array |
257
|
|
|
{ |
258
|
|
|
return $this->current->getIndexes(); |
259
|
16 |
|
} |
260
|
|
|
|
261
|
|
|
public function hasForeignKey(array $columns): bool |
262
|
|
|
{ |
263
|
|
|
return $this->current->hasForeignKey($columns); |
264
|
|
|
} |
265
|
16 |
|
|
266
|
|
|
/** |
267
|
16 |
|
* @return AbstractForeignKey[] |
268
|
|
|
*/ |
269
|
|
|
public function getForeignKeys(): array |
270
|
1952 |
|
{ |
271
|
|
|
return $this->current->getForeignKeys(); |
272
|
1952 |
|
} |
273
|
|
|
|
274
|
|
|
public function getDependencies(): array |
275
|
568 |
|
{ |
276
|
|
|
$tables = []; |
277
|
568 |
|
foreach ($this->current->getForeignKeys() as $foreignKey) { |
278
|
|
|
$tables[] = $foreignKey->getForeignTable(); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
return $tables; |
282
|
|
|
} |
283
|
1956 |
|
|
284
|
|
|
/** |
285
|
1956 |
|
* Get/create instance of AbstractColumn associated with current table. |
286
|
|
|
* |
287
|
|
|
* Attention, renamed column will be available by it's old name until being synced! |
288
|
458 |
|
* |
289
|
|
|
* @psalm-param non-empty-string $name |
290
|
458 |
|
* |
291
|
|
|
* Examples: |
292
|
|
|
* $table->column('name')->string(); |
293
|
|
|
*/ |
294
|
|
|
public function column(string $name): AbstractColumn |
|
|
|
|
295
|
|
|
{ |
296
|
1952 |
|
if ($this->current->hasColumn($name)) { |
297
|
|
|
//Column already exists |
298
|
1952 |
|
return $this->current->findColumn($name); |
|
|
|
|
299
|
|
|
} |
300
|
|
|
|
301
|
224 |
|
if ($this->initial->hasColumn($name)) { |
302
|
|
|
//Fetch from initial state (this code is required to ensure column states after schema |
303
|
224 |
|
//flushing) |
304
|
|
|
$column = clone $this->initial->findColumn($name); |
305
|
|
|
} else { |
306
|
|
|
$column = $this->createColumn($name); |
307
|
|
|
} |
308
|
|
|
|
309
|
1954 |
|
$this->current->registerColumn($column); |
310
|
|
|
|
311
|
1954 |
|
return $column; |
312
|
|
|
} |
313
|
|
|
|
314
|
120 |
|
/** |
315
|
|
|
* Get/create instance of AbstractIndex associated with current table based on list of forming |
316
|
120 |
|
* column names. |
317
|
120 |
|
* |
318
|
104 |
|
* Example: |
319
|
|
|
* $table->index(['key']); |
320
|
|
|
* $table->index(['key', 'key2']); |
321
|
120 |
|
* |
322
|
|
|
* @param array $columns List of index columns |
323
|
|
|
* |
324
|
|
|
* @throws SchemaException |
325
|
|
|
* @throws DriverException |
326
|
|
|
*/ |
327
|
|
|
public function index(array $columns): AbstractIndex |
328
|
|
|
{ |
329
|
|
|
$original = $columns; |
330
|
|
|
$normalized = []; |
331
|
|
|
$sort = []; |
332
|
|
|
|
333
|
|
|
foreach ($columns as $expression) { |
334
|
1950 |
|
[$column, $order] = AbstractIndex::parseColumn($expression); |
335
|
|
|
|
336
|
1950 |
|
// If expression like 'column DESC' was passed, we cast it to 'column' => 'DESC' |
337
|
|
|
if ($order !== null) { |
338
|
988 |
|
$this->isIndexColumnSortingSupported() or throw new DriverException(\sprintf( |
339
|
|
|
'Failed to create index with `%s` on `%s`, column sorting is not supported', |
340
|
|
|
$expression, |
341
|
1948 |
|
$this->getFullName(), |
342
|
|
|
)); |
343
|
|
|
|
344
|
|
|
$sort[$column] = $order; |
345
|
|
|
} |
346
|
1948 |
|
|
347
|
|
|
$normalized[] = $column; |
348
|
|
|
} |
349
|
1948 |
|
$columns = $normalized; |
350
|
|
|
|
351
|
1948 |
|
foreach ($columns as $column) { |
352
|
|
|
$this->hasColumn($column) or throw new SchemaException( |
353
|
|
|
"Undefined column '{$column}' in '{$this->getFullName()}'", |
354
|
|
|
); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
if ($this->hasIndex($original)) { |
358
|
|
|
return $this->current->findIndex($original); |
|
|
|
|
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
if ($this->initial->hasIndex($original)) { |
362
|
|
|
//Let's ensure that index name is always stays synced (not regenerated) |
363
|
|
|
$name = $this->initial->findIndex($original)->getName(); |
364
|
|
|
} else { |
365
|
|
|
$name = $this->createIdentifier('index', $original); |
366
|
|
|
} |
367
|
458 |
|
|
368
|
|
|
$index = $this->createIndex($name)->columns($columns)->sort($sort); |
369
|
458 |
|
|
370
|
458 |
|
//Adding to current schema |
371
|
458 |
|
$this->current->registerIndex($index); |
372
|
|
|
|
373
|
458 |
|
return $index; |
374
|
458 |
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
458 |
|
* Get/create instance of AbstractReference associated with current table based on local column |
378
|
16 |
|
* name. |
379
|
|
|
* |
380
|
|
|
* @throws SchemaException |
381
|
|
|
*/ |
382
|
|
|
public function foreignKey(array $columns, bool $indexCreate = true): AbstractForeignKey |
383
|
|
|
{ |
384
|
16 |
|
foreach ($columns as $column) { |
385
|
|
|
$this->hasColumn($column) or throw new SchemaException( |
386
|
|
|
"Undefined column '{$column}' in '{$this->getFullName()}'", |
387
|
458 |
|
); |
388
|
|
|
} |
389
|
458 |
|
|
390
|
|
|
if ($this->hasForeignKey($columns)) { |
391
|
458 |
|
return $this->current->findForeignKey($columns); |
|
|
|
|
392
|
458 |
|
} |
393
|
|
|
|
394
|
|
|
if ($this->initial->hasForeignKey($columns)) { |
395
|
|
|
//Let's ensure that FK name is always stays synced (not regenerated) |
396
|
|
|
$name = $this->initial->findForeignKey($columns)->getName(); |
397
|
458 |
|
} else { |
398
|
24 |
|
$name = $this->createIdentifier('foreign', $columns); |
399
|
|
|
} |
400
|
|
|
|
401
|
458 |
|
$foreign = $this->createForeign($name)->columns($columns); |
402
|
|
|
|
403
|
|
|
//Adding to current schema |
404
|
|
|
$this->current->registerForeignKey($foreign); |
405
|
458 |
|
|
406
|
|
|
//Let's ensure index existence to performance and compatibility reasons |
407
|
|
|
$indexCreate ? $this->index($columns) : $foreign->setIndex(false); |
408
|
458 |
|
|
409
|
|
|
return $foreign; |
410
|
|
|
} |
411
|
458 |
|
|
412
|
|
|
/** |
413
|
458 |
|
* Rename column (only if column exists). |
414
|
|
|
* |
415
|
|
|
* @psalm-param non-empty-string $column |
416
|
|
|
* @psalm-param non-empty-string $name New column name. |
417
|
|
|
* |
418
|
|
|
* @throws SchemaException |
419
|
|
|
*/ |
420
|
|
|
public function renameColumn(string $column, string $name): self |
421
|
|
|
{ |
422
|
224 |
|
$this->hasColumn($column) or throw new SchemaException( |
423
|
|
|
"Undefined column '{$column}' in '{$this->getFullName()}'", |
424
|
224 |
|
); |
425
|
224 |
|
|
426
|
|
|
//Rename operation is simple about declaring new name |
427
|
|
|
$this->column($column)->setName($name); |
428
|
|
|
|
429
|
|
|
return $this; |
430
|
224 |
|
} |
431
|
64 |
|
|
432
|
|
|
/** |
433
|
|
|
* Rename index (only if index exists). |
434
|
224 |
|
* |
435
|
|
|
* @param array $columns Index forming columns. |
436
|
|
|
* |
437
|
|
|
* @psalm-param non-empty-string $name New index name. |
438
|
224 |
|
* |
439
|
|
|
* @throws SchemaException |
440
|
|
|
*/ |
441
|
224 |
|
public function renameIndex(array $columns, string $name): self |
442
|
|
|
{ |
443
|
|
|
$this->hasIndex($columns) or throw new SchemaException( |
444
|
224 |
|
"Undefined index ['" . \implode("', '", $columns) . "'] in '{$this->getFullName()}'", |
445
|
|
|
); |
446
|
|
|
|
447
|
224 |
|
//Declaring new index name |
448
|
|
|
$this->index($columns)->setName($name); |
449
|
224 |
|
|
450
|
|
|
return $this; |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
/** |
454
|
|
|
* Drop column by it's name. |
455
|
|
|
* |
456
|
|
|
* @psalm-param non-empty-string $column |
457
|
|
|
* |
458
|
|
|
* @throws SchemaException |
459
|
|
|
*/ |
460
|
64 |
|
public function dropColumn(string $column): self |
461
|
|
|
{ |
462
|
64 |
|
$schema = $this->current->findColumn($column); |
463
|
|
|
$schema === null and throw new SchemaException("Undefined column '{$column}' in '{$this->getFullName()}'"); |
464
|
|
|
|
465
|
|
|
//Dropping column from current schema |
466
|
|
|
$this->current->forgetColumn($schema); |
|
|
|
|
467
|
64 |
|
|
468
|
|
|
return $this; |
469
|
64 |
|
} |
470
|
|
|
|
471
|
|
|
/** |
472
|
|
|
* Drop index by it's forming columns. |
473
|
|
|
* |
474
|
|
|
* @throws SchemaException |
475
|
|
|
*/ |
476
|
|
|
public function dropIndex(array $columns): self |
477
|
|
|
{ |
478
|
|
|
$schema = $this->current->findIndex($columns); |
479
|
|
|
$schema === null and throw new SchemaException( |
480
|
8 |
|
"Undefined index ['" . \implode("', '", $columns) . "'] in '{$this->getFullName()}'", |
481
|
|
|
); |
482
|
8 |
|
|
483
|
|
|
//Dropping index from current schema |
484
|
|
|
$this->current->forgetIndex($schema); |
|
|
|
|
485
|
|
|
|
486
|
|
|
return $this; |
487
|
8 |
|
} |
488
|
|
|
|
489
|
8 |
|
/** |
490
|
|
|
* Drop foreign key by it's name. |
491
|
|
|
* |
492
|
|
|
* @throws SchemaException |
493
|
|
|
*/ |
494
|
|
|
public function dropForeignKey(array $columns): self |
495
|
|
|
{ |
496
|
|
|
$schema = $this->current->findForeignKey($columns); |
497
|
|
|
if ($schema === null) { |
498
|
|
|
$names = \implode("','", $columns); |
499
|
32 |
|
throw new SchemaException("Undefined FK on '{$names}' in '{$this->getFullName()}'"); |
500
|
|
|
} |
501
|
32 |
|
|
502
|
32 |
|
//Dropping foreign from current schema |
503
|
|
|
$this->current->forgetForeignKey($schema); |
504
|
|
|
|
505
|
32 |
|
return $this; |
506
|
|
|
} |
507
|
32 |
|
|
508
|
|
|
/** |
509
|
|
|
* Get current table state (detached). |
510
|
|
|
*/ |
511
|
|
|
public function getState(): State |
512
|
|
|
{ |
513
|
|
|
$state = clone $this->current; |
514
|
|
|
$state->remountElements(); |
515
|
102 |
|
|
516
|
|
|
return $state; |
517
|
102 |
|
} |
518
|
102 |
|
|
519
|
|
|
/** |
520
|
|
|
* Reset table state to new form. |
521
|
|
|
* |
522
|
|
|
* @param State $state Use null to flush table schema. |
523
|
102 |
|
*/ |
524
|
|
|
public function setState(?State $state = null): self |
525
|
102 |
|
{ |
526
|
|
|
$this->current = new State($this->initial->getName()); |
527
|
|
|
|
528
|
|
|
if ($state !== null) { |
529
|
|
|
$this->current->setName($state->getName()); |
530
|
|
|
$this->current->syncState($state); |
531
|
|
|
} |
532
|
|
|
|
533
|
216 |
|
return $this; |
534
|
|
|
} |
535
|
216 |
|
|
536
|
216 |
|
/** |
537
|
|
|
* Reset table state to it initial form. |
538
|
|
|
*/ |
539
|
|
|
public function resetState(): self |
540
|
|
|
{ |
541
|
|
|
$this->setState($this->initial); |
542
|
216 |
|
|
543
|
|
|
return $this; |
544
|
216 |
|
} |
545
|
|
|
|
546
|
|
|
/** |
547
|
|
|
* Save table schema including every column, index, foreign key creation/altering. If table |
548
|
|
|
* does not exist it must be created. If table declared as dropped it will be removed from |
549
|
|
|
* the database. |
550
|
852 |
|
* |
551
|
|
|
* @param int $operation Operation to be performed while table being saved. In some cases |
552
|
852 |
|
* (when multiple tables are being updated) it is reasonable to drop |
553
|
852 |
|
* foreign keys and indexes prior to dropping related columns. See sync |
554
|
|
|
* bus class to get more details. |
555
|
852 |
|
* @param bool $reset When true schema will be marked as synced. |
556
|
|
|
* |
557
|
|
|
* @throws HandlerException |
558
|
|
|
* @throws SchemaException |
559
|
|
|
*/ |
560
|
|
|
public function save(int $operation = HandlerInterface::DO_ALL, bool $reset = true): void |
561
|
|
|
{ |
562
|
|
|
// We need an instance of Handler of dbal operations |
563
|
1974 |
|
$handler = $this->driver->getSchemaHandler(); |
564
|
|
|
|
565
|
1974 |
|
if ($this->status === self::STATUS_DECLARED_DROPPED && $operation & HandlerInterface::DO_DROP) { |
566
|
|
|
//We don't need reflector for this operation |
567
|
1974 |
|
$handler->dropTable($this); |
568
|
1974 |
|
|
569
|
1974 |
|
//Flushing status |
570
|
|
|
$this->status = self::STATUS_NEW; |
571
|
|
|
|
572
|
1974 |
|
return; |
573
|
|
|
} |
574
|
|
|
|
575
|
|
|
// Ensure that columns references to valid indexes and et |
576
|
|
|
$prepared = $this->normalizeSchema(($operation & HandlerInterface::CREATE_FOREIGN_KEYS) !== 0); |
577
|
|
|
|
578
|
116 |
|
if ($this->status === self::STATUS_NEW) { |
579
|
|
|
//Executing table creation |
580
|
116 |
|
$handler->createTable($prepared); |
581
|
|
|
} else { |
582
|
116 |
|
//Executing table syncing |
583
|
|
|
if ($this->hasChanges()) { |
584
|
|
|
$handler->syncTable($prepared, $operation); |
585
|
|
|
} |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
// Syncing our schemas |
589
|
|
|
if ($reset) { |
590
|
|
|
$this->status = self::STATUS_EXISTS; |
591
|
|
|
$this->initial->syncState($prepared->current); |
592
|
|
|
} |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
/** |
596
|
|
|
* Shortcut for column() method. |
597
|
|
|
* |
598
|
|
|
* @psalm-param non-empty-string $column |
599
|
1950 |
|
*/ |
600
|
|
|
public function __get(string $column): AbstractColumn |
601
|
|
|
{ |
602
|
1950 |
|
return $this->column($column); |
603
|
|
|
} |
604
|
1950 |
|
|
605
|
|
|
/** |
606
|
1930 |
|
* Column creation/altering shortcut, call chain is identical to: |
607
|
|
|
* AbstractTable->column($name)->$type($arguments). |
608
|
|
|
* |
609
|
1930 |
|
* Example: |
610
|
|
|
* $table->string("name"); |
611
|
1930 |
|
* $table->text("some_column"); |
612
|
|
|
* |
613
|
|
|
* @psalm-param non-empty-string $type |
614
|
|
|
* |
615
|
1950 |
|
* @param array $arguments Type specific parameters. |
616
|
|
|
*/ |
617
|
1950 |
|
public function __call(string $type, array $arguments): AbstractColumn |
618
|
|
|
{ |
619
|
1948 |
|
return \call_user_func_array( |
620
|
|
|
[$this->column($arguments[0]), $type], |
621
|
|
|
\array_slice($arguments, 1), |
622
|
1836 |
|
); |
623
|
646 |
|
} |
624
|
|
|
|
625
|
|
|
public function __toString(): string |
626
|
|
|
{ |
627
|
|
|
return $this->getFullName(); |
628
|
1944 |
|
} |
629
|
1944 |
|
|
630
|
1944 |
|
/** |
631
|
|
|
* Cloning schemas as well. |
632
|
1944 |
|
*/ |
633
|
|
|
public function __clone() |
634
|
|
|
{ |
635
|
|
|
$this->initial = clone $this->initial; |
636
|
|
|
$this->current = clone $this->current; |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
public function __debugInfo(): array |
640
|
|
|
{ |
641
|
458 |
|
return [ |
642
|
|
|
'status' => $this->status, |
643
|
458 |
|
'full_name' => $this->getFullName(), |
644
|
|
|
'name' => $this->getName(), |
645
|
|
|
'primaryKeys' => $this->getPrimaryKeys(), |
646
|
|
|
'columns' => \array_values($this->getColumns()), |
647
|
|
|
'indexes' => \array_values($this->getIndexes()), |
648
|
|
|
'foreignKeys' => \array_values($this->getForeignKeys()), |
649
|
1836 |
|
]; |
650
|
|
|
} |
651
|
1836 |
|
|
652
|
|
|
/** |
653
|
|
|
* Check if table schema has been modified since synchronization. |
654
|
|
|
*/ |
655
|
|
|
protected function hasChanges(): bool |
656
|
|
|
{ |
657
|
|
|
return $this->getComparator()->hasChanges() || $this->status === self::STATUS_DECLARED_DROPPED; |
658
|
|
|
} |
659
|
|
|
|
660
|
|
|
/** |
661
|
1974 |
|
* Add prefix to a given table name |
662
|
|
|
* |
663
|
1974 |
|
* @psalm-param non-empty-string $column |
664
|
|
|
* |
665
|
|
|
* @psalm-return non-empty-string |
666
|
|
|
*/ |
667
|
|
|
protected function prefixTableName(string $name): string |
668
|
|
|
{ |
669
|
|
|
return $this->prefix . $name; |
670
|
1950 |
|
} |
671
|
|
|
|
672
|
|
|
/** |
673
|
1950 |
|
* Ensure that no wrong indexes left in table. This method will create AbstractTable |
674
|
|
|
* copy in order to prevent cross modifications. |
675
|
|
|
*/ |
676
|
1950 |
|
protected function normalizeSchema(bool $withForeignKeys = true): self |
677
|
16 |
|
{ |
678
|
8 |
|
// To make sure that no pre-sync modifications will be reflected on current table |
679
|
|
|
$target = clone $this; |
680
|
|
|
|
681
|
|
|
// declare all FKs dropped on tables scheduled for removal |
682
|
|
|
if ($this->status === self::STATUS_DECLARED_DROPPED) { |
683
|
|
|
foreach ($target->getForeignKeys() as $fk) { |
684
|
|
|
$target->current->forgetForeignKey($fk); |
685
|
|
|
} |
686
|
1950 |
|
} |
687
|
36 |
|
|
688
|
12 |
|
/* |
689
|
|
|
* In cases where columns are removed we have to automatically remove related indexes and |
690
|
|
|
* foreign keys. |
691
|
|
|
*/ |
692
|
|
|
foreach ($this->getComparator()->droppedColumns() as $column) { |
693
|
36 |
|
foreach ($target->getIndexes() as $index) { |
694
|
12 |
|
if (\in_array($column->getName(), $index->getColumns(), true)) { |
695
|
|
|
$target->current->forgetIndex($index); |
696
|
|
|
} |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
foreach ($target->getForeignKeys() as $foreign) { |
700
|
|
|
if ($column->getName() === $foreign->getColumns()) { |
701
|
1950 |
|
$target->current->forgetForeignKey($foreign); |
702
|
|
|
} |
703
|
|
|
} |
704
|
|
|
} |
705
|
|
|
|
706
|
150 |
|
//We also have to adjusts indexes and foreign keys |
707
|
|
|
foreach ($this->getComparator()->alteredColumns() as $pair) { |
708
|
150 |
|
/** |
709
|
24 |
|
* @var AbstractColumn $initial |
710
|
16 |
|
* @var AbstractColumn $name |
711
|
|
|
*/ |
712
|
|
|
[$name, $initial] = $pair; |
713
|
16 |
|
|
714
|
16 |
|
foreach ($target->getIndexes() as $index) { |
715
|
16 |
|
if (\in_array($initial->getName(), $index->getColumns(), true)) { |
716
|
|
|
$columns = $index->getColumns(); |
717
|
|
|
|
718
|
16 |
|
//Replacing column name |
719
|
|
|
foreach ($columns as &$column) { |
720
|
16 |
|
if ($column === $initial->getName()) { |
721
|
|
|
$column = $name->getName(); |
722
|
16 |
|
} |
723
|
16 |
|
|
724
|
|
|
unset($column); |
725
|
16 |
|
} |
726
|
|
|
unset($column); |
727
|
|
|
|
728
|
16 |
|
$targetIndex = $target->initial->findIndex($index->getColumns()); |
729
|
|
|
if ($targetIndex !== null) { |
730
|
|
|
//Target index got renamed or removed. |
731
|
|
|
$targetIndex->columns($columns); |
732
|
150 |
|
} |
733
|
8 |
|
|
734
|
8 |
|
$index->columns($columns); |
735
|
8 |
|
} |
736
|
8 |
|
} |
737
|
|
|
|
738
|
|
|
foreach ($target->getForeignKeys() as $foreign) { |
739
|
|
|
$foreign->columns( |
740
|
|
|
\array_map( |
741
|
|
|
static fn($column) => $column === $initial->getName() ? $name->getName() : $column, |
742
|
1950 |
|
$foreign->getColumns(), |
743
|
1834 |
|
), |
744
|
|
|
); |
745
|
96 |
|
} |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
if (!$withForeignKeys) { |
749
|
1950 |
|
foreach ($this->getComparator()->addedForeignKeys() as $foreign) { |
750
|
|
|
//Excluding from creation |
751
|
|
|
$target->current->forgetForeignKey($foreign); |
752
|
|
|
} |
753
|
|
|
} |
754
|
|
|
|
755
|
1928 |
|
return $target; |
756
|
|
|
} |
757
|
1928 |
|
|
758
|
1928 |
|
/** |
759
|
|
|
* Populate table schema with values from database. |
760
|
|
|
*/ |
761
|
1928 |
|
protected function initSchema(State $state): void |
762
|
458 |
|
{ |
763
|
|
|
foreach ($this->fetchColumns() as $column) { |
764
|
|
|
$state->registerColumn($column); |
765
|
1928 |
|
} |
766
|
224 |
|
|
767
|
|
|
foreach ($this->fetchIndexes() as $index) { |
768
|
|
|
$state->registerIndex($index); |
769
|
1928 |
|
} |
770
|
|
|
|
771
|
1928 |
|
foreach ($this->fetchReferences() as $foreign) { |
772
|
|
|
$state->registerForeignKey($foreign); |
773
|
12 |
|
} |
774
|
|
|
|
775
|
12 |
|
$state->setPrimaryKeys($this->fetchPrimaryKeys()); |
776
|
|
|
//DBMS specific initialization can be placed here |
777
|
|
|
} |
778
|
|
|
|
779
|
|
|
protected function isIndexColumnSortingSupported(): bool |
780
|
|
|
{ |
781
|
|
|
return true; |
782
|
|
|
} |
783
|
|
|
|
784
|
|
|
/** |
785
|
|
|
* Fetch index declarations from database. |
786
|
|
|
* |
787
|
|
|
* @return AbstractColumn[] |
788
|
|
|
*/ |
789
|
|
|
abstract protected function fetchColumns(): array; |
790
|
|
|
|
791
|
|
|
/** |
792
|
|
|
* Fetch index declarations from database. |
793
|
|
|
* |
794
|
|
|
* @return AbstractIndex[] |
795
|
|
|
*/ |
796
|
|
|
abstract protected function fetchIndexes(): array; |
797
|
|
|
|
798
|
|
|
/** |
799
|
|
|
* Fetch references declaration from database. |
800
|
|
|
* |
801
|
|
|
* @return AbstractForeignKey[] |
802
|
|
|
*/ |
803
|
|
|
abstract protected function fetchReferences(): array; |
804
|
|
|
|
805
|
|
|
/** |
806
|
|
|
* Fetch names of primary keys from table. |
807
|
|
|
*/ |
808
|
|
|
abstract protected function fetchPrimaryKeys(): array; |
809
|
|
|
|
810
|
|
|
/** |
811
|
|
|
* Create column with a given name. |
812
|
|
|
* |
813
|
|
|
* @psalm-param non-empty-string $name |
814
|
|
|
* |
815
|
|
|
*/ |
816
|
|
|
abstract protected function createColumn(string $name): AbstractColumn; |
817
|
|
|
|
818
|
|
|
/** |
819
|
|
|
* Create index for a given set of columns. |
820
|
|
|
* |
821
|
|
|
* @psalm-param non-empty-string $name |
822
|
|
|
*/ |
823
|
|
|
abstract protected function createIndex(string $name): AbstractIndex; |
824
|
|
|
|
825
|
|
|
/** |
826
|
|
|
* Create reference on a given column set. |
827
|
|
|
* |
828
|
|
|
* @psalm-param non-empty-string $name |
829
|
|
|
*/ |
830
|
|
|
abstract protected function createForeign(string $name): AbstractForeignKey; |
831
|
|
|
|
832
|
458 |
|
/** |
833
|
|
|
* Generate unique name for indexes and foreign keys. |
834
|
|
|
* |
835
|
458 |
|
* @psalm-param non-empty-string $type |
836
|
458 |
|
*/ |
837
|
458 |
|
protected function createIdentifier(string $type, array $columns): string |
838
|
|
|
{ |
839
|
|
|
// Sanitize columns in case they have expressions |
840
|
458 |
|
$sanitized = []; |
841
|
458 |
|
foreach ($columns as $column) { |
842
|
458 |
|
$sanitized[] = self::sanitizeColumnExpression($column); |
843
|
458 |
|
} |
844
|
|
|
|
845
|
458 |
|
$name = $this->getFullName() |
846
|
|
|
. '_' . $type |
847
|
24 |
|
. '_' . \implode('_', $sanitized) |
848
|
|
|
. '_' . \uniqid(); |
849
|
|
|
|
850
|
458 |
|
if (\strlen($name) > 64) { |
851
|
|
|
//Many DBMS has limitations on identifier length |
852
|
|
|
$name = \md5($name); |
853
|
|
|
} |
854
|
|
|
|
855
|
|
|
return $name; |
856
|
|
|
} |
857
|
|
|
} |
858
|
|
|
|
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths