Index.php 8.62 KB
Newer Older
1 2 3 4
<?php

namespace Doctrine\DBAL\Schema;

5
use Doctrine\DBAL\Platforms\AbstractPlatform;
6
use InvalidArgumentException;
7
use function array_filter;
8 9 10
use function array_keys;
use function array_map;
use function array_search;
11
use function array_shift;
12 13 14
use function count;
use function is_string;
use function strtolower;
15

16
class Index extends AbstractAsset implements Constraint
17 18
{
    /**
19 20 21 22
     * Asset identifier instances of the column names the index is associated with.
     * array($columnName => Identifier)
     *
     * @var Identifier[]
23
     */
24
    protected $_columns = [];
25

26
    /** @var bool */
27 28
    protected $_isUnique = false;

29
    /** @var bool */
30 31
    protected $_isPrimary = false;

32 33
    /**
     * Platform specific flags for indexes.
34
     * array($flagName => true)
35
     *
36
     * @var true[]
37
     */
38
    protected $_flags = [];
39

40
    /**
41
     * Platform specific options
42
     *
43
     * @todo $_flags should eventually be refactored into options
44
     * @var mixed[]
45
     */
46
    private $options = [];
47

48
    /**
49 50
     * @param string   $indexName
     * @param string[] $columns
51 52
     * @param bool     $isUnique
     * @param bool     $isPrimary
53
     * @param string[] $flags
54
     * @param mixed[]  $options
55
     */
56
    public function __construct($indexName, array $columns, $isUnique = false, $isPrimary = false, array $flags = [], array $options = [])
57
    {
58
        $isUnique = $isUnique || $isPrimary;
59

60
        $this->_setName($indexName);
61
        $this->_isUnique  = $isUnique;
62
        $this->_isPrimary = $isPrimary;
63
        $this->options    = $options;
64

65
        foreach ($columns as $column) {
66 67
            $this->_addColumn($column);
        }
Grégoire Paris's avatar
Grégoire Paris committed
68

69 70 71
        foreach ($flags as $flag) {
            $this->addFlag($flag);
        }
72 73 74 75
    }

    /**
     * @param string $column
Benjamin Morel's avatar
Benjamin Morel committed
76 77 78
     *
     * @return void
     *
79
     * @throws InvalidArgumentException
80 81 82
     */
    protected function _addColumn($column)
    {
83 84
        if (! is_string($column)) {
            throw new InvalidArgumentException('Expecting a string as Index Column');
85
        }
86 87

        $this->_columns[$column] = new Identifier($column);
88 89 90
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
91
     * {@inheritdoc}
92 93 94
     */
    public function getColumns()
    {
95 96 97 98
        return array_keys($this->_columns);
    }

    /**
99
     * {@inheritdoc}
100 101 102
     */
    public function getQuotedColumns(AbstractPlatform $platform)
    {
103 104 105
        $subParts = $platform->supportsColumnLengthIndexes() && $this->hasOption('lengths')
            ? $this->getOption('lengths') : [];

106
        $columns = [];
107 108

        foreach ($this->_columns as $column) {
109 110 111 112 113 114 115 116 117
            $length = array_shift($subParts);

            $quotedColumn = $column->getQuotedName($platform);

            if ($length !== null) {
                $quotedColumn .= '(' . $length . ')';
            }

            $columns[] = $quotedColumn;
118 119 120
        }

        return $columns;
121
    }
122

123
    /**
124
     * @return string[]
125 126 127
     */
    public function getUnquotedColumns()
    {
128
        return array_map([$this, 'trimQuotes'], $this->getColumns());
129 130
    }

131 132
    /**
     * Is the index neither unique nor primary key?
133
     *
134
     * @return bool
135 136 137
     */
    public function isSimpleIndex()
    {
138
        return ! $this->_isPrimary && ! $this->_isUnique;
139
    }
140 141

    /**
142
     * @return bool
143 144 145 146 147 148 149
     */
    public function isUnique()
    {
        return $this->_isUnique;
    }

    /**
150
     * @return bool
151 152 153 154 155
     */
    public function isPrimary()
    {
        return $this->_isPrimary;
    }
156 157

    /**
158 159
     * @param string $columnName
     * @param int    $pos
Benjamin Morel's avatar
Benjamin Morel committed
160
     *
161
     * @return bool
162
     */
163
    public function hasColumnAtPosition($columnName, $pos = 0)
164
    {
165 166
        $columnName   = $this->trimQuotes(strtolower($columnName));
        $indexColumns = array_map('strtolower', $this->getUnquotedColumns());
Benjamin Morel's avatar
Benjamin Morel committed
167

168
        return array_search($columnName, $indexColumns) === $pos;
169
    }
170 171

    /**
Benjamin Morel's avatar
Benjamin Morel committed
172
     * Checks if this index exactly spans the given column names in the correct order.
173
     *
174
     * @param string[] $columnNames
Benjamin Morel's avatar
Benjamin Morel committed
175
     *
176
     * @return bool
177 178 179
     */
    public function spansColumns(array $columnNames)
    {
Steve Müller's avatar
Steve Müller committed
180
        $columns         = $this->getColumns();
181
        $numberOfColumns = count($columns);
Steve Müller's avatar
Steve Müller committed
182 183
        $sameColumns     = true;

184
        for ($i = 0; $i < $numberOfColumns; $i++) {
185 186
            if (isset($columnNames[$i]) && $this->trimQuotes(strtolower($columns[$i])) === $this->trimQuotes(strtolower($columnNames[$i]))) {
                continue;
187
            }
188 189

            $sameColumns = false;
190
        }
Steve Müller's avatar
Steve Müller committed
191

192 193 194 195
        return $sameColumns;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
196 197
     * Checks if the other index already fulfills all the indexing and constraint needs of the current one.
     *
198
     * @return bool
199 200 201 202 203
     */
    public function isFullfilledBy(Index $other)
    {
        // allow the other index to be equally large only. It being larger is an option
        // but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo)
204
        if (count($other->getColumns()) !== count($this->getColumns())) {
205 206 207 208 209 210 211
            return false;
        }

        // Check if columns are the same, and even in the same order
        $sameColumns = $this->spansColumns($other->getColumns());

        if ($sameColumns) {
212
            if (! $this->samePartialIndex($other)) {
213
                return false;
214 215
            }

216 217 218 219
            if (! $this->hasSameColumnLengths($other)) {
                return false;
            }

220
            if (! $this->isUnique() && ! $this->isPrimary()) {
Possum's avatar
Possum committed
221
                // this is a special case: If the current key is neither primary or unique, any unique or
222
                // primary key will always have the same effect for the index and there cannot be any constraint
Pascal Borreli's avatar
Pascal Borreli committed
223
                // overlaps. This means a primary or unique index can always fulfill the requirements of just an
224 225
                // index that has no constraints.
                return true;
226 227
            }

228
            if ($other->isPrimary() !== $this->isPrimary()) {
229
                return false;
230 231
            }

Gabriel Caruso's avatar
Gabriel Caruso committed
232
            return $other->isUnique() === $this->isUnique();
233
        }
Benjamin Morel's avatar
Benjamin Morel committed
234

235 236 237 238
        return false;
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
239 240
     * Detects if the other index is a non-unique, non primary index that can be overwritten by this one.
     *
241
     * @return bool
242 243 244
     */
    public function overrules(Index $other)
    {
245 246
        if ($other->isPrimary()) {
            return false;
247 248 249
        }

        if ($this->isSimpleIndex() && $other->isUnique()) {
250 251 252
            return false;
        }

Gabriel Caruso's avatar
Gabriel Caruso committed
253
        return $this->spansColumns($other->getColumns()) && ($this->isPrimary() || $this->isUnique()) && $this->samePartialIndex($other);
254
    }
255

256 257 258
    /**
     * Returns platform specific flags for indexes.
     *
259
     * @return string[]
260 261 262 263 264 265
     */
    public function getFlags()
    {
        return array_keys($this->_flags);
    }

266
    /**
Benjamin Morel's avatar
Benjamin Morel committed
267
     * Adds Flag for an index that translates to platform specific handling.
268 269
     *
     * @param string $flag
Benjamin Morel's avatar
Benjamin Morel committed
270
     *
271
     * @return Index
272 273
     *
     * @example $index->addFlag('CLUSTERED')
274 275 276
     */
    public function addFlag($flag)
    {
Steve Müller's avatar
Steve Müller committed
277
        $this->_flags[strtolower($flag)] = true;
Benjamin Morel's avatar
Benjamin Morel committed
278

279 280 281 282 283 284 285
        return $this;
    }

    /**
     * Does this index have a specific flag?
     *
     * @param string $flag
Benjamin Morel's avatar
Benjamin Morel committed
286
     *
287
     * @return bool
288 289 290
     */
    public function hasFlag($flag)
    {
Steve Müller's avatar
Steve Müller committed
291
        return isset($this->_flags[strtolower($flag)]);
292 293 294
    }

    /**
Benjamin Morel's avatar
Benjamin Morel committed
295
     * Removes a flag.
296 297
     *
     * @param string $flag
Benjamin Morel's avatar
Benjamin Morel committed
298
     *
299 300 301 302
     * @return void
     */
    public function removeFlag($flag)
    {
Steve Müller's avatar
Steve Müller committed
303
        unset($this->_flags[strtolower($flag)]);
304
    }
305 306

    /**
307 308
     * @param string $name
     *
309
     * @return bool
310 311 312
     */
    public function hasOption($name)
    {
313
        return isset($this->options[strtolower($name)]);
314 315 316 317 318 319
    }

    /**
     * @param string $name
     *
     * @return mixed
320
     */
321
    public function getOption($name)
322
    {
323
        return $this->options[strtolower($name)];
324
    }
325 326

    /**
327
     * @return mixed[]
328 329 330 331 332 333 334 335
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * Return whether the two indexes have the same partial index
336
     *
337
     * @return bool
338 339 340
     */
    private function samePartialIndex(Index $other)
    {
341
        if ($this->hasOption('where') && $other->hasOption('where') && $this->getOption('where') === $other->getOption('where')) {
342
            return true;
343 344
        }

Gabriel Caruso's avatar
Gabriel Caruso committed
345
        return ! $this->hasOption('where') && ! $other->hasOption('where');
346
    }
347 348 349 350 351 352 353 354 355 356 357 358 359

    /**
     * Returns whether the index has the same column lengths as the other
     */
    private function hasSameColumnLengths(self $other) : bool
    {
        $filter = static function (?int $length) : bool {
            return $length !== null;
        };

        return array_filter($this->options['lengths'] ?? [], $filter)
            === array_filter($other->options['lengths'] ?? [], $filter);
    }
360
}