AbstractTable::setPrimaryKeys()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 9
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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
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...
295
    {
296 1952
        if ($this->current->hasColumn($name)) {
297
            //Column already exists
298 1952
            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...
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);
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...
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);
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...
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);
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

466
        $this->current->forgetColumn(/** @scrutinizer ignore-type */ $schema);
Loading history...
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);
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

484
        $this->current->forgetIndex(/** @scrutinizer ignore-type */ $schema);
Loading history...
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