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