Failed Conditions
Pull Request — 3.0.x (#3980)
by Guilherme
124:28 queued 58:42
created

src/Schema/Table.php (1 issue)

Labels
Severity
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_merge;
11
use function in_array;
12
use function preg_match;
13
use function strlen;
14
use function strtolower;
15
16
/**
17
 * Object Representation of a table.
18
 */
19
class Table extends AbstractAsset
20
{
21
    /** @var Column[] */
22
    protected $_columns = [];
23
24
    /** @var Index[] */
25
    protected $_indexes = [];
26
27
    /** @var string|false */
28
    protected $_primaryKeyName = false;
29
30
    /** @var UniqueConstraint[] */
31
    protected $_uniqueConstraints = [];
32
33
    /** @var ForeignKeyConstraint[] */
34
    protected $_fkConstraints = [];
35
36
    /** @var mixed[] */
37
    protected $_options = [
38
        'create_options' => [],
39
    ];
40
41
    /** @var SchemaConfig|null */
42
    protected $_schemaConfig = null;
43
44
    /** @var Index[] */
45
    private $implicitIndexes = [];
46
47
    /**
48
     * @param string                 $tableName
49
     * @param Column[]               $columns
50
     * @param Index[]                $indexes
51
     * @param UniqueConstraint[]     $uniqueConstraints
52
     * @param ForeignKeyConstraint[] $fkConstraints
53
     * @param int                    $idGeneratorType
54 14477
     * @param mixed[]                $options
55
     *
56 14477
     * @throws DBALException
57 22
     */
58
    public function __construct(
59
        $tableName,
60 14455
        array $columns = [],
61
        array $indexes = [],
62 14455
        array $uniqueConstraints = [],
63 2062
        array $fkConstraints = [],
64
        $idGeneratorType = 0,
65
        array $options = []
66 14433
    )
67 654
    {
68
        if (strlen($tableName) === 0) {
69
            throw DBALException::invalidTableName($tableName);
70 14389
        }
71 253
72
        $this->_setName($tableName);
73
74 14389
        foreach ($columns as $column) {
75 14389
            $this->_addColumn($column);
76
        }
77
78
        foreach ($indexes as $idx) {
79
            $this->_addIndex($idx);
80 1443
        }
81
82 1443
        foreach ($uniqueConstraints as $uniqueConstraint) {
83 1443
            $this->_addUniqueConstraint($uniqueConstraint);
84
        }
85
86
        foreach ($fkConstraints as $constraint) {
87
            $this->_addForeignKeyConstraint($constraint);
88 2628
        }
89
90 2628
        $this->_options = array_merge($this->_options, $options);
91 227
    }
92
93
    /**
94 2443
     * @return void
95
     */
96
    public function setSchemaConfig(SchemaConfig $schemaConfig)
97
    {
98
        $this->_schemaConfig = $schemaConfig;
99
    }
100
101
    /**
102
     * Sets the Primary Key.
103
     *
104
     * @param string[]     $columnNames
105 5827
     * @param string|false $indexName
106
     *
107 5827
     * @return self
108 5805
     */
109
    public function setPrimaryKey(array $columnNames, $indexName = false)
110
    {
111 5827
        if ($indexName === false) {
112
            $indexName = 'primary';
113 5827
        }
114 5827
115 5827
        $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true));
116
117
        foreach ($columnNames as $columnName) {
118 5827
            $column = $this->getColumn($columnName);
119
            $column->setNotnull(true);
120
        }
121
122
        return $this;
123
    }
124
125
    /**
126
     * @param string[]    $columnNames
127
     * @param string|null $indexName
128
     * @param string[]    $flags
129 1815
     * @param mixed[]     $options
130
     *
131 1815
     * @return self
132 393
     */
133 393
    public function addIndex(array $columnNames, $indexName = null, array $flags = [], array $options = [])
134 393
    {
135 393
        if ($indexName === null) {
136
            $indexName = $this->_generateIdentifierName(
137
                array_merge([$this->getName()], $columnNames),
138
                'idx',
139 1815
                $this->_getMaxIdentifierLength()
140
            );
141
        }
142
143
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
144
    }
145
146
    /**
147 398
     * @param array       $columnNames
148
     * @param string|null $indexName
149 398
     * @param array       $options
150
     *
151
     * @return self
152
     */
153 398
    public function addUniqueConstraint(array $columnNames, $indexName = null, array $flags = [], array $options = [])
154 398
    {
155 398
        if ($indexName == null) {
0 ignored issues
show
It seems like you are loosely comparing $indexName of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
156
            $indexName = $this->_generateIdentifierName(
157
                array_merge(array($this->getName()), $columnNames), "uniq", $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 674
     *
167
     * @return void
168 674
     */
169 674
    public function dropPrimaryKey()
170
    {
171
        if ($this->_primaryKeyName === false) {
172 674
            return;
173 674
        }
174
175
        $this->dropIndex($this->_primaryKeyName);
176
        $this->_primaryKeyName = false;
177
    }
178
179
    /**
180
     * Drops an index from this table.
181
     *
182 543
     * @param string $indexName The index name.
183
     *
184 543
     * @return void
185 361
     *
186 361
     * @throws SchemaException If the index does not exist.
187 361
     */
188 361
    public function dropIndex($indexName)
189
    {
190
        $indexName = $this->normalizeIdentifier($indexName);
191
192 543
        if (! $this->hasIndex($indexName)) {
193
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
194
        }
195
196
        unset($this->_indexes[$indexName]);
197
    }
198
199
    /**
200
     * @param string[]    $columnNames
201
     * @param string|null $indexName
202
     * @param mixed[]     $options
203
     *
204
     * @return self
205
     */
206
    public function addUniqueIndex(array $columnNames, $indexName = null, array $options = [])
207 307
    {
208
        if ($indexName === null) {
209 307
            $indexName = $this->_generateIdentifierName(
210 307
                array_merge([$this->getName()], $columnNames),
211
                'uniq',
212 307
                $this->_getMaxIdentifierLength()
213 198
            );
214
        }
215
216 307
        return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
217 22
    }
218
219
    /**
220 285
     * Renames an index.
221 22
     *
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 263
     *                                  If null is given, the index name will be auto-generated.
225
     *
226 263
     * @return self This table instance.
227 22
     *
228
     * @throws SchemaException If no index exists for the given current name
229 22
     *                         or if an index with the given new name already exists on this table.
230
     */
231
    public function renameIndex($oldIndexName, $newIndexName = null)
232 263
    {
233
        $oldIndexName           = $this->normalizeIdentifier($oldIndexName);
234 263
        $normalizedNewIndexName = $this->normalizeIdentifier($newIndexName);
235 44
236
        if ($oldIndexName === $normalizedNewIndexName) {
237
            return $this;
238 241
        }
239
240
        if (! $this->hasIndex($oldIndexName)) {
241
            throw SchemaException::indexDoesNotExist($oldIndexName, $this->_name);
242
        }
243
244
        if ($this->hasIndex($normalizedNewIndexName)) {
245
            throw SchemaException::indexAlreadyExists($normalizedNewIndexName, $this->_name);
246
        }
247
248 43
        $oldIndex = $this->_indexes[$oldIndexName];
249
250 43
        if ($oldIndex->isPrimary()) {
251
            $this->dropPrimaryKey();
252 43
253 43
            return $this->setPrimaryKey($oldIndex->getColumns(), $newIndexName ?? false);
254
        }
255
256
        unset($this->_indexes[$oldIndexName]);
257
258
        if ($oldIndex->isUnique()) {
259
            return $this->addUniqueIndex($oldIndex->getColumns(), $newIndexName, $oldIndex->getOptions());
260
        }
261
262
        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 8259
    public function columnsAreIndexed(array $columnNames)
273
    {
274 8259
        foreach ($this->getIndexes() as $index) {
275 22
            /** @var $index Index */
276
            if ($index->spansColumns($columnNames)) {
277
                return true;
278 8237
            }
279 8215
        }
280 22
281
        return false;
282
    }
283
284 8215
    /**
285
     * @param string  $columnName
286
     * @param string  $typeName
287
     * @param mixed[] $options
288
     *
289
     * @return Column
290
     */
291
    public function addColumn($columnName, $typeName, array $options = [])
292
    {
293
        $column = new Column($columnName, Type::getType($typeName), $options);
294 11636
295
        $this->_addColumn($column);
296 11636
297
        return $column;
298 11636
    }
299
300 11636
    /**
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)
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
    public function changeColumn($columnName, array $options)
330 265
    {
331
        $column = $this->getColumn($columnName);
332 265
333 265
        $column->setOptions($options);
334
335 265
        return $this;
336
    }
337
338
    /**
339
     * Drops a Column from the Table.
340
     *
341
     * @param string $columnName
342
     *
343
     * @return self
344
     */
345 198
    public function dropColumn($columnName)
346
    {
347 198
        $columnName = $this->normalizeIdentifier($columnName);
348 198
349
        unset($this->_columns[$columnName]);
350 198
351
        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 1812
     */
367
    public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [], $constraintName = null)
368 1812
    {
369 954
        if ($constraintName === null) {
370
            $constraintName = $this->_generateIdentifierName(array_merge((array) $this->getName(), $localColumnNames), 'fk', $this->_getMaxIdentifierLength());
371
        }
372 1812
373
        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 71
     */
390
    public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
391 71
    {
392
        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 1834
     */
410
    public function addNamedForeignKeyConstraint($name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options = [])
411 1834
    {
412 923
        if ($foreignTable instanceof Table) {
413 923
            foreach ($foreignColumnNames as $columnName) {
414 22
                if (! $foreignTable->hasColumn($columnName)) {
415
                    throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName());
416
                }
417
            }
418
        }
419 1812
420 1812
        foreach ($localColumnNames as $columnName) {
421 22
            if (! $this->hasColumn($columnName)) {
422
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
423
            }
424
        }
425 1790
426 1790
        $constraint = new ForeignKeyConstraint(
427
            $localColumnNames,
428
            $foreignTable,
429
            $foreignColumnNames,
430
            $name,
431
            $options
432 1790
        );
433
434 1790
        return $this->_addForeignKeyConstraint($constraint);
435
    }
436
437
    /**
438
     * @param string $name
439
     * @param mixed  $value
440
     *
441
     * @return self
442
     */
443 1737
    public function addOption($name, $value)
444
    {
445 1737
        $this->_options[$name] = $value;
446
447 1737
        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 12617
     * @return bool
456
     */
457 12617
    public function hasForeignKey($constraintName)
458 12617
    {
459
        $constraintName = $this->normalizeIdentifier($constraintName);
460 12617
461 22
        return isset($this->_fkConstraints[$constraintName]);
462
    }
463
464 12617
    /**
465 12617
     * 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
    public function getForeignKey($constraintName)
474 8386
    {
475
        $constraintName = $this->normalizeIdentifier($constraintName);
476 8386
477 8386
        if (! $this->hasForeignKey($constraintName)) {
478 8386
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
479
        }
480 8386
481 550
        return $this->_fkConstraints[$constraintName];
482 462
    }
483
484
    /**
485 88
     * Removes the foreign key constraint with the given name.
486
     *
487
     * @param string $constraintName The constraint name.
488 8386
     *
489 8386
     * @return void
490
     *
491 44
     * @throws SchemaException
492
     */
493
    public function removeForeignKey($constraintName)
494 8386
    {
495 88
        $constraintName = $this->normalizeIdentifier($constraintName);
496
497
        if (! $this->hasForeignKey($constraintName)) {
498 8386
            throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name);
499 5893
        }
500
501
        unset($this->_fkConstraints[$constraintName]);
502 8386
    }
503
504 8386
    /**
505
     * Returns whether this table has a unique constraint with the given name.
506
     *
507
     * @param string $constraintName
508
     *
509
     * @return bool
510 1893
     */
511
    public function hasUniqueConstraint($constraintName)
512 1893
    {
513
        $constraintName = $this->normalizeIdentifier($constraintName);
514 1893
515 1870
        return isset($this->_uniqueConstraints[$constraintName]);
516
    }
517 23
518 23
    /**
519 23
     * Returns the unique constraint with the given name.
520 23
     *
521
     * @param string $constraintName The constraint name.
522
     *
523 1893
     * @return UniqueConstraint
524
     *
525 1893
     * @throws SchemaException If the unique constraint does not exist.
526
     */
527
    public function getUniqueConstraint($constraintName)
528
    {
529
        $constraintName = $this->normalizeIdentifier($constraintName);
530 1893
531 1893
        if (! $this->hasForeignKey($constraintName)) {
532 1893
            throw SchemaException::uniqueConstraintDoesNotExist($constraintName, $this->_name);
533 1893
        }
534
535 1893
        return $this->_uniqueConstraints[$constraintName];
536
    }
537 1893
538 1314
    /**
539 831
     * Removes the unique constraint with the given name.
540
     *
541
     * @param string $constraintName The constraint name.
542
     *
543 1424
     * @return void
544 1424
     *
545 1424
     * @throws SchemaException
546
     */
547
    public function removeUniqueConstraint($constraintName)
548
    {
549
        $constraintName = $this->normalizeIdentifier($constraintName);
550
551
        if (! $this->hasForeignKey($constraintName)) {
552
            throw SchemaException::uniqueConstraintDoesNotExist($constraintName, $this->_name);
553
        }
554 242
555
        unset($this->_uniqueConstraints[$constraintName]);
556 242
    }
557
558 242
    /**
559
     * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest)
560
     *
561
     * @return Column[]
562
     */
563
    public function getColumns()
564
    {
565
        $primaryKeyColumns = $this->hasPrimaryKey() ? $this->getPrimaryKeyColumns() : [];
566
        $foreignKeyColumns = $this->getForeignKeyColumns();
567
        $remainderColumns  = $this->filterColumns(
568
            array_merge(array_keys($primaryKeyColumns), array_keys($foreignKeyColumns)),
569
            true
570 177
        );
571
572 177
        return array_merge($primaryKeyColumns, $foreignKeyColumns, $remainderColumns);
573 177
    }
574
575
    /**
576
     * Returns whether this table has a Column with the given name.
577 177
     *
578
     * @param string $columnName The column name.
579
     *
580
     * @return bool
581
     */
582
    public function hasColumn($columnName)
583
    {
584
        $columnName = $this->normalizeIdentifier($columnName);
585
586
        return isset($this->_columns[$columnName]);
587
    }
588
589 220
    /**
590
     * Returns the Column with the given name.
591 220
     *
592 220
     * @param string $columnName The column name.
593
     *
594
     * @return Column
595
     *
596 220
     * @throws SchemaException If the column does not exist.
597 220
     */
598
    public function getColumn($columnName)
599
    {
600
        $columnName = $this->normalizeIdentifier($columnName);
601
602
        if (! $this->hasColumn($columnName)) {
603
            throw SchemaException::columnDoesNotExist($columnName, $this->_name);
604 9221
        }
605
606 9221
        return $this->_columns[$columnName];
607 9221
    }
608
609 9221
    /**
610 4336
     * Returns the primary key.
611
     *
612
     * @return Index|null The primary key, or null if this Table has no primary key.
613 9221
     */
614
    public function getPrimaryKey()
615
    {
616
        return ($this->_primaryKeyName !== false)
617
            ? $this->getIndex($this->_primaryKeyName)
618
            : null;
619
    }
620
621 9221
    /**
622
     * Returns the primary key columns.
623 9221
     *
624 9221
     * @return Column[]
625 894
     *
626
     * @throws DBALException
627
     */
628 9221
    public function getPrimaryKeyColumns()
629
    {
630
        $primaryKey = $this->getPrimaryKey();
631
632
        if ($primaryKey === null) {
633
            throw new DBALException('Table ' . $this->getName() . ' has no primary key.');
634
        }
635
636
        return $this->filterColumns($primaryKey->getColumns());
637
    }
638 9221
639
    /**
640
     * Returns the foreign key columns
641 8825
     *
642 9221
     * @return Column[]
643
     */
644
    public function getForeignKeyColumns()
645
    {
646
        $foreignKeyColumns = [];
647
648
        foreach ($this->getForeignKeys() as $foreignKey) {
649
            $foreignKeyColumns = array_merge($foreignKeyColumns, $foreignKey->getLocalColumns());
650
        }
651
652 10055
        return $this->filterColumns($foreignKeyColumns);
653
    }
654 10055
655
    /**
656 10055
     * Returns whether this table has a primary key.
657
     *
658
     * @return bool
659
     */
660
    public function hasPrimaryKey()
661
    {
662
        return $this->_primaryKeyName !== false && $this->hasIndex($this->_primaryKeyName);
663
    }
664
665
    /**
666
     * Returns whether this table has an Index with the given name.
667
     *
668 7958
     * @param string $indexName The index name.
669
     *
670 7958
     * @return bool
671 7958
     */
672 22
    public function hasIndex($indexName)
673
    {
674
        $indexName = $this->normalizeIdentifier($indexName);
675 7936
676
        return isset($this->_indexes[$indexName]);
677
    }
678
679
    /**
680
     * Returns the Index with the given name.
681
     *
682
     * @param string $indexName The index name.
683 9331
     *
684
     * @return Index
685 9331
     *
686 4446
     * @throws SchemaException If the index does not exist.
687
     */
688
    public function getIndex($indexName)
689 5262
    {
690
        $indexName = $this->normalizeIdentifier($indexName);
691
        if (! $this->hasIndex($indexName)) {
692
            throw SchemaException::indexDoesNotExist($indexName, $this->_name);
693
        }
694
695
        return $this->_indexes[$indexName];
696
    }
697
698
    /**
699 310
     * @return Index[]
700
     */
701 310
    public function getIndexes()
702
    {
703 310
        return $this->_indexes;
704
    }
705
706
    /**
707 310
     * Returns the unique constraints.
708
     *
709
     * @return UniqueConstraint[]
710
     */
711
    public function getUniqueConstraints()
712
    {
713
        return $this->_uniqueConstraints;
714
    }
715 939
716
    /**
717 939
     * Returns the foreign key constraints.
718
     *
719
     * @return ForeignKeyConstraint[]
720
     */
721
    public function getForeignKeys()
722
    {
723
        return $this->_fkConstraints;
724
    }
725
726
    /**
727 5619
     * @param string $name
728
     *
729 5619
     * @return bool
730
     */
731 5619
    public function hasOption($name)
732
    {
733
        return isset($this->_options[$name]);
734
    }
735
736
    /**
737
     * @param string $name
738
     *
739
     * @return mixed
740
     */
741
    public function getOption($name)
742
    {
743 5135
        return $this->_options[$name];
744
    }
745 5135
746 5135
    /**
747 22
     * @return mixed[]
748
     */
749
    public function getOptions()
750 5113
    {
751
        return $this->_options;
752
    }
753
754
    /**
755
     * @return void
756 9023
     */
757
    public function visit(Visitor $visitor)
758 9023
    {
759
        $visitor->acceptTable($this);
760
761
        foreach ($this->getColumns() as $column) {
762
            $visitor->acceptColumn($this, $column);
763
        }
764
765
        foreach ($this->getIndexes() as $index) {
766 9617
            $visitor->acceptIndex($this, $index);
767
        }
768 9617
769
        foreach ($this->getForeignKeys() as $constraint) {
770
            $visitor->acceptForeignKey($this, $constraint);
771
        }
772
    }
773
774
    /**
775
     * Clone of a Table triggers a deep clone of all affected assets.
776 4399
     *
777
     * @return void
778 4399
     */
779
    public function __clone()
780
    {
781
        foreach ($this->_columns as $k => $column) {
782
            $this->_columns[$k] = clone $column;
783
        }
784
        foreach ($this->_indexes as $k => $index) {
785
            $this->_indexes[$k] = clone $index;
786 221
        }
787
        foreach ($this->_fkConstraints as $k => $fk) {
788 221
            $this->_fkConstraints[$k] = clone $fk;
789
            $this->_fkConstraints[$k]->setLocalTable($this);
790
        }
791
    }
792
793
    public function setComment(?string $comment) : self
794 6995
    {
795
        // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options.
796 6995
        $this->addOption('comment', $comment);
797
798
        return $this;
799
    }
800
801
    public function getComment() : ?string
802 342
    {
803
        return $this->_options['comment'] ?? null;
804 342
    }
805
806 342
    /**
807 276
     * @return int
808
     */
809
    protected function _getMaxIdentifierLength()
810 342
    {
811 192
        if ($this->_schemaConfig instanceof SchemaConfig) {
812
            return $this->_schemaConfig->getMaxIdentifierLength();
813
        }
814 342
815 88
        return 63;
816
    }
817 342
818
    /**
819
     * @return void
820
     *
821
     * @throws SchemaException
822
     */
823
    protected function _addColumn(Column $column)
824 1052
    {
825
        $columnName = $column->getName();
826 1052
        $columnName = $this->normalizeIdentifier($columnName);
827 1030
828
        if (isset($this->_columns[$columnName])) {
829 1052
            throw SchemaException::columnAlreadyExists($this->getName(), $columnName);
830 742
        }
831
832 1052
        $this->_columns[$columnName] = $column;
833 175
    }
834 175
835
    /**
836 1052
     * Adds an index to the table.
837
     *
838
     * @return self
839
     *
840
     * @throws SchemaException
841
     */
842
    protected function _addIndex(Index $indexCandidate)
843
    {
844
        $indexName               = $indexCandidate->getName();
845
        $indexName               = $this->normalizeIdentifier($indexName);
846
        $replacedImplicitIndexes = [];
847 12705
848
        foreach ($this->implicitIndexes as $name => $implicitIndex) {
849 12705
            if (! $implicitIndex->isFullfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
850 22
                continue;
851
            }
852
853 12705
            $replacedImplicitIndexes[] = $name;
854
        }
855
856 44
        if ((isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
857
            ($this->_primaryKeyName !== false && $indexCandidate->isPrimary())
858
        ) {
859 44
            throw SchemaException::indexAlreadyExists($indexName, $this->_name);
860
        }
861 44
862
        foreach ($replacedImplicitIndexes as $name) {
863
            unset($this->_indexes[$name], $this->implicitIndexes[$name]);
864 44
        }
865
866 44
        if ($indexCandidate->isPrimary()) {
867
            $this->_primaryKeyName = $indexName;
868
        }
869
870
        $this->_indexes[$indexName] = $indexCandidate;
871
872
        return $this;
873
    }
874
875
    /**
876
     * @return self
877
     */
878
    protected function _addUniqueConstraint(UniqueConstraint $constraint)
879
    {
880
        $name = $constraint->getName() !== null && $constraint->getName() !== ''
881
            ? $constraint->getName()
882
            : $this->_generateIdentifierName(
883
                array_merge((array) $this->getName(), $constraint->getColumns()), "fk", $this->_getMaxIdentifierLength()
884
            )
885
        ;
886
887
        $name = $this->normalizeIdentifier($name);
888
889
        $this->_uniqueConstraints[$name] = $constraint;
890
891
        // If there is already an index that fulfills this requirements drop the request. In the case of __construct
892
        // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
893
        // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
894
        $indexName = $this->_generateIdentifierName(
895
            array_merge(array($this->getName()), $constraint->getColumns()), "idx", $this->_getMaxIdentifierLength()
896
        );
897
898
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
899
900
        foreach ($this->_indexes as $existingIndex) {
901
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
902
                return $this;
903
            }
904
        }
905
906
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
907
908
        return $this;
909
    }
910
911
    /**
912
     * @return self
913
     */
914
    protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint)
915
    {
916
        $constraint->setLocalTable($this);
917
918
        if (strlen($constraint->getName()) > 0) {
919
            $name = $constraint->getName();
920
        } else {
921
            $name = $this->_generateIdentifierName(
922
                array_merge((array) $this->getName(), $constraint->getLocalColumns()),
923
                'fk',
924
                $this->_getMaxIdentifierLength()
925
            );
926
        }
927
        $name = $this->normalizeIdentifier($name);
928
929
        $this->_fkConstraints[$name] = $constraint;
930
931
        // add an explicit index on the foreign key columns. If there is already an index that fulfils this requirements drop the request.
932
        // In the case of __construct calling this method during hydration from schema-details all the explicitly added indexes
933
        // lead to duplicates. This creates computation overhead in this case, however no duplicate indexes are ever added (based on columns).
934
        $indexName      = $this->_generateIdentifierName(
935
            array_merge([$this->getName()], $constraint->getColumns()),
936
            'idx',
937
            $this->_getMaxIdentifierLength()
938
        );
939
        $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false);
940
941
        foreach ($this->_indexes as $existingIndex) {
942
            if ($indexCandidate->isFullfilledBy($existingIndex)) {
943
                return $this;
944
            }
945
        }
946
947
        $this->_addIndex($indexCandidate);
948
        $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
949
950
        return $this;
951
    }
952
953
    /**
954
     * @param string[] $columnNames
955
     * @param string   $indexName
956
     * @param bool     $isUnique
957
     * @param bool     $isPrimary
958
     * @param string[] $flags
959
     * @param mixed[]  $options
960
     *
961
     * @return Index
962
     *
963
     * @throws SchemaException
964
     */
965
    private function _createIndex(array $columnNames, $indexName, $isUnique, $isPrimary, array $flags = [], array $options = [])
966
    {
967
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
968
            throw SchemaException::indexNameInvalid($indexName);
969
        }
970
971
        foreach ($columnNames as $columnName) {
972
            if (! $this->hasColumn($columnName)) {
973
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
974
            }
975
        }
976
977
        return new Index($indexName, $columnNames, $isUnique, $isPrimary, $flags, $options);
978
    }
979
980
    /**
981
     * @param array  $columnNames
982
     * @param string $indexName
983
     * @param array  $flags
984
     * @param array  $options
985
     *
986
     * @return UniqueConstraint
987
     *
988
     * @throws SchemaException
989
     */
990
    private function _createUniqueConstraint(array $columnNames, $indexName, array $flags = [], array $options = [])
991
    {
992
        if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
993
            throw SchemaException::indexNameInvalid($indexName);
994
        }
995
996
        foreach ($columnNames as $columnName => $indexColOptions) {
997
            if (is_numeric($columnName) && is_string($indexColOptions)) {
998
                $columnName = $indexColOptions;
999
            }
1000
1001
            if ( ! $this->hasColumn($columnName)) {
1002
                throw SchemaException::columnDoesNotExist($columnName, $this->_name);
1003
            }
1004
        }
1005
1006
        return new UniqueConstraint($indexName, $columnNames, $flags, $options);
1007
    }
1008
1009
    /**
1010
     * Normalizes a given identifier.
1011
     *
1012
     * Trims quotes and lowercases the given identifier.
1013
     *
1014
     * @param string|null $identifier The identifier to normalize.
1015
     *
1016
     * @return string The normalized identifier.
1017
     */
1018
    private function normalizeIdentifier($identifier)
1019
    {
1020
        if ($identifier === null) {
1021
            return '';
1022
        }
1023
1024
        return $this->trimQuotes(strtolower($identifier));
1025
    }
1026
1027
    /**
1028
     * Returns only columns that have specified names
1029
     *
1030
     * @param string[] $columnNames
1031
     *
1032
     * @return Column[]
1033
     */
1034
    private function filterColumns(array $columnNames, bool $reverse = false)
1035
    {
1036
        return array_filter($this->_columns, static function ($columnName) use ($columnNames, $reverse) : bool {
1037
            return in_array($columnName, $columnNames, true) !== $reverse;
1038
        }, ARRAY_FILTER_USE_KEY);
1039
    }
1040
}
1041