AbstractTable::normalizeSchema()   F
last analyzed

Complexity

Conditions 18
Paths 680

Size

Total Lines 80
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 18.0721

Importance

Changes 0
Metric Value
cc 18
eloc 34
c 0
b 0
f 0
nc 680
nop 1
dl 0
loc 80
ccs 31
cts 33
cp 0.9394
crap 18.0721
rs 1.1444

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 snowflake($column)
55
 * @method AbstractColumn ulid($column)
56
 * @method AbstractColumn uuid($column)
57
 */
58
abstract class AbstractTable implements TableInterface, ElementInterface
59
{
60
    /**
61
     * Table states.
62
     */
63
    public const STATUS_NEW = 0;
64
65
    public const STATUS_EXISTS = 1;
66
    public const STATUS_DECLARED_DROPPED = 2;
67
68
    /**
69
     * Initial table state.
70
     *
71
     * @internal
72
     */
73
    protected State $initial;
74
75
    /**
76
     * Currently defined table state.
77
     *
78
     * @internal
79
     */
80
    protected State $current;
81
82
    /**
83
     * Indication that table is exists and current schema is fetched from database.
84
     */
85
    private int $status = self::STATUS_NEW;
86
87
    /**
88 1974
     * @param DriverInterface $driver Parent driver.
89
     *
90
     * @psalm-param non-empty-string $name Table name, must include table prefix.
91
     *
92
     * @param string $prefix Database specific table prefix. Required for table renames.
93
     */
94 1974
    public function __construct(
95 1974
        protected DriverInterface $driver,
96 1974
        string $name,
97
        private string $prefix,
98 1974
    ) {
99 1928
        //Initializing states
100
        $prefixedName = $this->prefixTableName($name);
101
        $this->initial = new State($prefixedName);
102 1974
        $this->current = new State($prefixedName);
103
104 1928
        if ($this->driver->getSchemaHandler()->hasTable($this->getFullName())) {
105
            $this->status = self::STATUS_EXISTS;
106
        }
107 1974
108 1974
        if ($this->exists()) {
109
            //Initiating table schema
110
            $this->initSchema($this->initial);
111
        }
112
113
        $this->setState($this->initial);
114
    }
115 14
116
    /**
117 14
     * Sanitize column expression for index name
118
     *
119
     * @psalm-param non-empty-string $column
120
     *
121
     * @psalm-return non-empty-string
122
     */
123
    public static function sanitizeColumnExpression(string $column): string
124
    {
125
        return \preg_replace(['/\(/', '/\)/', '/ /'], '__', \strtolower($column));
126
    }
127
128
    /**
129
     * Get instance of associated driver.
130
     */
131
    public function getDriver(): DriverInterface
132 1948
    {
133
        return $this->driver;
134
    }
135 1948
136 1948
    /**
137
     * Return database specific table prefix.
138
     */
139
    public function getPrefix(): string
140
    {
141
        return $this->prefix;
142
    }
143
144
    public function getComparator(): ComparatorInterface
145
    {
146
        return new Comparator($this->initial, $this->current);
147
    }
148 1950
149
    public function exists(): bool
150 1950
    {
151 1950
        // Declared as dropped != actually dropped
152 1950
        return $this->status === self::STATUS_EXISTS || $this->status === self::STATUS_DECLARED_DROPPED;
153
    }
154 8
155
    /**
156
     * Table status (see codes above).
157 8
     */
158 8
    public function getStatus(): int
159 8
    {
160 8
        return $this->status;
161 8
    }
162 8
163 8
    /**
164
     * Sets table name. Use this function in combination with save to rename table.
165
     *
166
     * @psalm-param non-empty-string $name
167
     *
168
     * @psalm-return non-empty-string Prefixed table name.
169
     */
170 112
    public function setName(string $name): string
171
    {
172 112
        $this->current->setName($this->prefixTableName($name));
173
174
        return $this->getFullName();
175
    }
176
177
    /**
178 240
     * @psalm-return non-empty-string
179
     */
180 240
    public function getName(): string
181
    {
182
        return $this->getFullName();
183 1950
    }
184
185 1950
    /**
186
     * @psalm-return non-empty-string
187
     */
188 1974
    public function getFullName(): string
189
    {
190
        return $this->current->getName();
191 1974
    }
192
193
    /**
194
     * Table name before rename.
195
     *
196
     * @psalm-return non-empty-string
197 112
     */
198
    public function getInitialName(): string
199 112
    {
200
        return $this->initial->getName();
201
    }
202
203
    /**
204
     * Declare table as dropped, you have to sync table using "save" method in order to apply this
205
     * change.
206
     *
207
     * Attention, method will flush declared FKs to ensure that table express no dependecies.
208 132
     */
209
    public function declareDropped(): void
210 132
    {
211
        $this->status === self::STATUS_NEW and throw new SchemaException('Unable to drop non existed table');
212 132
213
        //Declaring as dropped
214
        $this->status = self::STATUS_DECLARED_DROPPED;
215
    }
216
217
    /**
218 18
     * Set table primary keys. Operation can only be applied for newly created tables. Now every
219
     * database might support compound indexes.
220 18
     */
221
    public function setPrimaryKeys(array $columns): self
222
    {
223
        //Originally i were forcing an exception when primary key were changed, now we should
224
        //force it when table will be synced
225
226 1974
        //Updating primary keys in current state
227
        $this->current->setPrimaryKeys($columns);
228 1974
229
        return $this;
230
    }
231
232
    public function getPrimaryKeys(): array
233
    {
234
        return $this->current->getPrimaryKeys();
235
    }
236 1930
237
    public function hasColumn(string $name): bool
238 1930
    {
239
        return $this->current->hasColumn($name);
240
    }
241
242
    /**
243
     * @return AbstractColumn[]
244
     */
245
    public function getColumns(): array
246
    {
247 1938
        return $this->current->getColumns();
248
    }
249 1938
250
    public function hasIndex(array $columns = []): bool
251
    {
252 1930
        return $this->current->hasIndex($columns);
253 1930
    }
254
255
    /**
256
     * @return AbstractIndex[]
257
     */
258
    public function getIndexes(): array
259 16
    {
260
        return $this->current->getIndexes();
261
    }
262
263
    public function hasForeignKey(array $columns): bool
264
    {
265 16
        return $this->current->hasForeignKey($columns);
266
    }
267 16
268
    /**
269
     * @return AbstractForeignKey[]
270 1952
     */
271
    public function getForeignKeys(): array
272 1952
    {
273
        return $this->current->getForeignKeys();
274
    }
275 568
276
    public function getDependencies(): array
277 568
    {
278
        $tables = [];
279
        foreach ($this->current->getForeignKeys() as $foreignKey) {
280
            $tables[] = $foreignKey->getForeignTable();
281
        }
282
283 1956
        return $tables;
284
    }
285 1956
286
    /**
287
     * Get/create instance of AbstractColumn associated with current table.
288 458
     *
289
     * Attention, renamed column will be available by it's old name until being synced!
290 458
     *
291
     * @psalm-param non-empty-string $name
292
     *
293
     * Examples:
294
     * $table->column('name')->string();
295
     */
296 1952
    public function column(string $name): 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...
297
    {
298 1952
        if ($this->current->hasColumn($name)) {
299
            //Column already exists
300
            return $this->current->findColumn($name);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->current->findColumn($name) could return the type null which is incompatible with the type-hinted return Cycle\Database\Schema\AbstractColumn. Consider adding an additional type-check to rule them out.
Loading history...
301 224
        }
302
303 224
        if ($this->initial->hasColumn($name)) {
304
            //Fetch from initial state (this code is required to ensure column states after schema
305
            //flushing)
306
            $column = clone $this->initial->findColumn($name);
307
        } else {
308
            $column = $this->createColumn($name);
309 1954
        }
310
311 1954
        $this->current->registerColumn($column);
312
313
        return $column;
314 120
    }
315
316 120
    /**
317 120
     * Get/create instance of AbstractIndex associated with current table based on list of forming
318 104
     * column names.
319
     *
320
     * Example:
321 120
     * $table->index(['key']);
322
     * $table->index(['key', 'key2']);
323
     *
324
     * @param array $columns List of index columns
325
     *
326
     * @throws SchemaException
327
     * @throws DriverException
328
     */
329
    public function index(array $columns): AbstractIndex
330
    {
331
        $original = $columns;
332
        $normalized = [];
333
        $sort = [];
334 1950
335
        foreach ($columns as $expression) {
336 1950
            [$column, $order] = AbstractIndex::parseColumn($expression);
337
338 988
            // If expression like 'column DESC' was passed, we cast it to 'column' => 'DESC'
339
            if ($order !== null) {
340
                $this->isIndexColumnSortingSupported() or throw new DriverException(\sprintf(
341 1948
                    'Failed to create index with `%s` on `%s`, column sorting is not supported',
342
                    $expression,
343
                    $this->getFullName(),
344
                ));
345
346 1948
                $sort[$column] = $order;
347
            }
348
349 1948
            $normalized[] = $column;
350
        }
351 1948
        $columns = $normalized;
352
353
        foreach ($columns as $column) {
354
            $this->hasColumn($column) or throw new SchemaException(
355
                "Undefined column '{$column}' in '{$this->getFullName()}'",
356
            );
357
        }
358
359
        if ($this->hasIndex($original)) {
360
            return $this->current->findIndex($original);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->current->findIndex($original) could return the type null which is incompatible with the type-hinted return Cycle\Database\Schema\AbstractIndex. Consider adding an additional type-check to rule them out.
Loading history...
361
        }
362
363
        if ($this->initial->hasIndex($original)) {
364
            //Let's ensure that index name is always stays synced (not regenerated)
365
            $name = $this->initial->findIndex($original)->getName();
366
        } else {
367 458
            $name = $this->createIdentifier('index', $original);
368
        }
369 458
370 458
        $index = $this->createIndex($name)->columns($columns)->sort($sort);
371 458
372
        //Adding to current schema
373 458
        $this->current->registerIndex($index);
374 458
375
        return $index;
376
    }
377 458
378 16
    /**
379
     * Get/create instance of AbstractReference associated with current table based on local column
380
     * name.
381
     *
382
     * @throws SchemaException
383
     */
384 16
    public function foreignKey(array $columns, bool $indexCreate = true): AbstractForeignKey
385
    {
386
        foreach ($columns as $column) {
387 458
            $this->hasColumn($column) or throw new SchemaException(
388
                "Undefined column '{$column}' in '{$this->getFullName()}'",
389 458
            );
390
        }
391 458
392 458
        if ($this->hasForeignKey($columns)) {
393
            return $this->current->findForeignKey($columns);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->current->findForeignKey($columns) could return the type null which is incompatible with the type-hinted return Cycle\Database\Schema\AbstractForeignKey. Consider adding an additional type-check to rule them out.
Loading history...
394
        }
395
396
        if ($this->initial->hasForeignKey($columns)) {
397 458
            //Let's ensure that FK name is always stays synced (not regenerated)
398 24
            $name = $this->initial->findForeignKey($columns)->getName();
399
        } else {
400
            $name = $this->createIdentifier('foreign', $columns);
401 458
        }
402
403
        $foreign = $this->createForeign($name)->columns($columns);
404
405 458
        //Adding to current schema
406
        $this->current->registerForeignKey($foreign);
407
408 458
        //Let's ensure index existence to performance and compatibility reasons
409
        $indexCreate ? $this->index($columns) : $foreign->setIndex(false);
410
411 458
        return $foreign;
412
    }
413 458
414
    /**
415
     * Rename column (only if column exists).
416
     *
417
     * @psalm-param non-empty-string $column
418
     * @psalm-param non-empty-string $name New column name.
419
     *
420
     * @throws SchemaException
421
     */
422 224
    public function renameColumn(string $column, string $name): self
423
    {
424 224
        $this->hasColumn($column) or throw new SchemaException(
425 224
            "Undefined column '{$column}' in '{$this->getFullName()}'",
426
        );
427
428
        //Rename operation is simple about declaring new name
429
        $this->column($column)->setName($name);
430 224
431 64
        return $this;
432
    }
433
434 224
    /**
435
     * Rename index (only if index exists).
436
     *
437
     * @param array $columns Index forming columns.
438 224
     *
439
     * @psalm-param non-empty-string $name New index name.
440
     *
441 224
     * @throws SchemaException
442
     */
443
    public function renameIndex(array $columns, string $name): self
444 224
    {
445
        $this->hasIndex($columns) or throw new SchemaException(
446
            "Undefined index ['" . \implode("', '", $columns) . "'] in '{$this->getFullName()}'",
447 224
        );
448
449 224
        //Declaring new index name
450
        $this->index($columns)->setName($name);
451
452
        return $this;
453
    }
454
455
    /**
456
     * Drop column by it's name.
457
     *
458
     * @psalm-param non-empty-string $column
459
     *
460 64
     * @throws SchemaException
461
     */
462 64
    public function dropColumn(string $column): self
463
    {
464
        $schema = $this->current->findColumn($column);
465
        $schema === null and throw new SchemaException("Undefined column '{$column}' in '{$this->getFullName()}'");
466
467 64
        //Dropping column from current schema
468
        $this->current->forgetColumn($schema);
0 ignored issues
show
Bug introduced by
It seems like $schema can also be of type null; however, parameter $column of Cycle\Database\Schema\State::forgetColumn() does only seem to accept Cycle\Database\Schema\AbstractColumn, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

468
        $this->current->forgetColumn(/** @scrutinizer ignore-type */ $schema);
Loading history...
469 64
470
        return $this;
471
    }
472
473
    /**
474
     * Drop index by it's forming columns.
475
     *
476
     * @throws SchemaException
477
     */
478
    public function dropIndex(array $columns): self
479
    {
480 8
        $schema = $this->current->findIndex($columns);
481
        $schema === null and throw new SchemaException(
482 8
            "Undefined index ['" . \implode("', '", $columns) . "'] in '{$this->getFullName()}'",
483
        );
484
485
        //Dropping index from current schema
486
        $this->current->forgetIndex($schema);
0 ignored issues
show
Bug introduced by
It seems like $schema can also be of type null; however, parameter $index of Cycle\Database\Schema\State::forgetIndex() does only seem to accept Cycle\Database\Schema\AbstractIndex, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

486
        $this->current->forgetIndex(/** @scrutinizer ignore-type */ $schema);
Loading history...
487 8
488
        return $this;
489 8
    }
490
491
    /**
492
     * Drop foreign key by it's name.
493
     *
494
     * @throws SchemaException
495
     */
496
    public function dropForeignKey(array $columns): self
497
    {
498
        $schema = $this->current->findForeignKey($columns);
499 32
        if ($schema === null) {
500
            $names = \implode("','", $columns);
501 32
            throw new SchemaException("Undefined FK on '{$names}' in '{$this->getFullName()}'");
502 32
        }
503
504
        //Dropping foreign from current schema
505 32
        $this->current->forgetForeignKey($schema);
506
507 32
        return $this;
508
    }
509
510
    /**
511
     * Get current table state (detached).
512
     */
513
    public function getState(): State
514
    {
515 102
        $state = clone $this->current;
516
        $state->remountElements();
517 102
518 102
        return $state;
519
    }
520
521
    /**
522
     * Reset table state to new form.
523 102
     *
524
     * @param State $state Use null to flush table schema.
525 102
     */
526
    public function setState(?State $state = null): self
527
    {
528
        $this->current = new State($this->initial->getName());
529
530
        if ($state !== null) {
531
            $this->current->setName($state->getName());
532
            $this->current->syncState($state);
533 216
        }
534
535 216
        return $this;
536 216
    }
537
538
    /**
539
     * Reset table state to it initial form.
540
     */
541
    public function resetState(): self
542 216
    {
543
        $this->setState($this->initial);
544 216
545
        return $this;
546
    }
547
548
    /**
549
     * Save table schema including every column, index, foreign key creation/altering. If table
550 852
     * does not exist it must be created. If table declared as dropped it will be removed from
551
     * the database.
552 852
     *
553 852
     * @param int  $operation Operation to be performed while table being saved. In some cases
554
     *                        (when multiple tables are being updated) it is reasonable to drop
555 852
     *                        foreign keys and indexes prior to dropping related columns. See sync
556
     *                        bus class to get more details.
557
     * @param bool $reset     When true schema will be marked as synced.
558
     *
559
     * @throws HandlerException
560
     * @throws SchemaException
561
     */
562
    public function save(int $operation = HandlerInterface::DO_ALL, bool $reset = true): void
563 1974
    {
564
        // We need an instance of Handler of dbal operations
565 1974
        $handler = $this->driver->getSchemaHandler();
566
567 1974
        if ($this->status === self::STATUS_DECLARED_DROPPED && $operation & HandlerInterface::DO_DROP) {
568 1974
            //We don't need reflector for this operation
569 1974
            $handler->dropTable($this);
570
571
            //Flushing status
572 1974
            $this->status = self::STATUS_NEW;
573
574
            return;
575
        }
576
577
        // Ensure that columns references to valid indexes and et
578 116
        $prepared = $this->normalizeSchema(($operation & HandlerInterface::CREATE_FOREIGN_KEYS) !== 0);
579
580 116
        if ($this->status === self::STATUS_NEW) {
581
            //Executing table creation
582 116
            $handler->createTable($prepared);
583
        } else {
584
            //Executing table syncing
585
            if ($this->hasChanges()) {
586
                $handler->syncTable($prepared, $operation);
587
            }
588
        }
589
590
        // Syncing our schemas
591
        if ($reset) {
592
            $this->status = self::STATUS_EXISTS;
593
            $this->initial->syncState($prepared->current);
594
        }
595
    }
596
597
    /**
598
     * Shortcut for column() method.
599 1950
     *
600
     * @psalm-param non-empty-string $column
601
     */
602 1950
    public function __get(string $column): AbstractColumn
603
    {
604 1950
        return $this->column($column);
605
    }
606 1930
607
    /**
608
     * Column creation/altering shortcut, call chain is identical to:
609 1930
     * AbstractTable->column($name)->$type($arguments).
610
     *
611 1930
     * Example:
612
     * $table->string("name");
613
     * $table->text("some_column");
614
     *
615 1950
     * @psalm-param non-empty-string $type
616
     *
617 1950
     * @param array $arguments Type specific parameters.
618
     */
619 1948
    public function __call(string $type, array $arguments): AbstractColumn
620
    {
621
        return \call_user_func_array(
622 1836
            [$this->column($arguments[0]), $type],
623 646
            \array_slice($arguments, 1),
624
        );
625
    }
626
627
    public function __toString(): string
628 1944
    {
629 1944
        return $this->getFullName();
630 1944
    }
631
632 1944
    /**
633
     * Cloning schemas as well.
634
     */
635
    public function __clone()
636
    {
637
        $this->initial = clone $this->initial;
638
        $this->current = clone $this->current;
639
    }
640
641 458
    public function __debugInfo(): array
642
    {
643 458
        return [
644
            'status'      => $this->status,
645
            'full_name'   => $this->getFullName(),
646
            'name'        => $this->getName(),
647
            'primaryKeys' => $this->getPrimaryKeys(),
648
            'columns'     => \array_values($this->getColumns()),
649 1836
            'indexes'     => \array_values($this->getIndexes()),
650
            'foreignKeys' => \array_values($this->getForeignKeys()),
651 1836
        ];
652
    }
653
654
    /**
655
     * Check if table schema has been modified since synchronization.
656
     */
657
    protected function hasChanges(): bool
658
    {
659
        return $this->getComparator()->hasChanges() || $this->status === self::STATUS_DECLARED_DROPPED;
660
    }
661 1974
662
    /**
663 1974
     * Add prefix to a given table name
664
     *
665
     * @psalm-param non-empty-string $column
666
     *
667
     * @psalm-return non-empty-string
668
     */
669
    protected function prefixTableName(string $name): string
670 1950
    {
671
        return $this->prefix . $name;
672
    }
673 1950
674
    /**
675
     * Ensure that no wrong indexes left in table. This method will create AbstractTable
676 1950
     * copy in order to prevent cross modifications.
677 16
     */
678 8
    protected function normalizeSchema(bool $withForeignKeys = true): self
679
    {
680
        // To make sure that no pre-sync modifications will be reflected on current table
681
        $target = clone $this;
682
683
        // declare all FKs dropped on tables scheduled for removal
684
        if ($this->status === self::STATUS_DECLARED_DROPPED) {
685
            foreach ($target->getForeignKeys() as $fk) {
686 1950
                $target->current->forgetForeignKey($fk);
687 36
            }
688 12
        }
689
690
        /*
691
         * In cases where columns are removed we have to automatically remove related indexes and
692
         * foreign keys.
693 36
         */
694 12
        foreach ($this->getComparator()->droppedColumns() as $column) {
695
            foreach ($target->getIndexes() as $index) {
696
                if (\in_array($column->getName(), $index->getColumns(), true)) {
697
                    $target->current->forgetIndex($index);
698
                }
699
            }
700
701 1950
            foreach ($target->getForeignKeys() as $foreign) {
702
                if ($column->getName() === $foreign->getColumns()) {
703
                    $target->current->forgetForeignKey($foreign);
704
                }
705
            }
706 150
        }
707
708 150
        //We also have to adjusts indexes and foreign keys
709 24
        foreach ($this->getComparator()->alteredColumns() as $pair) {
710 16
            /**
711
             * @var AbstractColumn $initial
712
             * @var AbstractColumn $name
713 16
             */
714 16
            [$name, $initial] = $pair;
715 16
716
            foreach ($target->getIndexes() as $index) {
717
                if (\in_array($initial->getName(), $index->getColumns(), true)) {
718 16
                    $columns = $index->getColumns();
719
720 16
                    //Replacing column name
721
                    foreach ($columns as &$column) {
722 16
                        if ($column === $initial->getName()) {
723 16
                            $column = $name->getName();
724
                        }
725 16
726
                        unset($column);
727
                    }
728 16
                    unset($column);
729
730
                    $targetIndex = $target->initial->findIndex($index->getColumns());
731
                    if ($targetIndex !== null) {
732 150
                        //Target index got renamed or removed.
733 8
                        $targetIndex->columns($columns);
734 8
                    }
735 8
736 8
                    $index->columns($columns);
737
                }
738
            }
739
740
            foreach ($target->getForeignKeys() as $foreign) {
741
                $foreign->columns(
742 1950
                    \array_map(
743 1834
                        static fn($column) => $column === $initial->getName() ? $name->getName() : $column,
744
                        $foreign->getColumns(),
745 96
                    ),
746
                );
747
            }
748
        }
749 1950
750
        if (!$withForeignKeys) {
751
            foreach ($this->getComparator()->addedForeignKeys() as $foreign) {
752
                //Excluding from creation
753
                $target->current->forgetForeignKey($foreign);
754
            }
755 1928
        }
756
757 1928
        return $target;
758 1928
    }
759
760
    /**
761 1928
     * Populate table schema with values from database.
762 458
     */
763
    protected function initSchema(State $state): void
764
    {
765 1928
        foreach ($this->fetchColumns() as $column) {
766 224
            $state->registerColumn($column);
767
        }
768
769 1928
        foreach ($this->fetchIndexes() as $index) {
770
            $state->registerIndex($index);
771 1928
        }
772
773 12
        foreach ($this->fetchReferences() as $foreign) {
774
            $state->registerForeignKey($foreign);
775 12
        }
776
777
        $state->setPrimaryKeys($this->fetchPrimaryKeys());
778
        //DBMS specific initialization can be placed here
779
    }
780
781
    protected function isIndexColumnSortingSupported(): bool
782
    {
783
        return true;
784
    }
785
786
    /**
787
     * Fetch index declarations from database.
788
     *
789
     * @return AbstractColumn[]
790
     */
791
    abstract protected function fetchColumns(): array;
792
793
    /**
794
     * Fetch index declarations from database.
795
     *
796
     * @return AbstractIndex[]
797
     */
798
    abstract protected function fetchIndexes(): array;
799
800
    /**
801
     * Fetch references declaration from database.
802
     *
803
     * @return AbstractForeignKey[]
804
     */
805
    abstract protected function fetchReferences(): array;
806
807
    /**
808
     * Fetch names of primary keys from table.
809
     */
810
    abstract protected function fetchPrimaryKeys(): array;
811
812
    /**
813
     * Create column with a given name.
814
     *
815
     * @psalm-param non-empty-string $name
816
     *
817
     */
818
    abstract protected function createColumn(string $name): AbstractColumn;
819
820
    /**
821
     * Create index for a given set of columns.
822
     *
823
     * @psalm-param non-empty-string $name
824
     */
825
    abstract protected function createIndex(string $name): AbstractIndex;
826
827
    /**
828
     * Create reference on a given column set.
829
     *
830
     * @psalm-param non-empty-string $name
831
     */
832 458
    abstract protected function createForeign(string $name): AbstractForeignKey;
833
834
    /**
835 458
     * Generate unique name for indexes and foreign keys.
836 458
     *
837 458
     * @psalm-param non-empty-string $type
838
     */
839
    protected function createIdentifier(string $type, array $columns): string
840 458
    {
841 458
        // Sanitize columns in case they have expressions
842 458
        $sanitized = [];
843 458
        foreach ($columns as $column) {
844
            $sanitized[] = self::sanitizeColumnExpression($column);
845 458
        }
846
847 24
        $name = $this->getFullName()
848
            . '_' . $type
849
            . '_' . \implode('_', $sanitized)
850 458
            . '_' . \uniqid();
851
852
        if (\strlen($name) > 64) {
853
            //Many DBMS has limitations on identifier length
854
            $name = \md5($name);
855
        }
856
857
        return $name;
858
    }
859
}
860