Index.php 8.7 KB

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