Failed Conditions
Pull Request — 3.0.x (#3980)
by Guilherme
14:00 queued 29s
created

Table::hasUniqueConstraint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace Doctrine\DBAL\Schema;
4
5
use Doctrine\DBAL\DBALException;
6
use Doctrine\DBAL\Schema\Visitor\Visitor;
7
use Doctrine\DBAL\Types\Type;
8
use const ARRAY_FILTER_USE_KEY;
9
use function array_filter;
10
use function array_keys;
11
use function array_merge;
12
use function in_array;
13
use function is_numeric;
14
use function is_string;
15
use function preg_match;
16
use function strlen;
17
use function strtolower;
18
19
/**
20
 * Object Representation of a table.
21
 */
22
class Table extends AbstractAsset
23
{
24
    /** @var Column[] */
25
    protected $_columns = [];
26
27
    /** @var Index[] */
28
    protected $_indexes = [];
29
30
    /** @var string|null */
31
    protected $_primaryKeyName = null;
32
33
    /** @var UniqueConstraint[] */
34
    protected $_uniqueConstraints = [];
35
36
    /** @var ForeignKeyConstraint[] */
37
    protected $_fkConstraints = [];
38
39
    /** @var mixed[] */
40
    protected $_options = [
41
        'create_options' => [],
42
    ];
43
44
    /** @var SchemaConfig|null */
45
    protected $_schemaConfig = null;
46
47
    /** @var Index[] */
48
    private $implicitIndexes = [];
49
50
    /**
51
     * @param string                 $tableName
52
     * @param Column[]               $columns
53
     * @param Index[]                $indexes
54
     * @param UniqueConstraint[]     $uniqueConstraints
55
     * @param ForeignKeyConstraint[] $fkConstraints
56
     * @param mixed[]                $options
57
     *
58
     * @throws DBALException
59
     */
60 14477
    public function __construct(
61
        $tableName,
62
        array $columns = [],
63
        array $indexes = [],
64
        array $uniqueConstraints = [],
65
        array $fkConstraints = [],
66
        array $options = []
67
    ) {
68 14477
        if (strlen($tableName) === 0) {
69 22
            throw DBALException::invalidTableName($tableName);
70
        }
71
72 14455
        $this->_setName($tableName);
73
74 14455
        foreach ($columns as $column) {
75 2062
            $this->_addColumn($column);
76
        }
77
78 14433
        foreach ($indexes as $idx) {
79 654
            $this->_addIndex($idx);
80
        }
81
82 14389
        foreach ($uniqueConstraints as $uniqueConstraint) {
83
            $this->_addUniqueConstraint($uniqueConstraint);
84
        }
85
86 14389
        foreach ($fkConstraints as $constraint) {
87 253
            $this->_addForeignKeyConstraint($constraint);
88
        }
89
90 14389
        $this->_options = array_merge($this->_options, $options);
91 14389
    }
92
93
    /**
94
     * @return void
95
     */
96 1443
    public function setSchemaConfig(SchemaConfig $schemaConfig)
97
    {
98 1443
        $this->_schemaConfig = $schemaConfig;
99 1443
    }
100
101
    /**
102
     * Sets the Primary Key.
103
     *
104
     * @param string[]     $columnNames
105
     * @param string|false $indexName
106
     *
107
     * @return self
108
     */
109 5827
    public function setPrimaryKey(array $columnNames, $indexName = false)
110
    {
111 5827
        if ($indexName === false) {
112 5805
            $indexName = 'primary';
113
        }
114
115 5827
        $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true));
116
117 5827
        foreach ($columnNames as $columnName) {
118 5827
            $column = $this->getColumn($columnName);
119 5827
            $column->setNotnull(true);
120
        }
121
122 5827
        return $this;
123
    }
124
125
    /**
126
     * @param string[] $columnNames
127
     * @param string[] $flags
128
     * @param mixed[]  $options
129
     *
130
     * @return self
131
     */
132 1815
    public function addIndex(array $columnNames, ?string $indexName = null, array $flags = [], array $options = [])
133
    {
134 1815
        if ($indexName === null) {
135 393
            $indexName = $this->_generateIdentifierName(
136 393
                array_merge([$this->getName()], $columnNames),
137 393
                'idx',
138 393
                $this->_getMaxIdentifierLength()
139
            );
140
        }
141
142 1815
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
143
    }
144
145
    /**
146
     * @param string[] $columnNames
147
     * @param string[] $flags
148
     * @param mixed[]  $options
149
     *
150
     * @return self
151
     */
152
    public function addUniqueConstraint(array $columnNames, ?string $indexName = null, array $flags = [], array $options = [])
153
    {
154
        if ($indexName === null) {
155
            $indexName = $this->_generateIdentifierName(
156
                array_merge([$this->getName()], $columnNames),
157
                'uniq',
158
                $this->_getMaxIdentifierLength()
159
            );
160
        }
161
162
        return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options));
163
    }
164
165
    /**
166
     * Drops the primary key from this table.
167
     *
168
     * @return void
169
     */
170 398
    public function dropPrimaryKey()
171
    {
172 398
        if ($this->_primaryKeyName === null) {
173
            return;
174
        }
175
176 398
        $this->dropIndex($this->_primaryKeyName);
177 398
        $this->_primaryKeyName = null;
178 398
    }
179
180
    /**
181
     * Drops an index from this table.
182
     *
183
     * @param string $indexName The index name.
184
     *
185
     * @return void
186
     *
187
     * @throws SchemaException If the index does not exist.
188
     */
189 674
    public function dropIndex($indexName)
190
    {
191 674
        $indexName = $this->normalizeIdentifier($indexName);
192
193 674
        if (! $this->hasIndex($indexName)) {
194
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
195
        }
196
197 674
        unset($this->_indexes[$indexName]);
198 674
    }
199
200
    /**
201
     * @param string[]    $columnNames
202
     * @param string|null $indexName
203
     * @param mixed[]     $options
204
     *
205
     * @return self
206
     */
207 543
    public function addUniqueIndex(array $columnNames, $indexName = null, array $options = [])
208
    {
209 543
        if ($indexName === null) {
210 361
            $indexName = $this->_generateIdentifierName(
211 361
                array_merge([$this->getName()], $columnNames),
212 361
                'uniq',
213 361
                $this->_getMaxIdentifierLength()
214
            );
215
        }
216
217 543
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
218
    }
219
220
    /**
221
     * Renames an index.
222
     *
223
     * @param string      $oldIndexName The name of the index to rename from.
224
     * @param string|null $newIndexName The name of the index to rename to.
225
     *                                  If null is given, the index name will be auto-generated.
226
     *
227
     * @return self This table instance.
228
     *
229
     * @throws SchemaException If no index exists for the given current name
230
     *                         or if an index with the given new name already exists on this table.
231
     */
232 307
    public function renameIndex($oldIndexName, $newIndexName = null)
233
    {
234 307
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
235 307
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
236
237 307
        if ($oldIndexName === $normalizedNewIndexName) {
238 198
            return $this;
239
        }
240
241 307
        if (! $this->hasIndex($oldIndexName)) {
242 22
            throw SchemaException::indexDoesNotExist($oldIndexName, $this->_name);
243
        }
244
245 285
        if ($this->hasIndex($normalizedNewIndexName)) {
246 22
            throw SchemaException::indexAlreadyExists($normalizedNewIndexName, $this->_name);
247
        }
248
249 263
        $oldIndex = $this->_indexes[$oldIndexName];
250
251 263
        if ($oldIndex->isPrimary()) {
252 22
            $this->dropPrimaryKey();
253
254 22
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? false);
255
        }
256
257 263
        unset($this->_indexes[$oldIndexName]);
258
259 263
        if ($oldIndex->isUnique()) {
260 44
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
261
        }
262
263 241
        return $this->addIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getFlags(), $oldIndex->getOptions());
264
    }
265
266
    /**
267
     * Checks if an index begins in the order of the given columns.
268
     *
269
     * @param string[] $columnNames
270
     *
271
     * @return bool
272
     */
273 43
    public function columnsAreIndexed(array $columnNames)
274
    {
275 43
        foreach ($this->getIndexes() as $index) {
276
            /** @var $index Index */
277 43
            if ($index->spansColumns($columnNames)) {
278 43
                return true;
279
            }
280
        }
281
282
        return false;
283
    }
284
285
    /**
286
     * @param string  $columnName
287
     * @param string  $typeName
288
     * @param mixed[] $options
289
     *
290
     * @return Column
291
     */
292 11636
    public function addColumn($columnName, $typeName, array $options = [])
293
    {
294 11636
        $column = new Column($columnName, Type::getType($typeName), $options);
295
296 11636
        $this->_addColumn($column);
297
298 11636
        return $column;
299
    }
300
301
    /**
302
     * Renames a Column.
303
     *
304
     * @deprecated
305
     *
306
     * @param string $oldColumnName
307
     * @param string $newColumnName
308
     *
309
     * @return void
310
     *
311
     * @throws DBALException
312
     */
313
    public function renameColumn($oldColumnName, $newColumnName)
0 ignored issues
show
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

313
    public function renameColumn($oldColumnName, /** @scrutinizer ignore-unused */ $newColumnName)

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 $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

313
    public function renameColumn(/** @scrutinizer ignore-unused */ $oldColumnName, $newColumnName)

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...
314
    {
315
        throw new DBALException(
316
            'Table#renameColumn() was removed, because it drops and recreates ' .
317
            'the column instead. There is no fix available, because a schema diff cannot reliably detect if a ' .
318
            'column was renamed or one column was created and another one dropped.'
319
        );
320
    }
321
322
    /**
323
     * Change Column Details.
324
     *
325
     * @param string  $columnName
326
     * @param mixed[] $options
327
     *
328
     * @return self
329
     */
330 265
    public function changeColumn($columnName, array $options)
331
    {
332 265
        $column = $this->getColumn($columnName);
333
334 265
        $column->setOptions($options);
335
336 265
        return $this;
337
    }
338
339
    /**
340
     * Drops a Column from the Table.
341
     *
342
     * @param string $columnName
343
     *
344
     * @return self
345
     */
346 198
    public function dropColumn($columnName)
347
    {
348 198
        $columnName = $this->normalizeIdentifier($columnName);
349
350 198
        unset($this->_columns[$columnName]);
351
352 198
        return $this;
353
    }
354
355
    /**
356
     * Adds a foreign key constraint.
357
     *
358
     * Name is inferred from the local columns.
359
     *
360
     * @param Table|string $foreignTable       Table schema instance or table name
361
     * @param string[]     $localColumnNames
362
     * @param string[]     $foreignColumnNames
363
     * @param mixed[]      $options
364
     * @param string|null  $constraintName
365
     *
366
     * @return self
367
     */
368 1812
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], $constraintName = null)
369
    {
370 1812
        if ($constraintName === null) {
371 954
            $constraintName = $this->_generateIdentifierName(array_merge((array) $this->getName(), $localColumnNames), 'fk', $this->_getMaxIdentifierLength());
372
        }
373
374 1812
        return $this->addNamedForeignKeyConstraint($constraintName, $foreignTable, $localColumnNames, $foreignColumnNames, $options);
375
    }
376
377
    /**
378
     * Adds a foreign key constraint.
379
     *
380
     * Name is to be generated by the database itself.
381
     *
382
     * @deprecated Use {@link addForeignKeyConstraint}
383
     *
384
     * @param Table|string $foreignTable       Table schema instance or table name
385
     * @param string[]     $localColumnNames
386
     * @param string[]     $foreignColumnNames
387
     * @param mixed[]      $options
388
     *
389
     * @return self
390
     */
391 71
    public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
392
    {
393 71
        return $this->addForeignKeyConstraint($foreignTable, $localColumnNames, $foreignColumnNames, $options);
394
    }
395
396
    /**
397
     * Adds a foreign key constraint with a given name.
398
     *
399
     * @deprecated Use {@link addForeignKeyConstraint}
400
     *
401
     * @param string       $name
402
     * @param Table|string $foreignTable       Table schema instance or table name
403
     * @param string[]     $localColumnNames
404
     * @param string[]     $foreignColumnNames
405
     * @param mixed[]      $options
406
     *
407
     * @return self
408
     *
409
     * @throws SchemaException
410
     */
411 1834
    public function addNamedForeignKeyConstraint($name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
412
    {
413 1834
        if ($foreignTable instanceof Table) {
414 923
            foreach ($foreignColumnNames as $columnName) {
415 923
                if (! $foreignTable->hasColumn($columnName)) {
416 22
                    throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName());
417
                }
418
            }
419
        }
420
421 1812
        foreach ($localColumnNames as $columnName) {
422 1812
            if (! $this->hasColumn($columnName)) {
423 22
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
424
            }
425
        }
426
427 1790
        $constraint = new ForeignKeyConstraint(
428 1790
            $localColumnNames,
429
            $foreignTable,
430
            $foreignColumnNames,
431
            $name,
432
            $options
433
        );
434
435 1790
        return $this->_addForeignKeyConstraint($constraint);
436
    }
437
438
    /**
439
     * @param string $name
440
     * @param mixed  $value
441
     *
442
     * @return self
443
     */
444 1737
    public function addOption($name, $value)
445
    {
446 1737
        $this->_options[$name] = $value;
447
448 1737
        return $this;
449
    }
450
451
    /**
452
     * Returns whether this table has a foreign key constraint with the given name.
453
     *
454
     * @param string $constraintName
455
     *
456
     * @return bool
457
     */
458 242
    public function hasForeignKey($constraintName)
459
    {
460 242
        $constraintName = $this->normalizeIdentifier($constraintName);
461
462 242
        return isset($this->_fkConstraints[$constraintName]);
463
    }
464
465
    /**
466
     * Returns the foreign key constraint with the given name.
467
     *
468
     * @param string $constraintName The constraint name.
469
     *
470
     * @return ForeignKeyConstraint
471
     *
472
     * @throws SchemaException If the foreign key does not exist.
473
     */
474 177
    public function getForeignKey($constraintName)
475
    {
476 177
        $constraintName = $this->normalizeIdentifier($constraintName);
477
478 177
        if (! $this->hasForeignKey($constraintName)) {
479
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
480
        }
481
482 177
        return $this->_fkConstraints[$constraintName];
483
    }
484
485
    /**
486
     * Removes the foreign key constraint with the given name.
487
     *
488
     * @param string $constraintName The constraint name.
489
     *
490
     * @return void
491
     *
492
     * @throws SchemaException
493
     */
494 220
    public function removeForeignKey($constraintName)
495
    {
496 220
        $constraintName = $this->normalizeIdentifier($constraintName);
497
498 220
        if (! $this->hasForeignKey($constraintName)) {
499
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
500
        }
501
502 220
        unset($this->_fkConstraints[$constraintName]);
503 220
    }
504
505
    /**
506
     * Returns whether this table has a unique constraint with the given name.
507
     *
508
     * @param string $constraintName
509
     *
510
     * @return bool
511
     */
512
    public function hasUniqueConstraint($constraintName)
513
    {
514
        $constraintName = $this->normalizeIdentifier($constraintName);
515
516
        return isset($this->_uniqueConstraints[$constraintName]);
517
    }
518
519
    /**
520
     * Returns the unique constraint with the given name.
521
     *
522
     * @param string $constraintName The constraint name.
523
     *
524
     * @return UniqueConstraint
525
     *
526
     * @throws SchemaException If the unique constraint does not exist.
527
     */
528
    public function getUniqueConstraint($constraintName)
529
    {
530
        $constraintName = $this->normalizeIdentifier($constraintName);
531
532
        if (! $this->hasForeignKey($constraintName)) {
533
            throw SchemaException::uniqueConstraintDoesNotExist($constraintName, $this->_name);
534
        }
535
536
        return $this->_uniqueConstraints[$constraintName];
537
    }
538
539
    /**
540
     * Removes the unique constraint with the given name.
541
     *
542
     * @param string $constraintName The constraint name.
543
     *
544
     * @return void
545
     *
546
     * @throws SchemaException
547
     */
548
    public function removeUniqueConstraint($constraintName)
549
    {
550
        $constraintName = $this->normalizeIdentifier($constraintName);
551
552
        if (! $this->hasForeignKey($constraintName)) {
553
            throw SchemaException::uniqueConstraintDoesNotExist($constraintName, $this->_name);
554
        }
555
556
        unset($this->_uniqueConstraints[$constraintName]);
557
    }
558
559
    /**
560
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
561
     *
562
     * @return Column[]
563
     */
564 9221
    public function getColumns()
565
    {
566 9221
        $primaryKeyColumns = $this->hasPrimaryKey() ? $this->getPrimaryKeyColumns() : [];
567 9221
        $foreignKeyColumns = $this->getForeignKeyColumns();
568 9221
        $remainderColumns  = $this->filterColumns(
569 9221
            array_merge(array_keys($primaryKeyColumns), array_keys($foreignKeyColumns)),
570 9221
            true
571
        );
572
573 9221
        return array_merge($primaryKeyColumns, $foreignKeyColumns, $remainderColumns);
574
    }
575
576
    /**
577
     * Returns whether this table has a Column with the given name.
578
     *
579
     * @param string $columnName The column name.
580
     *
581
     * @return bool
582
     */
583 10055
    public function hasColumn($columnName)
584
    {
585 10055
        $columnName = $this->normalizeIdentifier($columnName);
586
587 10055
        return isset($this->_columns[$columnName]);
588
    }
589
590
    /**
591
     * Returns the Column with the given name.
592
     *
593
     * @param string $columnName The column name.
594
     *
595
     * @return Column
596
     *
597
     * @throws SchemaException If the column does not exist.
598
     */
599 7958
    public function getColumn($columnName)
600
    {
601 7958
        $columnName = $this->normalizeIdentifier($columnName);
602
603 7958
        if (! $this->hasColumn($columnName)) {
604 22
            throw SchemaException::columnDoesNotExist($columnName, $this->_name);
605
        }
606
607 7936
        return $this->_columns[$columnName];
608
    }
609
610
    /**
611
     * Returns the primary key.
612
     *
613
     * @return Index|null The primary key, or null if this Table has no primary key.
614
     */
615 4446
    public function getPrimaryKey()
616
    {
617 4446
        return $this->_primaryKeyName !== null
618 4446
            ? $this->getIndex($this->_primaryKeyName)
619 4446
            : null;
620
    }
621
622
    /**
623
     * Returns the primary key columns.
624
     *
625
     * @return Column[]
626
     *
627
     * @throws DBALException
628
     */
629 4336
    public function getPrimaryKeyColumns()
630
    {
631 4336
        $primaryKey = $this->getPrimaryKey();
632
633 4336
        if ($primaryKey === null) {
634
            throw new DBALException('Table ' . $this->getName() . ' has no primary key.');
635
        }
636
637 4336
        return $this->filterColumns($primaryKey->getColumns());
638
    }
639
640
    /**
641
     * Returns the foreign key columns
642
     *
643
     * @return Column[]
644
     */
645 9221
    public function getForeignKeyColumns()
646
    {
647 9221
        $foreignKeyColumns = [];
648
649 9221
        foreach ($this->getForeignKeys() as $foreignKey) {
650 894
            $foreignKeyColumns = array_merge($foreignKeyColumns, $foreignKey->getLocalColumns());
651
        }
652
653 9221
        return $this->filterColumns($foreignKeyColumns);
654
    }
655
656
    /**
657
     * Returns whether this table has a primary key.
658
     *
659
     * @return bool
660
     */
661 9309
    public function hasPrimaryKey()
662
    {
663 9309
        return $this->_primaryKeyName !== null && $this->hasIndex($this->_primaryKeyName);
664
    }
665
666
    /**
667
     * Returns whether this table has an Index with the given name.
668
     *
669
     * @param string $indexName The index name.
670
     *
671
     * @return bool
672
     */
673 5619
    public function hasIndex($indexName)
674
    {
675 5619
        $indexName = $this->normalizeIdentifier($indexName);
676
677 5619
        return isset($this->_indexes[$indexName]);
678
    }
679
680
    /**
681
     * Returns the Index with the given name.
682
     *
683
     * @param string $indexName The index name.
684
     *
685
     * @return Index
686
     *
687
     * @throws SchemaException If the index does not exist.
688
     */
689 5135
    public function getIndex($indexName)
690
    {
691 5135
        $indexName = $this->normalizeIdentifier($indexName);
692 5135
        if (! $this->hasIndex($indexName)) {
693 22
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
694
        }
695
696 5113
        return $this->_indexes[$indexName];
697
    }
698
699
    /**
700
     * @return Index[]
701
     */
702 9023
    public function getIndexes()
703
    {
704 9023
        return $this->_indexes;
705
    }
706
707
    /**
708
     * Returns the unique constraints.
709
     *
710
     * @return UniqueConstraint[]
711
     */
712 6731
    public function getUniqueConstraints()
713
    {
714 6731
        return $this->_uniqueConstraints;
715
    }
716
717
    /**
718
     * Returns the foreign key constraints.
719
     *
720
     * @return ForeignKeyConstraint[]
721
     */
722 9617
    public function getForeignKeys()
723
    {
724 9617
        return $this->_fkConstraints;
725
    }
726
727
    /**
728
     * @param string $name
729
     *
730
     * @return bool
731
     */
732 4399
    public function hasOption($name)
733
    {
734 4399
        return isset($this->_options[$name]);
735
    }
736
737
    /**
738
     * @param string $name
739
     *
740
     * @return mixed
741
     */
742 221
    public function getOption($name)
743
    {
744 221
        return $this->_options[$name];
745
    }
746
747
    /**
748
     * @return mixed[]
749
     */
750 6995
    public function getOptions()
751
    {
752 6995
        return $this->_options;
753
    }
754
755
    /**
756
     * @return void
757
     */
758 342
    public function visit(Visitor $visitor)
759
    {
760 342
        $visitor->acceptTable($this);
761
762 342
        foreach ($this->getColumns() as $column) {
763 276
            $visitor->acceptColumn($this, $column);
764
        }
765
766 342
        foreach ($this->getIndexes() as $index) {
767 192
            $visitor->acceptIndex($this, $index);
768
        }
769
770 342
        foreach ($this->getForeignKeys() as $constraint) {
771 88
            $visitor->acceptForeignKey($this, $constraint);
772
        }
773 342
    }
774
775
    /**
776
     * Clone of a Table triggers a deep clone of all affected assets.
777
     *
778
     * @return void
779
     */
780 1052
    public function __clone()
781
    {
782 1052
        foreach ($this->_columns as $k => $column) {
783 1030
            $this->_columns[$k] = clone $column;
784
        }
785 1052
        foreach ($this->_indexes as $k => $index) {
786 742
            $this->_indexes[$k] = clone $index;
787
        }
788 1052
        foreach ($this->_fkConstraints as $k => $fk) {
789 175
            $this->_fkConstraints[$k] = clone $fk;
790 175
            $this->_fkConstraints[$k]->setLocalTable($this);
791
        }
792 1052
    }
793
794 44
    public function setComment(?string $comment) : self
795
    {
796
        // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options.
797 44
        $this->addOption('comment', $comment);
798
799 44
        return $this;
800
    }
801
802 44
    public function getComment() : ?string
803
    {
804 44
        return $this->_options['comment'] ?? null;
805
    }
806
807
    /**
808
     * @return int
809
     */
810 2628
    protected function _getMaxIdentifierLength()
811
    {
812 2628
        if ($this->_schemaConfig instanceof SchemaConfig) {
813 227
            return $this->_schemaConfig->getMaxIdentifierLength();
814
        }
815
816 2443
        return 63;
817
    }
818
819
    /**
820
     * @return void
821
     *
822
     * @throws SchemaException
823
     */
824 12617
    protected function _addColumn(Column $column)
825
    {
826 12617
        $columnName = $column->getName();
827 12617
        $columnName = $this->normalizeIdentifier($columnName);
828
829 12617
        if (isset($this->_columns[$columnName])) {
830 22
            throw SchemaException::columnAlreadyExists($this->getName(), $columnName);
831
        }
832
833 12617
        $this->_columns[$columnName] = $column;
834 12617
    }
835
836
    /**
837
     * Adds an index to the table.
838
     *
839
     * @return self
840
     *
841
     * @throws SchemaException
842
     */
843 8386
    protected function _addIndex(Index $indexCandidate)
844
    {
845 8386
        $indexName               = $indexCandidate->getName();
846 8386
        $indexName               = $this->normalizeIdentifier($indexName);
847 8386
        $replacedImplicitIndexes = [];
848
849 8386
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
850 550
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
851 462
                continue;
852
            }
853
854 88
            $replacedImplicitIndexes[] = $name;
855
        }
856
857 8386
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
858 8386
            ($this->_primaryKeyName !== null && $indexCandidate->isPrimary())
859
        ) {
860 44
            throw SchemaException::indexAlreadyExists($indexName, $this->_name);
861
        }
862
863 8386
        foreach ($replacedImplicitIndexes as $name) {
864 88
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
865
        }
866
867 8386
        if ($indexCandidate->isPrimary()) {
868 5893
            $this->_primaryKeyName = $indexName;
869
        }
870
871 8386
        $this->_indexes[$indexName] = $indexCandidate;
872
873 8386
        return $this;
874
    }
875
876
    /**
877
     * @return self
878
     */
879
    protected function _addUniqueConstraint(UniqueConstraint $constraint)
880
    {
881
        $mergedNames = array_merge([$this->getName()], $constraint->getColumns());
882
        $name        = strlen($constraint->getName()) > 0
883
            ? $constraint->getName()
884
            : $this->_generateIdentifierName($mergedNames, 'fk', $this->_getMaxIdentifierLength());
885
886
        $name = $this->normalizeIdentifier($name);
887
888
        $this->_uniqueConstraints[$name] = $constraint;
889
890
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
891
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
892
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
893
        $indexName = $this->_generateIdentifierName($mergedNames, 'idx', $this->_getMaxIdentifierLength());
894
895
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
896
897
        foreach ($this->_indexes as $existingIndex) {
898
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
899
                return $this;
900
            }
901
        }
902
903
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
904
905
        return $this;
906
    }
907
908
    /**
909
     * @return self
910
     */
911 1893
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint)
912
    {
913 1893
        $constraint->setLocalTable($this);
914
915 1893
        if (strlen($constraint->getName()) > 0) {
916 1870
            $name = $constraint->getName();
917
        } else {
918 23
            $name = $this->_generateIdentifierName(
919 23
                array_merge([$this->getName()], $constraint->getLocalColumns()),
920 23
                'fk',
921 23
                $this->_getMaxIdentifierLength()
922
            );
923
        }
924
925 1893
        $name = $this->normalizeIdentifier($name);
926
927 1893
        $this->_fkConstraints[$name] = $constraint;
928
929
        // add an explicit index on the foreign key columns. If there is already an index that fulfils this requirements drop the request.
930
        // In the case of __construct calling this method during hydration from schema-details all the explicitly added indexes
931
        // lead to duplicates. This creates computation overhead in this case, however no duplicate indexes are ever added (based on columns).
932 1893
        $indexName      = $this->_generateIdentifierName(
933 1893
            array_merge([$this->getName()], $constraint->getColumns()),
934 1893
            'idx',
935 1893
            $this->_getMaxIdentifierLength()
936
        );
937 1893
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
938
939 1893
        foreach ($this->_indexes as $existingIndex) {
940 1314
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
941 831
                return $this;
942
            }
943
        }
944
945 1424
        $this->_addIndex($indexCandidate);
946 1424
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
947
948 1424
        return $this;
949
    }
950
951
    /**
952
     * @param string[] $columnNames
953
     * @param string   $indexName
954
     * @param bool     $isUnique
955
     * @param bool     $isPrimary
956
     * @param string[] $flags
957
     * @param mixed[]  $options
958
     *
959
     * @return Index
960
     *
961
     * @throws SchemaException
962
     */
963 8259
    private function _createIndex(array $columnNames, $indexName, $isUnique, $isPrimary, array $flags = [], array $options = [])
964
    {
965 8259
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
966 22
            throw SchemaException::indexNameInvalid($indexName);
967
        }
968
969 8237
        foreach ($columnNames as $columnName) {
970 8215
            if (! $this->hasColumn($columnName)) {
971 22
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
972
            }
973
        }
974
975 8215
        return new Index($indexName, $columnNames, $isUnique, $isPrimary, $flags, $options);
976
    }
977
978
    /**
979
     * @param string[] $columnNames
980
     * @param string[] $flags
981
     * @param mixed[]  $options
982
     *
983
     * @return UniqueConstraint
984
     *
985
     * @throws SchemaException
986
     */
987
    private function _createUniqueConstraint(array $columnNames, string $indexName, array $flags = [], array $options = [])
988
    {
989
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
990
            throw SchemaException::indexNameInvalid($indexName);
991
        }
992
993
        foreach ($columnNames as $columnName => $indexColOptions) {
994
            if (is_numeric($columnName) && is_string($indexColOptions)) {
995
                $columnName = $indexColOptions;
996
            }
997
998
            if (! $this->hasColumn($columnName)) {
999
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
1000
            }
1001
        }
1002
1003
        return new UniqueConstraint($indexName, $columnNames, $flags, $options);
1004
    }
1005
1006
    /**
1007
     * Normalizes a given identifier.
1008
     *
1009
     * Trims quotes and lowercases the given identifier.
1010
     *
1011
     * @return string The normalized identifier.
1012
     */
1013 12705
    private function normalizeIdentifier(?string $identifier)
1014
    {
1015 12705
        if ($identifier === null) {
1016 22
            return '';
1017
        }
1018
1019 12705
        return $this->trimQuotes(strtolower($identifier));
1020
    }
1021
1022
    /**
1023
     * Returns only columns that have specified names
1024
     *
1025
     * @param string[] $columnNames
1026
     *
1027
     * @return Column[]
1028
     */
1029 9221
    private function filterColumns(array $columnNames, bool $reverse = false)
1030
    {
1031
        return array_filter($this->_columns, static function ($columnName) use ($columnNames, $reverse) : bool {
1032 8825
            return in_array($columnName, $columnNames, true) !== $reverse;
1033 9221
        }, ARRAY_FILTER_USE_KEY);
1034
    }
1035
}
1036