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

Table::_createUniqueConstraint()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

312
    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...
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

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