Failed Conditions
Pull Request — develop (#3348)
by Sergei
101:57 queued 37:03
created

Table::_createUniqueConstraint()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.2742

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 19
ccs 7
cts 9
cp 0.7778
rs 9.6111
c 0
b 0
f 0
cc 5
nc 6
nop 4
crap 5.2742
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 assert;
23
use function in_array;
24
use function is_string;
25
use function preg_match;
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 Column[]               $columns
61
     * @param Index[]                $indexes
62
     * @param UniqueConstraint[]     $uniqueConstraints
63
     * @param ForeignKeyConstraint[] $fkConstraints
64
     * @param mixed[]                $options
65
     *
66
     * @throws DBALException
67
     */
68 19176
    public function __construct(
69
        string $tableName,
70
        array $columns = [],
71
        array $indexes = [],
72
        array $uniqueConstraints = [],
73
        array $fkConstraints = [],
74
        array $options = []
75
    ) {
76 19176
        if (strlen($tableName) === 0) {
77 1627
            throw InvalidTableName::new($tableName);
78
        }
79
80 19174
        $this->_setName($tableName);
81
82 19174
        foreach ($columns as $column) {
83 17055
            $this->_addColumn($column);
84
        }
85
86 19172
        foreach ($indexes as $idx) {
87 16959
            $this->_addIndex($idx);
88
        }
89
90 19168
        foreach ($uniqueConstraints as $uniqueConstraint) {
91 202
            $this->_addUniqueConstraint($uniqueConstraint);
92
        }
93
94 19168
        foreach ($fkConstraints as $fkConstraint) {
95 15912
            $this->_addForeignKeyConstraint($fkConstraint);
96
        }
97
98 19168
        $this->_options = $options;
99 19168
    }
100
101
    public function getName() : string
102
    {
103
        $name = parent::getName();
104 17626
        assert(is_string($name));
105
106 17626
        return $name;
107 17626
    }
108
109
    public function setSchemaConfig(SchemaConfig $schemaConfig) : void
110
    {
111
        $this->_schemaConfig = $schemaConfig;
112
    }
113
114
    /**
115
     * Sets the Primary Key.
116
     *
117 18374
     * @param string[]     $columnNames
118
     * @param string|false $indexName
119 18374
     */
120
    public function setPrimaryKey(array $columnNames, $indexName = false) : self
121 18374
    {
122 18374
        $this->_addIndex($this->_createIndex($columnNames, $indexName ?: 'primary', true, true));
123 18374
124
        foreach ($columnNames as $columnName) {
125
            $column = $this->getColumn($columnName);
126 18374
            $column->setNotnull(true);
127
        }
128
129
        return $this;
130
    }
131
132
    /**
133
     * @param mixed[]  $columnNames
134
     * @param string[] $flags
135
     * @param mixed[]  $options
136
     */
137
    public function addUniqueConstraint(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) : self
138
    {
139
        if ($indexName === null) {
140
            $indexName = $this->_generateIdentifierName(
141
                array_merge([$this->getName()], $columnNames),
142
                'uniq',
143
                $this->_getMaxIdentifierLength()
144
            );
145
        }
146
147
        return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options));
148
    }
149
150
    /**
151
     * @param string[] $columnNames
152
     * @param string[] $flags
153
     * @param mixed[]  $options
154
     */
155
    public function addIndex(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) : self
156
    {
157
        if ($indexName === null) {
158 16577
            $indexName = $this->_generateIdentifierName(
159
                array_merge([$this->getName()], $columnNames),
160 16577
                'idx',
161 16278
                $this->_getMaxIdentifierLength()
162 16278
            );
163 16278
        }
164 16278
165
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
166
    }
167
168 16577
    /**
169
     * Drops the primary key from this table.
170
     */
171
    public function dropPrimaryKey() : void
172
    {
173
        $this->dropIndex($this->_primaryKeyName);
174
        $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...
175
    }
176 16160
177
    /**
178 16160
     * Drops an index from this table.
179 16160
     *
180 16160
     * @param string $indexName The index name.
181
     *
182
     * @throws SchemaException If the index does not exist.
183
     */
184
    public function dropIndex(string $indexName) : void
185
    {
186
        $indexName = $this->normalizeIdentifier($indexName);
187
188
        if (! $this->hasIndex($indexName)) {
189
            throw IndexDoesNotExist::new($indexName, $this->_name);
190
        }
191 16191
192
        unset($this->_indexes[$indexName]);
193 16191
    }
194
195 16191
    /**
196
     * @param string[] $columnNames
197
     * @param mixed[]  $options
198
     */
199 16191
    public function addUniqueIndex(array $columnNames, ?string $indexName = null, array $options = []) : self
200 16191
    {
201
        if ($indexName === null) {
202
            $indexName = $this->_generateIdentifierName(
203
                array_merge([$this->getName()], $columnNames),
204
                'uniq',
205
                $this->_getMaxIdentifierLength()
206
            );
207
        }
208
209 17471
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
210
    }
211 17471
212 17453
    /**
213 17453
     * Renames an index.
214 17453
     *
215 17453
     * @param string      $oldIndexName The name of the index to rename from.
216
     * @param string|null $newIndexName The name of the index to rename to.
217
     *                                  If null is given, the index name will be auto-generated.
218
     *
219 17471
     * @return self This table instance.
220
     *
221
     * @throws SchemaException If no index exists for the given current name
222
     *                         or if an index with the given new name already exists on this table.
223
     */
224
    public function renameIndex(string $oldIndexName, ?string $newIndexName = null) : self
225
    {
226
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
227
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
228
229
        if ($oldIndexName === $normalizedNewIndexName) {
230
            return $this;
231
        }
232
233
        if (! $this->hasIndex($oldIndexName)) {
234 15015
            throw IndexDoesNotExist::new($oldIndexName, $this->_name);
235
        }
236 15015
237 15015
        if ($this->hasIndex($normalizedNewIndexName)) {
238
            throw IndexAlreadyExists::new($normalizedNewIndexName, $this->_name);
239 15015
        }
240 493
241
        $oldIndex = $this->_indexes[$oldIndexName];
242
243 15015
        if ($oldIndex->isPrimary()) {
244 402
            $this->dropPrimaryKey();
245
246
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? false);
247 15013
        }
248 377
249
        unset($this->_indexes[$oldIndexName]);
250
251 15011
        if ($oldIndex->isUnique()) {
252
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
253 15011
        }
254 477
255
        return $this->addIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getFlags(), $oldIndex->getOptions());
256 477
    }
257
258
    /**
259 15011
     * Checks if an index begins in the order of the given columns.
260
     *
261 15011
     * @param string[] $columnNames
262 479
     */
263
    public function columnsAreIndexed(array $columnNames) : bool
264
    {
265 15009
        foreach ($this->getIndexes() as $index) {
266
            /** @var $index Index */
267
            if ($index->spansColumns($columnNames)) {
268
                return true;
269
            }
270
        }
271
272
        return false;
273
    }
274
275 15188
    /**
276
     * @param mixed[] $options
277 15188
     */
278
    public function addColumn(string $columnName, string $typeName, array $options = []) : Column
279 15188
    {
280 15188
        $column = new Column($columnName, Type::getType($typeName), $options);
281
282
        $this->_addColumn($column);
283
284
        return $column;
285
    }
286
287
    /**
288
     * Renames a Column.
289
     *
290
     * @deprecated
291
     *
292
     * @throws DBALException
293
     */
294 18934
    public function renameColumn(string $oldColumnName, string $newColumnName) : void
0 ignored issues
show
Unused Code introduced by
The parameter $oldColumnName is not used and could be removed. ( Ignorable by Annotation )

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

294
    public function renameColumn(/** @scrutinizer ignore-unused */ string $oldColumnName, string $newColumnName) : void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $newColumnName is not used and could be removed. ( Ignorable by Annotation )

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

294
    public function renameColumn(string $oldColumnName, /** @scrutinizer ignore-unused */ string $newColumnName) : void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
295
    {
296 18934
        throw new DBALException(
297
            'Table#renameColumn() was removed, because it drops and recreates the column instead. ' .
298 18934
            'There is no fix available, because a schema diff cannot reliably detect if a column ' .
299
            'was renamed or one column was created and another one dropped.'
300 18934
        );
301
    }
302
303
    /**
304
     * Change Column Details.
305
     *
306
     * @param mixed[] $options
307
     */
308
    public function changeColumn(string $columnName, array $options) : self
309
    {
310
        $column = $this->getColumn($columnName);
311
312
        $column->setOptions($options);
313
314
        return $this;
315
    }
316
317
    /**
318
     * Drops a Column from the Table.
319
     */
320
    public function dropColumn(string $columnName) : self
321
    {
322
        $columnName = $this->normalizeIdentifier($columnName);
323
324
        unset($this->_columns[$columnName]);
325
326
        return $this;
327
    }
328
329
    /**
330 15439
     * Adds a foreign key constraint.
331
     *
332 15439
     * Name is inferred from the local columns.
333
     *
334 15439
     * @param Table|string $foreignTable       Table schema instance or table name
335
     * @param string[]     $localColumnNames
336 15439
     * @param string[]     $foreignColumnNames
337
     * @param mixed[]      $options
338
     */
339
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], ?string $constraintName = null) : self
340
    {
341
        if (! $constraintName) {
342
            $constraintName = $this->_generateIdentifierName(
343
                array_merge((array) $this->getName(), $localColumnNames),
344
                'fk',
345
                $this->_getMaxIdentifierLength()
346 1518
            );
347
        }
348 1518
349
        return $this->addNamedForeignKeyConstraint($constraintName, $foreignTable, $localColumnNames, $foreignColumnNames, $options);
1 ignored issue
show
Deprecated Code introduced by
The function Doctrine\DBAL\Schema\Tab...dForeignKeyConstraint() has been deprecated: Use {@link addForeignKeyConstraint} ( Ignorable by Annotation )

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

349
        return /** @scrutinizer ignore-deprecated */ $this->addNamedForeignKeyConstraint($constraintName, $foreignTable, $localColumnNames, $foreignColumnNames, $options);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
350 1518
    }
351
352 1518
    /**
353
     * Adds a foreign key constraint.
354
     *
355
     * Name is to be generated by the database itself.
356
     *
357
     * @deprecated Use {@link addForeignKeyConstraint}
358
     *
359
     * @param Table|string $foreignTable       Table schema instance or table name
360
     * @param string[]     $localColumnNames
361
     * @param string[]     $foreignColumnNames
362
     * @param mixed[]      $options
363
     */
364
    public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = []) : self
365
    {
366
        return $this->addForeignKeyConstraint($foreignTable, $localColumnNames, $foreignColumnNames, $options);
367
    }
368 17641
369
    /**
370 17641
     * Adds a foreign key constraint with a given name.
371 17549
     *
372 17549
     * @deprecated Use {@link addForeignKeyConstraint}
373 17549
     *
374 17549
     * @param Table|string $foreignTable       Table schema instance or table name
375
     * @param string[]     $localColumnNames
376
     * @param string[]     $foreignColumnNames
377
     * @param mixed[]      $options
378 17641
     *
379
     * @throws SchemaException
380
     */
381
    public function addNamedForeignKeyConstraint(string $name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = []) : self
382
    {
383
        if ($foreignTable instanceof Table) {
384
            foreach ($foreignColumnNames as $columnName) {
385
                if (! $foreignTable->hasColumn($columnName)) {
386
                    throw ColumnDoesNotExist::new($columnName, $foreignTable->getName());
387
                }
388
            }
389
        }
390
391
        foreach ($localColumnNames as $columnName) {
392
            if (! $this->hasColumn($columnName)) {
393
                throw ColumnDoesNotExist::new($columnName, $this->_name);
394
            }
395 10058
        }
396
397 10058
        $constraint = new ForeignKeyConstraint(
398
            $localColumnNames,
399
            $foreignTable,
400
            $foreignColumnNames,
401
            $name,
402
            $options
403
        );
404
405
        return $this->_addForeignKeyConstraint($constraint);
406
    }
407
408
    /**
409
     * @param mixed $value
410
     */
411
    public function addOption(string $name, $value) : self
412
    {
413
        $this->_options[$name] = $value;
414
415 17643
        return $this;
416
    }
417 17643
418 17529
    /**
419 17529
     * Returns whether this table has a foreign key constraint with the given name.
420 1142
     */
421
    public function hasForeignKey(string $constraintName) : bool
422
    {
423
        $constraintName = $this->normalizeIdentifier($constraintName);
424
425 17641
        return isset($this->_fkConstraints[$constraintName]);
426 17641
    }
427 1255
428
    /**
429
     * Returns the foreign key constraint with the given name.
430
     *
431 17639
     * @param string $constraintName The constraint name.
432 17639
     *
433 178
     * @throws SchemaException If the foreign key does not exist.
434 178
     */
435 178
    public function getForeignKey(string $constraintName) : ForeignKeyConstraint
436 178
    {
437
        $constraintName = $this->normalizeIdentifier($constraintName);
438
439 17639
        if (! $this->hasForeignKey($constraintName)) {
440
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
441
        }
442
443
        return $this->_fkConstraints[$constraintName];
444
    }
445
446
    /**
447
     * Removes the foreign key constraint with the given name.
448 16311
     *
449
     * @param string $constraintName The constraint name.
450 16311
     *
451
     * @throws SchemaException
452 16311
     */
453
    public function removeForeignKey(string $constraintName) : void
454
    {
455
        $constraintName = $this->normalizeIdentifier($constraintName);
456
457
        if (! $this->hasForeignKey($constraintName)) {
458
            throw ForeignKeyDoesNotExist::new($constraintName, $this->_name);
459
        }
460
461
        unset($this->_fkConstraints[$constraintName]);
462 15006
    }
463
464 15006
    /**
465
     * Returns whether this table has a unique constraint with the given name.
466 15006
     */
467
    public function hasUniqueConstraint(string $constraintName) : bool
468
    {
469
        $constraintName = $this->normalizeIdentifier($constraintName);
470
471
        return isset($this->_uniqueConstraints[$constraintName]);
472
    }
473
474
    /**
475
     * Returns the unique constraint with the given name.
476
     *
477
     * @param string $constraintName The constraint name.
478 368
     *
479
     * @throws SchemaException If the foreign key does not exist.
480 368
     */
481
    public function getUniqueConstraint(string $constraintName) : UniqueConstraint
482 368
    {
483
        $constraintName = $this->normalizeIdentifier($constraintName);
484
485
        if (! $this->hasUniqueConstraint($constraintName)) {
486 368
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
487
        }
488
489
        return $this->_uniqueConstraints[$constraintName];
490
    }
491
492
    /**
493
     * Removes the unique constraint with the given name.
494
     *
495
     * @param string $constraintName The constraint name.
496
     *
497
     * @throws SchemaException
498 370
     */
499
    public function removeUniqueConstraint(string $constraintName) : void
500 370
    {
501
        $constraintName = $this->normalizeIdentifier($constraintName);
502 370
503
        if (! $this->hasUniqueConstraint($constraintName)) {
504
            throw UniqueConstraintDoesNotExist::new($constraintName, $this->_name);
505
        }
506 370
507 370
        unset($this->_uniqueConstraints[$constraintName]);
508
    }
509
510
    /**
511
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
512
     *
513
     * @return Column[]
514
     */
515
    public function getColumns() : array
516
    {
517
        $columns = $this->_columns;
518
        $pkCols  = [];
519
        $fkCols  = [];
520
521
        $primaryKey = $this->getPrimaryKey();
522
523
        if ($primaryKey !== null) {
524
            $pkCols = $primaryKey->getColumns();
525
        }
526
527
        foreach ($this->getForeignKeys() as $fk) {
528
            /** @var ForeignKeyConstraint $fk */
529
            $fkCols = array_merge($fkCols, $fk->getColumns());
530
        }
531
532
        $colNames = array_unique(array_merge($pkCols, $fkCols, array_keys($columns)));
533
534
        uksort($columns, static function ($a, $b) use ($colNames) : bool {
535
            return array_search($a, $colNames) >= array_search($b, $colNames);
536
        });
537
538
        return $columns;
539
    }
540
541
    /**
542
     * Returns whether this table has a Column with the given name.
543
     *
544
     * @param string $columnName The column name.
545
     */
546
    public function hasColumn(string $columnName) : bool
547
    {
548
        $columnName = $this->normalizeIdentifier($columnName);
549
550
        return isset($this->_columns[$columnName]);
551
    }
552
553
    /**
554
     * Returns the Column with the given name.
555
     *
556
     * @param string $columnName The column name.
557
     *
558
     * @throws SchemaException If the column does not exist.
559
     */
560
    public function getColumn(string $columnName) : Column
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() : ?Index
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() : array
591 18688
    {
592
        $primaryKey = $this->getPrimaryKey();
593
594
        if ($primaryKey === null) {
595
            throw new DBALException('Table ' . $this->getName() . ' has no primary key.');
596
        }
597
598
        return $primaryKey->getColumns();
599
    }
600
601 18744
    /**
602
     * Returns whether this table has a primary key.
603 18744
     */
604
    public function hasPrimaryKey() : bool
605 18744
    {
606
        return $this->_primaryKeyName && $this->hasIndex($this->_primaryKeyName);
607
    }
608
609
    /**
610
     * Returns whether this table has an Index with the given name.
611
     *
612
     * @param string $indexName The index name.
613
     */
614
    public function hasIndex(string $indexName) : bool
615
    {
616
        $indexName = $this->normalizeIdentifier($indexName);
617 18534
618
        return isset($this->_indexes[$indexName]);
619 18534
    }
620
621 18534
    /**
622 1477
     * Returns the Index with the given name.
623
     *
624
     * @param string $indexName The index name.
625 18532
     *
626
     * @throws SchemaException If the index does not exist.
627
     */
628
    public function getIndex(string $indexName) : Index
629
    {
630
        $indexName = $this->normalizeIdentifier($indexName);
631
632
        if (! $this->hasIndex($indexName)) {
633 18698
            throw IndexDoesNotExist::new($indexName, $this->_name);
634
        }
635 18698
636 18222
        return $this->_indexes[$indexName];
637 18698
    }
638
639
    /**
640
     * @return Index[]
641
     */
642
    public function getIndexes() : array
643
    {
644
        return $this->_indexes;
645
    }
646
647 16150
    /**
648
     * Returns the unique constraints.
649 16150
     *
650
     * @return UniqueConstraint[]
651 16150
     */
652
    public function getUniqueConstraints() : array
653
    {
654
        return $this->_uniqueConstraints;
655 16150
    }
656
657
    /**
658
     * Returns the foreign key constraints.
659
     *
660
     * @return ForeignKeyConstraint[]
661
     */
662
    public function getForeignKeys() : array
663 18704
    {
664
        return $this->_fkConstraints;
665 18704
    }
666
667
    public function hasOption(string $name) : bool
668
    {
669
        return isset($this->_options[$name]);
670
    }
671
672
    /**
673
     * @return mixed
674
     */
675 18326
    public function getOption(string $name)
676
    {
677 18326
        return $this->_options[$name];
678
    }
679 18326
680
    /**
681
     * @return mixed[]
682
     */
683
    public function getOptions() : array
684
    {
685
        return $this->_options;
686
    }
687
688
    public function visit(Visitor $visitor) : void
689
    {
690
        $visitor->acceptTable($this);
691 18282
692
        foreach ($this->getColumns() as $column) {
693 18282
            $visitor->acceptColumn($this, $column);
694
        }
695 18282
696 1352
        foreach ($this->getIndexes() as $index) {
697
            $visitor->acceptIndex($this, $index);
698
        }
699 18280
700
        foreach ($this->getForeignKeys() as $constraint) {
701
            $visitor->acceptForeignKey($this, $constraint);
702
        }
703
    }
704
705 18662
    /**
706
     * Clone of a Table triggers a deep clone of all affected assets.
707 18662
     */
708
    public function __clone()
709
    {
710
        foreach ($this->_columns as $k => $column) {
711
            $this->_columns[$k] = clone $column;
712
        }
713
714
        foreach ($this->_indexes as $k => $index) {
715 18442
            $this->_indexes[$k] = clone $index;
716
        }
717 18442
718
        foreach ($this->_fkConstraints as $k => $fk) {
719
            $this->_fkConstraints[$k] = clone $fk;
720
            $this->_fkConstraints[$k]->setLocalTable($this);
721
        }
722
    }
723
724
    protected function _getMaxIdentifierLength() : int
725 18724
    {
726
        return $this->_schemaConfig instanceof SchemaConfig
727 18724
            ? $this->_schemaConfig->getMaxIdentifierLength()
728
            : 63;
729
    }
730
731
    /**
732
     * @throws SchemaException
733
     */
734
    protected function _addColumn(Column $column) : void
735 16290
    {
736
        $columnName = $column->getName();
737 16290
        $columnName = $this->normalizeIdentifier($columnName);
738
739
        if (isset($this->_columns[$columnName])) {
740
            throw ColumnAlreadyExists::new($this->getName(), $columnName);
741
        }
742
743
        $this->_columns[$columnName] = $column;
744
    }
745 15731
746
    /**
747 15731
     * Adds an index to the table.
748
     *
749
     * @throws SchemaException
750
     */
751
    protected function _addIndex(Index $indexCandidate) : self
752
    {
753 18468
        $indexName               = $indexCandidate->getName();
754
        $indexName               = $this->normalizeIdentifier($indexName);
755 18468
        $replacedImplicitIndexes = [];
756
757
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
758
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
759
                continue;
760
            }
761 17534
762
            $replacedImplicitIndexes[] = $name;
763 17534
        }
764
765 17534
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
766 17528
            ($this->_primaryKeyName !== false && $indexCandidate->isPrimary())
767
        ) {
768
            throw IndexAlreadyExists::new($indexName, $this->_name);
769 17534
        }
770 17514
771
        foreach ($replacedImplicitIndexes as $name) {
772
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
773 17534
        }
774 158
775
        if ($indexCandidate->isPrimary()) {
776 17534
            $this->_primaryKeyName = $indexName;
777
        }
778
779
        $this->_indexes[$indexName] = $indexCandidate;
780
781
        return $this;
782
    }
783 16832
784
    protected function _addUniqueConstraint(UniqueConstraint $constraint) : self
785 16832
    {
786 16830
        $name = $constraint->getName() ?? $this->_generateIdentifierName(
787
            array_merge([$this->getName()], $constraint->getColumns()),
788
            'fk',
789 16832
            $this->_getMaxIdentifierLength()
790 16808
        );
791
792
        $name = $this->normalizeIdentifier($name);
793 16832
794 15479
        $this->_uniqueConstraints[$name] = $constraint;
795 15479
796
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
797 16832
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
798
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
799
        $indexName = $this->_generateIdentifierName(
800
            array_merge([$this->getName()], $constraint->getColumns()),
801
            'idx',
802 17849
            $this->_getMaxIdentifierLength()
803
        );
804 17849
805 17437
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
806 17849
807
        foreach ($this->_indexes as $existingIndex) {
808
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
809
                return $this;
810
            }
811
        }
812
813
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
814 18994
815
        return $this;
816 18994
    }
817 18994
818
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint) : self
819 18994
    {
820 1452
        $constraint->setLocalTable($this);
821
822
        $name = $constraint->getName() ?? $this->_generateIdentifierName(
823 18994
            array_merge([$this->getName()], $constraint->getLocalColumns()),
824 18994
            'fk',
825
            $this->_getMaxIdentifierLength()
826
        );
827
828
        $name = $this->normalizeIdentifier($name);
829
830
        $this->_fkConstraints[$name] = $constraint;
831
832
        // add an explicit index on the foreign key columns.
833 18622
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
834
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
835 18622
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
836 18622
        $indexName = $this->_generateIdentifierName(
837 18622
            array_merge([$this->getName()], $constraint->getColumns()),
838
            'idx',
839 18622
            $this->_getMaxIdentifierLength()
840 13346
        );
841 13338
842
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
843
844 758
        foreach ($this->_indexes as $existingIndex) {
845
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
846
                return $this;
847 18622
            }
848 18622
        }
849
850 1329
        $this->_addIndex($indexCandidate);
851
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
852
853 18622
        return $this;
854 758
    }
855
856
    /**
857 18622
     * Normalizes a given identifier.
858 18378
     *
859
     * Trims quotes and lowercases the given identifier.
860
     *
861 18622
     * @param string|null $identifier The identifier to normalize.
862
     *
863 18622
     * @return string The normalized identifier.
864
     */
865
    private function normalizeIdentifier(?string $identifier) : string
866
    {
867
        if ($identifier === null) {
868
            return '';
869 202
        }
870
871 202
        return $this->trimQuotes(strtolower($identifier));
872
    }
873 202
874 202
    /**
875 202
     * @param mixed[] $columns
876 202
     * @param mixed[] $flags
877
     * @param mixed[] $options
878
     *
879 202
     * @throws SchemaException
880
     */
881 202
    private function _createUniqueConstraint(array $columns, string $indexName, array $flags = [], array $options = []) : UniqueConstraint
882
    {
883
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
884
            throw IndexNameInvalid::new($indexName);
885
        }
886 202
887 202
        foreach ($columns as $index => $value) {
888 202
            if (is_string($index)) {
889 202
                $columnName = $index;
890
            } else {
891
                $columnName = $value;
892 202
            }
893
894 202
            if (! $this->hasColumn($columnName)) {
895
                throw ColumnDoesNotExist::new($columnName, $this->_name);
896
            }
897
        }
898
899
        return new UniqueConstraint($indexName, $columns, $flags, $options);
900 202
    }
901
902 202
    /**
903
     * @param mixed[]  $columns
904
     * @param string[] $flags
905
     * @param mixed[]  $options
906
     *
907
     * @throws SchemaException
908 17728
     */
909
    private function _createIndex(array $columns, string $indexName, bool $isUnique, bool $isPrimary, array $flags = [], array $options = []) : Index
910 17728
    {
911
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName))) {
912 17728
            throw IndexNameInvalid::new($indexName);
913 17647
        }
914 1848
915 1848
        foreach ($columns as $index => $value) {
916 1848
            if (is_string($index)) {
917 17728
                $columnName = $index;
918
            } else {
919
                $columnName = $value;
920 17728
            }
921
922 17728
            if (! $this->hasColumn($columnName)) {
923
                throw ColumnDoesNotExist::new($columnName, $this->_name);
924
            }
925
        }
926
927
        return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options);
928 17728
    }
929
}
930