Issues (201)

src/Schema/Index.php (1 issue)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\DBAL\Schema;
6
7
use Doctrine\DBAL\Platforms\AbstractPlatform;
8
use function array_filter;
9
use function array_keys;
10
use function array_map;
11
use function array_search;
12
use function array_shift;
13
use function count;
14
use function strtolower;
15
16
class Index extends AbstractAsset implements Constraint
17
{
18
    /**
19
     * Asset identifier instances of the column names the index is associated with.
20
     *
21
     * @var array<string, Identifier>
22
     */
23
    protected $_columns = [];
24
25
    /** @var bool */
26
    protected $_isUnique = false;
27
28
    /** @var bool */
29
    protected $_isPrimary = false;
30
31
    /**
32
     * Platform specific flags for indexes.
33
     *
34
     * @var array<string, true>
35
     */
36
    protected $_flags = [];
37
38
    /**
39
     * Platform specific options
40
     *
41
     * @todo $_flags should eventually be refactored into options
42
     * @var array<string, mixed>
43
     */
44
    private $options = [];
45
46
    /**
47
     * @param array<int, string>   $columns
48
     * @param array<int, string>   $flags
49
     * @param array<string, mixed> $options
50
     */
51 10210
    public function __construct(?string $indexName, array $columns, bool $isUnique = false, bool $isPrimary = false, array $flags = [], array $options = [])
52
    {
53 10210
        $isUnique = $isUnique || $isPrimary;
54
55 10210
        if ($indexName !== null) {
56 10188
            $this->_setName($indexName);
57
        }
58
59 10210
        $this->_isUnique  = $isUnique;
60 10210
        $this->_isPrimary = $isPrimary;
61 10210
        $this->options    = $options;
62
63 10210
        foreach ($columns as $column) {
64 10100
            $this->_addColumn($column);
65
        }
66
67 10210
        foreach ($flags as $flag) {
68 194
            $this->addFlag($flag);
69
        }
70 10210
    }
71
72 10100
    protected function _addColumn(string $column) : void
73
    {
74 10100
        $this->_columns[$column] = new Identifier($column);
75 10100
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 8146
    public function getColumns() : array
81
    {
82 8146
        return array_keys($this->_columns);
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88 6418
    public function getQuotedColumns(AbstractPlatform $platform) : array
89
    {
90 6418
        $subParts = $platform->supportsColumnLengthIndexes() && $this->hasOption('lengths')
91 6418
            ? $this->getOption('lengths') : [];
92
93 6418
        $columns = [];
94
95 6418
        foreach ($this->_columns as $column) {
96 6396
            $length = array_shift($subParts);
97
98 6396
            $quotedColumn = $column->getQuotedName($platform);
99
100 6396
            if ($length !== null) {
101 24
                $quotedColumn .= '(' . $length . ')';
102
            }
103
104 6396
            $columns[] = $quotedColumn;
105
        }
106
107 6418
        return $columns;
108
    }
109
110
    /**
111
     * @return array<int, string>
112
     */
113 23
    public function getUnquotedColumns() : array
114
    {
115 23
        return array_map([$this, 'trimQuotes'], $this->getColumns());
116
    }
117
118
    /**
119
     * Is the index neither unique nor primary key?
120
     */
121 22
    public function isSimpleIndex() : bool
122
    {
123 22
        return ! $this->_isPrimary && ! $this->_isUnique;
124
    }
125
126 4490
    public function isUnique() : bool
127
    {
128 4490
        return $this->_isUnique;
129
    }
130
131 9769
    public function isPrimary() : bool
132
    {
133 9769
        return $this->_isPrimary;
134
    }
135
136 23
    public function hasColumnAtPosition(string $columnName, int $pos = 0) : bool
137
    {
138 23
        $columnName   = $this->trimQuotes(strtolower($columnName));
139 23
        $indexColumns = array_map('strtolower', $this->getUnquotedColumns());
140
141 23
        return array_search($columnName, $indexColumns, true) === $pos;
142
    }
143
144
    /**
145
     * Checks if this index exactly spans the given column names in the correct order.
146
     *
147
     * @param array<int, string> $columnNames
148
     */
149 2355
    public function spansColumns(array $columnNames) : bool
150
    {
151 2355
        $columns         = $this->getColumns();
152 2355
        $numberOfColumns = count($columns);
153 2355
        $sameColumns     = true;
154
155 2355
        for ($i = 0; $i < $numberOfColumns; $i++) {
156 2355
            if (isset($columnNames[$i]) && $this->trimQuotes(strtolower($columns[$i])) === $this->trimQuotes(strtolower($columnNames[$i]))) {
157 1640
                continue;
158
            }
159
160 1125
            $sameColumns = false;
161
        }
162
163 2355
        return $sameColumns;
164
    }
165
166
    /**
167
     * Checks if the other index already fulfills all the indexing and constraint needs of the current one.
168
     */
169 2488
    public function isFullfilledBy(Index $other) : bool
170
    {
171
        // allow the other index to be equally large only. It being larger is an option
172
        // but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo)
173 2488
        if (count($other->getColumns()) !== count($this->getColumns())) {
174 309
            return false;
175
        }
176
177
        // Check if columns are the same, and even in the same order
178 2267
        $sameColumns = $this->spansColumns($other->getColumns());
179
180 2267
        if ($sameColumns) {
0 ignored issues
show
The condition $sameColumns is always true.
Loading history...
181 1552
            if (! $this->samePartialIndex($other)) {
182 22
                return false;
183
            }
184
185 1552
            if (! $this->hasSameColumnLengths($other)) {
186 44
                return false;
187
            }
188
189 1508
            if (! $this->isUnique() && ! $this->isPrimary()) {
190
                // this is a special case: If the current key is neither primary or unique, any unique or
191
                // primary key will always have the same effect for the index and there cannot be any constraint
192
                // overlaps. This means a primary or unique index can always fulfill the requirements of just an
193
                // index that has no constraints.
194 1182
                return true;
195
            }
196
197 380
            if ($other->isPrimary() !== $this->isPrimary()) {
198 100
                return false;
199
            }
200
201 302
            return $other->isUnique() === $this->isUnique();
202
        }
203
204 1125
        return false;
205
    }
206
207
    /**
208
     * Detects if the other index is a non-unique, non primary index that can be overwritten by this one.
209
     */
210 22
    public function overrules(Index $other) : bool
211
    {
212 22
        if ($other->isPrimary()) {
213
            return false;
214
        }
215
216 22
        if ($this->isSimpleIndex() && $other->isUnique()) {
217
            return false;
218
        }
219
220 22
        return $this->spansColumns($other->getColumns()) && ($this->isPrimary() || $this->isUnique()) && $this->samePartialIndex($other);
221
    }
222
223
    /**
224
     * Returns platform specific flags for indexes.
225
     *
226
     * @return array<int, string>
227
     */
228 287
    public function getFlags() : array
229
    {
230 287
        return array_keys($this->_flags);
231
    }
232
233
    /**
234
     * Adds Flag for an index that translates to platform specific handling.
235
     *
236
     * @example $index->addFlag('CLUSTERED')
237
     */
238 414
    public function addFlag(string $flag) : self
239
    {
240 414
        $this->_flags[strtolower($flag)] = true;
241
242 414
        return $this;
243
    }
244
245
    /**
246
     * Does this index have a specific flag?
247
     */
248 1954
    public function hasFlag(string $flag) : bool
249
    {
250 1954
        return isset($this->_flags[strtolower($flag)]);
251
    }
252
253
    /**
254
     * Removes a flag.
255
     */
256 22
    public function removeFlag(string $flag) : void
257
    {
258 22
        unset($this->_flags[strtolower($flag)]);
259 22
    }
260
261 4822
    public function hasOption(string $name) : bool
262
    {
263 4822
        return isset($this->options[strtolower($name)]);
264
    }
265
266
    /**
267
     * @return mixed
268
     */
269 139
    public function getOption(string $name)
270
    {
271 139
        return $this->options[strtolower($name)];
272
    }
273
274
    /**
275
     * @return array<string, mixed>
276
     */
277 286
    public function getOptions() : array
278
    {
279 286
        return $this->options;
280
    }
281
282
    /**
283
     * Return whether the two indexes have the same partial index
284
     */
285 1574
    private function samePartialIndex(Index $other) : bool
286
    {
287 1574
        if ($this->hasOption('where') && $other->hasOption('where') && $this->getOption('where') === $other->getOption('where')) {
288 49
            return true;
289
        }
290
291 1569
        return ! $this->hasOption('where') && ! $other->hasOption('where');
292
    }
293
294
    /**
295
     * Returns whether the index has the same column lengths as the other
296
     */
297 1552
    private function hasSameColumnLengths(self $other) : bool
298
    {
299
        $filter = static function (?int $length) : bool {
300 307
            return $length !== null;
301 1552
        };
302
303 1552
        return array_filter($this->options['lengths'] ?? [], $filter)
304 1552
            === array_filter($other->options['lengths'] ?? [], $filter);
305
    }
306
}
307