Failed Conditions
Pull Request — 3.0.x (#3980)
by Guilherme
07:55
created

Table::removeUniqueConstraint()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
ccs 0
cts 5
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 6
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 function array_filter;
9
use function array_keys;
10
use function array_merge;
11
use function in_array;
12
use function is_numeric;
13
use function is_string;
14
use function preg_match;
15
use function strlen;
16
use function strtolower;
17
use const ARRAY_FILTER_USE_KEY;
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 Column[]               $columns
52
     * @param Index[]                $indexes
53
     * @param UniqueConstraint[]     $uniqueConstraints
54
     * @param ForeignKeyConstraint[] $fkConstraints
55
     * @param mixed[]                $options
56
     *
57
     * @throws DBALException
58
     */
59 14560
    public function __construct(
60
        string $name,
61
        array $columns = [],
62
        array $indexes = [],
63
        array $uniqueConstraints = [],
64
        array $fkConstraints = [],
65
        array $options = []
66
    ) {
67 14560
        if ($name === '') {
68 22
            throw DBALException::invalidTableName($name);
69
        }
70
71 14538
        $this->_setName($name);
72
73 14538
        foreach ($columns as $column) {
74 2103
            $this->_addColumn($column);
75
        }
76
77 14516
        foreach ($indexes as $idx) {
78 692
            $this->_addIndex($idx);
79
        }
80
81 14472
        foreach ($uniqueConstraints as $uniqueConstraint) {
82
            $this->_addUniqueConstraint($uniqueConstraint);
83
        }
84
85 14472
        foreach ($fkConstraints as $constraint) {
86 287
            $this->_addForeignKeyConstraint($constraint);
87
        }
88
89 14472
        $this->_options = array_merge($this->_options, $options);
90 14472
    }
91
92
    /**
93
     * @return void
94
     */
95 1479
    public function setSchemaConfig(SchemaConfig $schemaConfig)
96
    {
97 1479
        $this->_schemaConfig = $schemaConfig;
98 1479
    }
99
100
    /**
101
     * @return int
102
     */
103 2676
    protected function _getMaxIdentifierLength()
104
    {
105 2676
        if ($this->_schemaConfig instanceof SchemaConfig) {
106 231
            return $this->_schemaConfig->getMaxIdentifierLength();
107
        }
108
109 2489
        return 63;
110
    }
111
112
    /**
113
     * Sets the Primary Key.
114
     *
115
     * @param string[]     $columnNames
116
     * @param string|false $indexName
117
     *
118
     * @return self
119
     */
120 5878
    public function setPrimaryKey(array $columnNames, $indexName = false)
121
    {
122 5878
        if ($indexName === false) {
123 5856
            $indexName = 'primary';
124
        }
125
126 5878
        $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true));
127
128 5878
        foreach ($columnNames as $columnName) {
129 5878
            $column = $this->getColumn($columnName);
130 5878
            $column->setNotnull(true);
131
        }
132
133 5878
        return $this;
134
    }
135
136
    /**
137
     * @param string[] $columnNames
138
     * @param string[] $flags
139
     * @param mixed[]  $options
140
     *
141
     * @return self
142
     */
143 1818
    public function addIndex(array $columnNames, ?string $indexName = null, array $flags = [], array $options = [])
144
    {
145 1818
        if ($indexName === null) {
146 392
            $indexName = $this->_generateIdentifierName(
147 392
                array_merge([$this->getName()], $columnNames),
148 392
                'idx',
149 392
                $this->_getMaxIdentifierLength()
150
            );
151
        }
152
153 1818
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
154
    }
155
156
    /**
157
     * @param string[] $columnNames
158
     * @param string[] $flags
159
     * @param mixed[]  $options
160
     *
161
     * @return self
162
     */
163
    public function addUniqueConstraint(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) : Table
164
    {
165
        if ($indexName === null) {
166
            $indexName = $this->_generateIdentifierName(
167
                array_merge([$this->getName()], $columnNames),
168
                'uniq',
169
                $this->_getMaxIdentifierLength()
170
            );
171
        }
172
173
        return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options));
174
    }
175
176
    /**
177
     * Drops the primary key from this table.
178
     *
179
     * @return void
180
     */
181 398
    public function dropPrimaryKey()
182
    {
183 398
        if ($this->_primaryKeyName === null) {
184
            return;
185
        }
186
187 398
        $this->dropIndex($this->_primaryKeyName);
188 398
        $this->_primaryKeyName = null;
189 398
    }
190
191
    /**
192
     * Drops an index from this table.
193
     *
194
     * @param string $indexName The index name.
195
     *
196
     * @return void
197
     *
198
     * @throws SchemaException If the index does not exist.
199
     */
200 674
    public function dropIndex($indexName)
201
    {
202 674
        $indexName = $this->normalizeIdentifier($indexName);
203
204 674
        if (! $this->hasIndex($indexName)) {
205
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
206
        }
207
208 674
        unset($this->_indexes[$indexName]);
209 674
    }
210
211
    /**
212
     * @param string[]    $columnNames
213
     * @param string|null $indexName
214
     * @param mixed[]     $options
215
     *
216
     * @return self
217
     */
218 546
    public function addUniqueIndex(array $columnNames, $indexName = null, array $options = [])
219
    {
220 546
        if ($indexName === null) {
221 363
            $indexName = $this->_generateIdentifierName(
222 363
                array_merge([$this->getName()], $columnNames),
223 363
                'uniq',
224 363
                $this->_getMaxIdentifierLength()
225
            );
226
        }
227
228 546
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
229
    }
230
231
    /**
232
     * Renames an index.
233
     *
234
     * @param string      $oldIndexName The name of the index to rename from.
235
     * @param string|null $newIndexName The name of the index to rename to.
236
     *                                  If null is given, the index name will be auto-generated.
237
     *
238
     * @return self This table instance.
239
     *
240
     * @throws SchemaException If no index exists for the given current name
241
     *                         or if an index with the given new name already exists on this table.
242
     */
243 308
    public function renameIndex($oldIndexName, $newIndexName = null)
244
    {
245 308
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
246 308
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
247
248 308
        if ($oldIndexName === $normalizedNewIndexName) {
249 198
            return $this;
250
        }
251
252 308
        if (! $this->hasIndex($oldIndexName)) {
253 22
            throw SchemaException::indexDoesNotExist($oldIndexName, $this->_name);
254
        }
255
256 286
        if ($this->hasIndex($normalizedNewIndexName)) {
257 22
            throw SchemaException::indexAlreadyExists($normalizedNewIndexName, $this->_name);
258
        }
259
260 264
        $oldIndex = $this->_indexes[$oldIndexName];
261
262 264
        if ($oldIndex->isPrimary()) {
263 22
            $this->dropPrimaryKey();
264
265 22
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? false);
266
        }
267
268 264
        unset($this->_indexes[$oldIndexName]);
269
270 264
        if ($oldIndex->isUnique()) {
271 44
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
272
        }
273
274 242
        return $this->addIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getFlags(), $oldIndex->getOptions());
275
    }
276
277
    /**
278
     * Checks if an index begins in the order of the given columns.
279
     *
280
     * @param string[] $columnNames
281
     *
282
     * @return bool
283
     */
284 44
    public function columnsAreIndexed(array $columnNames)
285
    {
286 44
        foreach ($this->getIndexes() as $index) {
287 44
            if ($index->spansColumns($columnNames)) {
288 44
                return true;
289
            }
290
        }
291
292
        return false;
293
    }
294
295
    /**
296
     * @param string[] $columnNames
297
     * @param string   $indexName
298
     * @param bool     $isUnique
299
     * @param bool     $isPrimary
300
     * @param string[] $flags
301
     * @param mixed[]  $options
302
     *
303
     * @return Index
304
     *
305
     * @throws SchemaException
306
     */
307 8313
    private function _createIndex(array $columnNames, $indexName, $isUnique, $isPrimary, array $flags = [], array $options = [])
308
    {
309 8313
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
310 22
            throw SchemaException::indexNameInvalid($indexName);
311
        }
312
313 8291
        foreach ($columnNames as $columnName) {
314 8269
            if (! $this->hasColumn($columnName)) {
315 22
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
316
            }
317
        }
318
319 8269
        return new Index($indexName, $columnNames, $isUnique, $isPrimary, $flags, $options);
320
    }
321
322
    /**
323
     * @param string  $columnName
324
     * @param string  $typeName
325
     * @param mixed[] $options
326
     *
327
     * @return Column
328
     */
329 11696
    public function addColumn($columnName, $typeName, array $options = [])
330
    {
331 11696
        $column = new Column($columnName, Type::getType($typeName), $options);
332
333 11696
        $this->_addColumn($column);
334
335 11696
        return $column;
336
    }
337
338
    /**
339
     * Renames a Column.
340
     *
341
     * @deprecated
342
     *
343
     * @param string $oldColumnName
344
     * @param string $newColumnName
345
     *
346
     * @return void
347
     *
348
     * @throws DBALException
349
     */
350
    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

350
    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

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