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