Completed
Push — develop ( 600d79...d1c669 )
by Sergei
117:04 queued 52:01
created

Table::addNamedForeignKeyConstraint()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 25
ccs 14
cts 14
cp 1
rs 9.2222
c 0
b 0
f 0
cc 6
nc 7
nop 5
crap 6

1 Method

Rating   Name   Duplication   Size   Complexity  
A Table::getForeignKey() 0 9 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Schema;
6
7
use Doctrine\DBAL\DBALException;
8
use Doctrine\DBAL\Schema\Exception\ColumnAlreadyExists;
9
use Doctrine\DBAL\Schema\Exception\ColumnDoesNotExist;
10
use Doctrine\DBAL\Schema\Exception\ForeignKeyDoesNotExist;
11
use Doctrine\DBAL\Schema\Exception\IndexAlreadyExists;
12
use Doctrine\DBAL\Schema\Exception\IndexDoesNotExist;
13
use Doctrine\DBAL\Schema\Exception\IndexNameInvalid;
14
use Doctrine\DBAL\Schema\Exception\InvalidTableName;
15
use Doctrine\DBAL\Schema\Exception\UniqueConstraintDoesNotExist;
16
use Doctrine\DBAL\Schema\Visitor\Visitor;
17
use Doctrine\DBAL\Types\Type;
18
use function array_keys;
19
use function array_merge;
20
use function array_search;
21
use function array_unique;
22
use function in_array;
23
use function is_string;
24
use function preg_match;
25
use function sprintf;
26
use function strlen;
27
use function strtolower;
28
use function uksort;
29
30
/**
31
 * Object Representation of a table.
32
 */
33
class Table extends AbstractAsset
34
{
35
    /** @var Column[] */
36
    protected $_columns = [];
37
38
    /** @var Index[] */
39
    private $implicitIndexes = [];
40
41
    /** @var Index[] */
42
    protected $_indexes = [];
43
44
    /** @var string */
45
    protected $_primaryKeyName = false;
46
47
    /** @var UniqueConstraint[] */
48
    protected $_uniqueConstraints = [];
49
50
    /** @var ForeignKeyConstraint[] */
51
    protected $_fkConstraints = [];
52
53
    /** @var mixed[] */
54
    protected $_options = [];
55
56
    /** @var SchemaConfig|null */
57
    protected $_schemaConfig = null;
58
59
    /**
60
     * @param string                 $tableName
61
     * @param Column[]               $columns
62
     * @param Index[]                $indexes
63
     * @param UniqueConstraint[]     $uniqueConstraints
64
     * @param ForeignKeyConstraint[] $fkConstraints
65
     * @param mixed[]                $options
66
     *
67
     * @throws DBALException
68 19176
     */
69
    public function __construct(
70
        $tableName,
71
        array $columns = [],
72
        array $indexes = [],
73
        array $uniqueConstraints = [],
74
        array $fkConstraints = [],
75
        array $options = []
76 19176
    ) {
77 1627
        if (strlen($tableName) === 0) {
78
            throw InvalidTableName::new($tableName);
79
        }
80 19174
81
        $this->_setName($tableName);
82 19174
83 17055
        foreach ($columns as $column) {
84
            $this->_addColumn($column);
85
        }
86 19172
87 16959
        foreach ($indexes as $idx) {
88
            $this->_addIndex($idx);
89
        }
90 19168
91 202
        foreach ($uniqueConstraints as $uniqueConstraint) {
92
            $this->_addUniqueConstraint($uniqueConstraint);
93
        }
94 19168
95 15912
        foreach ($fkConstraints as $fkConstraint) {
96
            $this->_addForeignKeyConstraint($fkConstraint);
97
        }
98 19168
99 19168
        $this->_options = $options;
100
    }
101
102
    /**
103
     * @return void
104 17626
     */
105
    public function setSchemaConfig(SchemaConfig $schemaConfig)
106 17626
    {
107 17626
        $this->_schemaConfig = $schemaConfig;
108
    }
109
110
    /**
111
     * Sets the Primary Key.
112
     *
113
     * @param string[]     $columnNames
114
     * @param string|false $indexName
115
     *
116
     * @return self
117 18374
     */
118
    public function setPrimaryKey(array $columnNames, $indexName = false)
119 18374
    {
120
        $this->_addIndex($this->_createIndex($columnNames, $indexName ?: 'primary', true, true));
121 18374
122 18374
        foreach ($columnNames as $columnName) {
123 18374
            $column = $this->getColumn($columnName);
124
            $column->setNotnull(true);
125
        }
126 18374
127
        return $this;
128
    }
129
130
    /**
131
     * @param mixed[]     $columnNames
132
     * @param string|null $indexName
133
     * @param string[]    $flags
134
     * @param mixed[]     $options
135
     *
136
     * @return self
137
     */
138
    public function addUniqueConstraint(array $columnNames, $indexName = null, array $flags = [], array $options = [])
139
    {
140
        if ($indexName === null) {
141
            $indexName = $this->_generateIdentifierName(
142
                array_merge([$this->getName()], $columnNames),
143
                'uniq',
144
                $this->_getMaxIdentifierLength()
145
            );
146
        }
147
148
        return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options));
149
    }
150
151
    /**
152
     * @param string[]    $columnNames
153
     * @param string|null $indexName
154
     * @param string[]    $flags
155
     * @param mixed[]     $options
156
     *
157
     * @return self
158 16577
     */
159
    public function addIndex(array $columnNames, $indexName = null, array $flags = [], array $options = [])
160 16577
    {
161 16278
        if ($indexName === null) {
162 16278
            $indexName = $this->_generateIdentifierName(
163 16278
                array_merge([$this->getName()], $columnNames),
164 16278
                'idx',
165
                $this->_getMaxIdentifierLength()
166
            );
167
        }
168 16577
169
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
170
    }
171
172
    /**
173
     * Drops the primary key from this table.
174
     *
175
     * @return void
176 16160
     */
177
    public function dropPrimaryKey()
178 16160
    {
179 16160
        $this->dropIndex($this->_primaryKeyName);
180 16160
        $this->_primaryKeyName = false;
0 ignored issues
show
Documentation Bug introduced by
The property $_primaryKeyName was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
181
    }
182
183
    /**
184
     * Drops an index from this table.
185
     *
186
     * @param string $indexName The index name.
187
     *
188
     * @return void
189
     *
190
     * @throws SchemaException If the index does not exist.
191 16191
     */
192
    public function dropIndex($indexName)
193 16191
    {
194
        $indexName = $this->normalizeIdentifier($indexName);
195 16191
196
        if (! $this->hasIndex($indexName)) {
197
            throw IndexDoesNotExist::new($indexName, $this->_name);
198
        }
199 16191
200 16191
        unset($this->_indexes[$indexName]);
201
    }
202
203
    /**
204
     * @param string[]    $columnNames
205
     * @param string|null $indexName
206
     * @param mixed[]     $options
207
     *
208
     * @return self
209 17471
     */
210
    public function addUniqueIndex(array $columnNames, $indexName = null, array $options = [])
211 17471
    {
212 17453
        if ($indexName === null) {
213 17453
            $indexName = $this->_generateIdentifierName(
214 17453
                array_merge([$this->getName()], $columnNames),
215 17453
                'uniq',
216
                $this->_getMaxIdentifierLength()
217
            );
218
        }
219 17471
220
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
221
    }
222
223
    /**
224
     * Renames an index.
225
     *
226
     * @param string      $oldIndexName The name of the index to rename from.
227
     * @param string|null $newIndexName The name of the index to rename to.
228
     *                                  If null is given, the index name will be auto-generated.
229
     *
230
     * @return self This table instance.
231
     *
232
     * @throws SchemaException If no index exists for the given current name
233
     *                         or if an index with the given new name already exists on this table.
234 15015
     */
235
    public function renameIndex($oldIndexName, $newIndexName = null)
236 15015
    {
237 15015
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
238
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
239 15015
240 493
        if ($oldIndexName === $normalizedNewIndexName) {
241
            return $this;
242
        }
243 15015
244 402
        if (! $this->hasIndex($oldIndexName)) {
245
            throw IndexDoesNotExist::new($oldIndexName, $this->_name);
246
        }
247 15013
248 377
        if ($this->hasIndex($normalizedNewIndexName)) {
249
            throw IndexAlreadyExists::new($normalizedNewIndexName, $this->_name);
250
        }
251 15011
252
        $oldIndex = $this->_indexes[$oldIndexName];
253 15011
254 477
        if ($oldIndex->isPrimary()) {
255
            $this->dropPrimaryKey();
256 477
257
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? false);
258
        }
259 15011
260
        unset($this->_indexes[$oldIndexName]);
261 15011
262 479
        if ($oldIndex->isUnique()) {
263
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
264
        }
265 15009
266
        return $this->addIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getFlags(), $oldIndex->getOptions());
267
    }
268
269
    /**
270
     * Checks if an index begins in the order of the given columns.
271
     *
272
     * @param string[] $columnNames
273
     *
274
     * @return bool
275 15188
     */
276
    public function columnsAreIndexed(array $columnNames)
277 15188
    {
278
        foreach ($this->getIndexes() as $index) {
279 15188
            /** @var $index Index */
280 15188
            if ($index->spansColumns($columnNames)) {
281
                return true;
282
            }
283
        }
284
285
        return false;
286
    }
287
288
    /**
289
     * @param string  $columnName
290
     * @param string  $typeName
291
     * @param mixed[] $options
292
     *
293
     * @return Column
294 18934
     */
295
    public function addColumn($columnName, $typeName, array $options = [])
296 18934
    {
297
        $column = new Column($columnName, Type::getType($typeName), $options);
298 18934
299
        $this->_addColumn($column);
300 18934
301
        return $column;
302
    }
303
304
    /**
305
     * Change Column Details.
306
     *
307
     * @param string  $columnName
308
     * @param mixed[] $options
309
     *
310
     * @return self
311
     */
312
    public function changeColumn($columnName, array $options)
313
    {
314
        $column = $this->getColumn($columnName);
315
316
        $column->setOptions($options);
317
318
        return $this;
319
    }
320
321
    /**
322
     * Drops a Column from the Table.
323
     *
324
     * @param string $columnName
325
     *
326
     * @return self
327
     */
328
    public function dropColumn($columnName)
329
    {
330 15439
        $columnName = $this->normalizeIdentifier($columnName);
331
332 15439
        unset($this->_columns[$columnName]);
333
334 15439
        return $this;
335
    }
336 15439
337
    /**
338
     * Adds a foreign key constraint.
339
     *
340
     * Name is inferred from the local columns.
341
     *
342
     * @param Table|string $foreignTable       Table schema instance or table name
343
     * @param string[]     $localColumnNames
344
     * @param string[]     $foreignColumnNames
345
     * @param mixed[]      $options
346 1518
     * @param string|null  $name
347
     *
348 1518
     * @return self
349
     */
350 1518
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], $name = null)
351
    {
352 1518
        if (! $name) {
353
            $name = $this->_generateIdentifierName(
354
                array_merge((array) $this->getName(), $localColumnNames),
355
                'fk',
356
                $this->_getMaxIdentifierLength()
357
            );
358
        }
359
360
        if ($foreignTable instanceof Table) {
361
            foreach ($foreignColumnNames as $columnName) {
362
                if (! $foreignTable->hasColumn($columnName)) {
363
                    throw ColumnDoesNotExist::new($columnName, $foreignTable->getName());
364
                }
365
            }
366
        }
367
368 17641
        foreach ($localColumnNames as $columnName) {
369
            if (! $this->hasColumn($columnName)) {
370 17641
                throw ColumnDoesNotExist::new($columnName, $this->_name);
371 17549
            }
372 17549
        }
373 17549
374 17549
        $constraint = new ForeignKeyConstraint(
375
            $localColumnNames,
376
            $foreignTable,
377
            $foreignColumnNames,
378 17641
            $name,
379
            $options
380
        );
381
382
        return $this->_addForeignKeyConstraint($constraint);
383
    }
384
385
    /**
386
     * @param string $name
387
     * @param mixed  $value
388
     *
389
     * @return self
390
     */
391
    public function addOption($name, $value)
392
    {
393
        $this->_options[$name] = $value;
394
395 10058
        return $this;
396
    }
397 10058
398
    /**
399
     * Returns whether this table has a foreign key constraint with the given name.
400
     *
401
     * @param string $constraintName
402
     *
403
     * @return bool
404
     */
405
    public function hasForeignKey($constraintName)
406
    {
407
        $constraintName = $this->normalizeIdentifier($constraintName);
408
409
        return isset($this->_fkConstraints[$constraintName]);
410
    }
411
412
    /**
413
     * Returns the foreign key constraint with the given name.
414
     *
415 17643
     * @param string $constraintName The constraint name.
416
     *
417 17643
     * @return ForeignKeyConstraint
418 17529
     *
419 17529
     * @throws SchemaException If the foreign key does not exist.
420 1142
     */
421
    public function getForeignKey($constraintName)
422
    {
423
        $constraintName = $this->normalizeIdentifier($constraintName);
424
425 17641
        if (! $this->hasForeignKey($constraintName)) {
426 17641
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
427 1255
        }
428
429
        return $this->_fkConstraints[$constraintName];
430
    }
431 17639
432 17639
    /**
433 178
     * Removes the foreign key constraint with the given name.
434 178
     *
435 178
     * @param string $constraintName The constraint name.
436 178
     *
437
     * @return void
438
     *
439 17639
     * @throws SchemaException
440
     */
441
    public function removeForeignKey($constraintName)
442
    {
443
        $constraintName = $this->normalizeIdentifier($constraintName);
444
445
        if (! $this->hasForeignKey($constraintName)) {
446
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
447
        }
448 16311
449
        unset($this->_fkConstraints[$constraintName]);
450 16311
    }
451
452 16311
    /**
453
     * Returns whether this table has a unique constraint with the given name.
454
     *
455
     * @param string $constraintName
456
     *
457
     * @return bool
458
     */
459
    public function hasUniqueConstraint($constraintName)
460
    {
461
        $constraintName = $this->normalizeIdentifier($constraintName);
462 15006
463
        return isset($this->_uniqueConstraints[$constraintName]);
464 15006
    }
465
466 15006
    /**
467
     * Returns the unique constraint with the given name.
468
     *
469
     * @param string $constraintName The constraint name.
470
     *
471
     * @return UniqueConstraint
472
     *
473
     * @throws SchemaException If the foreign key does not exist.
474
     */
475
    public function getUniqueConstraint($constraintName)
476
    {
477
        $constraintName = $this->normalizeIdentifier($constraintName);
478 368
479
        if (! $this->hasUniqueConstraint($constraintName)) {
480 368
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
481
        }
482 368
483
        return $this->_uniqueConstraints[$constraintName];
484
    }
485
486 368
    /**
487
     * Removes the unique constraint with the given name.
488
     *
489
     * @param string $constraintName The constraint name.
490
     *
491
     * @return void
492
     *
493
     * @throws SchemaException
494
     */
495
    public function removeUniqueConstraint($constraintName)
496
    {
497
        $constraintName = $this->normalizeIdentifier($constraintName);
498 370
499
        if (! $this->hasUniqueConstraint($constraintName)) {
500 370
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
501
        }
502 370
503
        unset($this->_uniqueConstraints[$constraintName]);
504
    }
505
506 370
    /**
507 370
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
508
     *
509
     * @return Column[]
510
     */
511
    public function getColumns()
512
    {
513
        $columns = $this->_columns;
514
        $pkCols  = [];
515
        $fkCols  = [];
516
517
        $primaryKey = $this->getPrimaryKey();
518
519
        if ($primaryKey !== null) {
520
            $pkCols = $primaryKey->getColumns();
521
        }
522
523
        foreach ($this->getForeignKeys() as $fk) {
524
            /** @var ForeignKeyConstraint $fk */
525
            $fkCols = array_merge($fkCols, $fk->getColumns());
526
        }
527
528
        $colNames = array_unique(array_merge($pkCols, $fkCols, array_keys($columns)));
529
530
        uksort($columns, static function ($a, $b) use ($colNames) {
531
            return array_search($a, $colNames) >= array_search($b, $colNames);
532
        });
533
534
        return $columns;
535
    }
536
537
    /**
538
     * Returns whether this table has a Column with the given name.
539
     *
540
     * @param string $columnName The column name.
541
     *
542
     * @return bool
543
     */
544
    public function hasColumn($columnName)
545
    {
546
        $columnName = $this->normalizeIdentifier($columnName);
547
548
        return isset($this->_columns[$columnName]);
549
    }
550
551
    /**
552
     * Returns the Column with the given name.
553
     *
554
     * @param string $columnName The column name.
555
     *
556
     * @return Column
557
     *
558
     * @throws SchemaException If the column does not exist.
559
     */
560
    public function getColumn($columnName)
561
    {
562
        $columnName = $this->normalizeIdentifier($columnName);
563
564
        if (! $this->hasColumn($columnName)) {
565
            throw ColumnDoesNotExist::new($columnName, $this->_name);
566
        }
567
568 18688
        return $this->_columns[$columnName];
569
    }
570 18688
571 18688
    /**
572 18688
     * Returns the primary key.
573
     *
574 18688
     * @return Index|null The primary key, or null if this Table has no primary key.
575
     */
576 18688
    public function getPrimaryKey()
577 18212
    {
578
        return $this->hasPrimaryKey()
579
            ? $this->getIndex($this->_primaryKeyName)
580 18688
            : null;
581
    }
582 17628
583
    /**
584
     * Returns the primary key columns.
585 18688
     *
586
     * @return string[]
587
     *
588 18366
     * @throws DBALException
589 18688
     */
590
    public function getPrimaryKeyColumns()
591 18688
    {
592
        $primaryKey = $this->getPrimaryKey();
593
594
        if ($primaryKey === null) {
595
            throw new DBALException(sprintf('Table "%s" has no primary key.', $this->getName()));
596
        }
597
598
        return $primaryKey->getColumns();
599
    }
600
601 18744
    /**
602
     * Returns whether this table has a primary key.
603 18744
     *
604
     * @return bool
605 18744
     */
606
    public function hasPrimaryKey()
607
    {
608
        return $this->_primaryKeyName && $this->hasIndex($this->_primaryKeyName);
609
    }
610
611
    /**
612
     * Returns whether this table has an Index with the given name.
613
     *
614
     * @param string $indexName The index name.
615
     *
616
     * @return bool
617 18534
     */
618
    public function hasIndex($indexName)
619 18534
    {
620
        $indexName = $this->normalizeIdentifier($indexName);
621 18534
622 1477
        return isset($this->_indexes[$indexName]);
623
    }
624
625 18532
    /**
626
     * Returns the Index with the given name.
627
     *
628
     * @param string $indexName The index name.
629
     *
630
     * @return Index
631
     *
632
     * @throws SchemaException If the index does not exist.
633 18698
     */
634
    public function getIndex($indexName)
635 18698
    {
636 18222
        $indexName = $this->normalizeIdentifier($indexName);
637 18698
638
        if (! $this->hasIndex($indexName)) {
639
            throw IndexDoesNotExist::new($indexName, $this->_name);
640
        }
641
642
        return $this->_indexes[$indexName];
643
    }
644
645
    /**
646
     * @return Index[]
647 16150
     */
648
    public function getIndexes()
649 16150
    {
650
        return $this->_indexes;
651 16150
    }
652
653
    /**
654
     * Returns the unique constraints.
655 16150
     *
656
     * @return UniqueConstraint[]
657
     */
658
    public function getUniqueConstraints()
659
    {
660
        return $this->_uniqueConstraints;
661
    }
662
663 18704
    /**
664
     * Returns the foreign key constraints.
665 18704
     *
666
     * @return ForeignKeyConstraint[]
667
     */
668
    public function getForeignKeys()
669
    {
670
        return $this->_fkConstraints;
671
    }
672
673
    /**
674
     * @param string $name
675 18326
     *
676
     * @return bool
677 18326
     */
678
    public function hasOption($name)
679 18326
    {
680
        return isset($this->_options[$name]);
681
    }
682
683
    /**
684
     * @param string $name
685
     *
686
     * @return mixed
687
     */
688
    public function getOption($name)
689
    {
690
        return $this->_options[$name];
691 18282
    }
692
693 18282
    /**
694
     * @return mixed[]
695 18282
     */
696 1352
    public function getOptions()
697
    {
698
        return $this->_options;
699 18280
    }
700
701
    /**
702
     * @return void
703
     */
704
    public function visit(Visitor $visitor)
705 18662
    {
706
        $visitor->acceptTable($this);
707 18662
708
        foreach ($this->getColumns() as $column) {
709
            $visitor->acceptColumn($this, $column);
710
        }
711
712
        foreach ($this->getIndexes() as $index) {
713
            $visitor->acceptIndex($this, $index);
714
        }
715 18442
716
        foreach ($this->getForeignKeys() as $constraint) {
717 18442
            $visitor->acceptForeignKey($this, $constraint);
718
        }
719
    }
720
721
    /**
722
     * Clone of a Table triggers a deep clone of all affected assets.
723
     *
724
     * @return void
725 18724
     */
726
    public function __clone()
727 18724
    {
728
        foreach ($this->_columns as $k => $column) {
729
            $this->_columns[$k] = clone $column;
730
        }
731
732
        foreach ($this->_indexes as $k => $index) {
733
            $this->_indexes[$k] = clone $index;
734
        }
735 16290
736
        foreach ($this->_fkConstraints as $k => $fk) {
737 16290
            $this->_fkConstraints[$k] = clone $fk;
738
            $this->_fkConstraints[$k]->setLocalTable($this);
739
        }
740
    }
741
742
    /**
743
     * @return int
744
     */
745 15731
    protected function _getMaxIdentifierLength()
746
    {
747 15731
        return $this->_schemaConfig instanceof SchemaConfig
748
            ? $this->_schemaConfig->getMaxIdentifierLength()
749
            : 63;
750
    }
751
752
    /**
753 18468
     * @return void
754
     *
755 18468
     * @throws SchemaException
756
     */
757
    protected function _addColumn(Column $column)
758
    {
759
        $columnName = $column->getName();
760
        $columnName = $this->normalizeIdentifier($columnName);
761 17534
762
        if (isset($this->_columns[$columnName])) {
763 17534
            throw ColumnAlreadyExists::new($this->getName(), $columnName);
764
        }
765 17534
766 17528
        $this->_columns[$columnName] = $column;
767
    }
768
769 17534
    /**
770 17514
     * Adds an index to the table.
771
     *
772
     * @return self
773 17534
     *
774 158
     * @throws SchemaException
775
     */
776 17534
    protected function _addIndex(Index $indexCandidate)
777
    {
778
        $indexName               = $indexCandidate->getName();
779
        $indexName               = $this->normalizeIdentifier($indexName);
780
        $replacedImplicitIndexes = [];
781
782
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
783 16832
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
784
                continue;
785 16832
            }
786 16830
787
            $replacedImplicitIndexes[] = $name;
788
        }
789 16832
790 16808
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
791
            ($this->_primaryKeyName !== false && $indexCandidate->isPrimary())
792
        ) {
793 16832
            throw IndexAlreadyExists::new($indexName, $this->_name);
794 15479
        }
795 15479
796
        foreach ($replacedImplicitIndexes as $name) {
797 16832
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
798
        }
799
800
        if ($indexCandidate->isPrimary()) {
801
            $this->_primaryKeyName = $indexName;
802 17849
        }
803
804 17849
        $this->_indexes[$indexName] = $indexCandidate;
805 17437
806 17849
        return $this;
807
    }
808
809
    /**
810
     * @return self
811
     */
812
    protected function _addUniqueConstraint(UniqueConstraint $constraint)
813
    {
814 18994
        $name = strlen($constraint->getName())
815
            ? $constraint->getName()
816 18994
            : $this->_generateIdentifierName(
817 18994
                array_merge((array) $this->getName(), $constraint->getColumns()),
818
                'fk',
819 18994
                $this->_getMaxIdentifierLength()
820 1452
            );
821
822
        $name = $this->normalizeIdentifier($name);
823 18994
824 18994
        $this->_uniqueConstraints[$name] = $constraint;
825
826
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
827
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
828
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
829
        $indexName = $this->_generateIdentifierName(
830
            array_merge([$this->getName()], $constraint->getColumns()),
831
            'idx',
832
            $this->_getMaxIdentifierLength()
833 18622
        );
834
835 18622
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
836 18622
837 18622
        foreach ($this->_indexes as $existingIndex) {
838
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
839 18622
                return $this;
840 13346
            }
841 13338
        }
842
843
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
844 758
845
        return $this;
846
    }
847 18622
848 18622
    /**
849
     * @return self
850 1329
     */
851
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint)
852
    {
853 18622
        $constraint->setLocalTable($this);
854 758
855
        $name = strlen($constraint->getName())
856
            ? $constraint->getName()
857 18622
            : $this->_generateIdentifierName(
858 18378
                array_merge((array) $this->getName(), $constraint->getLocalColumns()),
859
                'fk',
860
                $this->_getMaxIdentifierLength()
861 18622
            );
862
863 18622
        $name = $this->normalizeIdentifier($name);
864
865
        $this->_fkConstraints[$name] = $constraint;
866
867
        // add an explicit index on the foreign key columns.
868
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
869 202
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
870
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
871 202
        $indexName = $this->_generateIdentifierName(
872
            array_merge([$this->getName()], $constraint->getColumns()),
873 202
            'idx',
874 202
            $this->_getMaxIdentifierLength()
875 202
        );
876 202
877
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
878
879 202
        foreach ($this->_indexes as $existingIndex) {
880
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
881 202
                return $this;
882
            }
883
        }
884
885
        $this->_addIndex($indexCandidate);
886 202
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
887 202
888 202
        return $this;
889 202
    }
890
891
    /**
892 202
     * Normalizes a given identifier.
893
     *
894 202
     * Trims quotes and lowercases the given identifier.
895
     *
896
     * @param string|null $identifier The identifier to normalize.
897
     *
898
     * @return string The normalized identifier.
899
     */
900 202
    private function normalizeIdentifier($identifier)
901
    {
902 202
        if ($identifier === null) {
903
            return '';
904
        }
905
906
        return $this->trimQuotes(strtolower($identifier));
907
    }
908 17728
909
    /**
910 17728
     * @param mixed[] $columns
911
     * @param string  $indexName
912 17728
     * @param mixed[] $flags
913 17647
     * @param mixed[] $options
914 1848
     *
915 1848
     * @return UniqueConstraint
916 1848
     *
917 17728
     * @throws SchemaException
918
     */
919
    private function _createUniqueConstraint(array $columns, $indexName, array $flags = [], array $options = [])
920 17728
    {
921
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
922 17728
            throw IndexNameInvalid::new($indexName);
923
        }
924
925
        foreach ($columns as $index => $value) {
926
            if (is_string($index)) {
927
                $columnName = $index;
928 17728
            } else {
929 17728
                $columnName = $value;
930 17728
            }
931 17728
932
            if (! $this->hasColumn($columnName)) {
933
                throw ColumnDoesNotExist::new($columnName, $this->_name);
934 17728
            }
935
        }
936 17728
937 17676
        return new UniqueConstraint($indexName, $columns, $flags, $options);
938 15945
    }
939
940
    /**
941
     * @param mixed[]  $columns
942 17680
     * @param string   $indexName
943 17680
     * @param bool     $isUnique
944
     * @param bool     $isPrimary
945 17680
     * @param string[] $flags
946
     * @param mixed[]  $options
947
     *
948
     * @return Index
949
     *
950
     * @throws SchemaException
951
     */
952
    private function _createIndex(array $columns, $indexName, $isUnique, $isPrimary, array $flags = [], array $options = [])
953
    {
954
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
955
            throw IndexNameInvalid::new($indexName);
956
        }
957 19002
958
        foreach ($columns as $index => $value) {
959 19002
            if (is_string($index)) {
960 477
                $columnName = $index;
961
            } else {
962
                $columnName = $value;
963 19002
            }
964
965
            if (! $this->hasColumn($columnName)) {
966
                throw ColumnDoesNotExist::new($columnName, $this->_name);
967
            }
968
        }
969
970
        return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options);
971
    }
972
}
973