Merge remote-tracking branch 'origin/QA_4_0' into QA_4_0
[phpmyadmin.git] / libraries / Index.class.php
blobc349ea4d5375c08dc36d7295d4f743930c747ff4
1 <?php
2 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 /**
4 * holds the database index class
6 * @package PhpMyAdmin
7 */
8 if (! defined('PHPMYADMIN')) {
9 exit;
12 /**
14 * @package PhpMyAdmin
15 * @since phpMyAdmin 3.0.0
17 class PMA_Index
19 /**
20 * Class-wide storage container for indexes (caching, singleton)
22 * @var array
24 private static $_registry = array();
26 /**
27 * @var string The name of the schema
29 private $_schema = '';
31 /**
32 * @var string The name of the table
34 private $_table = '';
36 /**
37 * @var string The name of the index
39 private $_name = '';
41 /**
42 * Columns in index
44 * @var array
46 private $_columns = array();
48 /**
49 * The index method used (BTREE, SPATIAL, FULLTEXT, HASH, RTREE).
51 * @var string
53 private $_type = '';
55 /**
56 * The index choice (PRIMARY, UNIQUE, INDEX, SPATIAL, FULLTEXT)
58 * @var string
60 private $_choice = '';
62 /**
63 * Various remarks.
65 * @var string
67 private $_remarks = '';
69 /**
70 * Any comment provided for the index with a COMMENT attribute when the
71 * index was created.
73 * @var string
75 private $_comment = '';
77 /**
78 * @var integer 0 if the index cannot contain duplicates, 1 if it can.
80 private $_non_unique = 0;
82 /**
83 * Indicates how the key is packed. NULL if it is not.
85 * @var string
87 private $_packed = null;
89 /**
90 * Constructor
92 * @param array $params parameters
94 public function __construct($params = array())
96 $this->set($params);
99 /**
100 * Creates(if not already created) and returns the corresponding Index object
102 * @param string $schema database name
103 * @param string $table table name
104 * @param string $index_name index name
106 * @return object corresponding Index object
108 static public function singleton($schema, $table, $index_name = '')
110 PMA_Index::_loadIndexes($table, $schema);
111 if (! isset(PMA_Index::$_registry[$schema][$table][$index_name])) {
112 $index = new PMA_Index;
113 if (strlen($index_name)) {
114 $index->setName($index_name);
115 PMA_Index::$_registry[$schema][$table][$index->getName()] = $index;
117 return $index;
118 } else {
119 return PMA_Index::$_registry[$schema][$table][$index_name];
124 * returns an array with all indexes from the given table
126 * @param string $table table
127 * @param string $schema schema
129 * @return array array of indexes
131 static public function getFromTable($table, $schema)
133 PMA_Index::_loadIndexes($table, $schema);
135 if (isset(PMA_Index::$_registry[$schema][$table])) {
136 return PMA_Index::$_registry[$schema][$table];
137 } else {
138 return array();
143 * return primary if set, false otherwise
145 * @param string $table table
146 * @param string $schema schema
148 * @return mixed primary index or false if no one exists
150 static public function getPrimary($table, $schema)
152 PMA_Index::_loadIndexes($table, $schema);
154 if (isset(PMA_Index::$_registry[$schema][$table]['PRIMARY'])) {
155 return PMA_Index::$_registry[$schema][$table]['PRIMARY'];
156 } else {
157 return false;
162 * Load index data for table
164 * @param string $table table
165 * @param string $schema schema
167 * @return boolean whether loading was successful
169 static private function _loadIndexes($table, $schema)
171 if (isset(PMA_Index::$_registry[$schema][$table])) {
172 return true;
175 $_raw_indexes = PMA_DBI_get_table_indexes($schema, $table);
176 foreach ($_raw_indexes as $_each_index) {
177 $_each_index['Schema'] = $schema;
178 if (! isset(PMA_Index::$_registry[$schema][$table][$_each_index['Key_name']])) {
179 $key = new PMA_Index($_each_index);
180 PMA_Index::$_registry[$schema][$table][$_each_index['Key_name']] = $key;
181 } else {
182 $key = PMA_Index::$_registry[$schema][$table][$_each_index['Key_name']];
185 $key->addColumn($_each_index);
188 return true;
192 * Add column to index
194 * @param array $params column params
196 * @return void
198 public function addColumn($params)
200 if (strlen($params['Column_name'])) {
201 $this->_columns[$params['Column_name']] = new PMA_Index_Column($params);
206 * Adds a list of columns to the index
208 * @param array $columns array containing details about the columns
210 * @return void
212 public function addColumns($columns)
214 $_columns = array();
216 if (isset($columns['names'])) {
217 // coming from form
218 // $columns[names][]
219 // $columns[sub_parts][]
220 foreach ($columns['names'] as $key => $name) {
221 $sub_part = isset($columns['sub_parts'][$key])
222 ? $columns['sub_parts'][$key] : '';
223 $_columns[] = array(
224 'Column_name' => $name,
225 'Sub_part' => $sub_part,
228 } else {
229 // coming from SHOW INDEXES
230 // $columns[][name]
231 // $columns[][sub_part]
232 // ...
233 $_columns = $columns;
236 foreach ($_columns as $column) {
237 $this->addColumn($column);
242 * Returns true if $column indexed in this index
244 * @param string $column the column
246 * @return boolean true if $column indexed in this index
248 public function hasColumn($column)
250 return isset($this->_columns[$column]);
254 * Sets index details
256 * @param array $params index details
258 * @return void
260 public function set($params)
262 if (isset($params['columns'])) {
263 $this->addColumns($params['columns']);
265 if (isset($params['Schema'])) {
266 $this->_schema = $params['Schema'];
268 if (isset($params['Table'])) {
269 $this->_table = $params['Table'];
271 if (isset($params['Key_name'])) {
272 $this->_name = $params['Key_name'];
274 if (isset($params['Index_type'])) {
275 $this->_type = $params['Index_type'];
277 if (isset($params['Comment'])) {
278 $this->_remarks = $params['Comment'];
280 if (isset($params['Index_comment'])) {
281 $this->_comment = $params['Index_comment'];
283 if (isset($params['Non_unique'])) {
284 $this->_non_unique = $params['Non_unique'];
286 if (isset($params['Packed'])) {
287 $this->_packed = $params['Packed'];
289 if ('PRIMARY' == $this->_name) {
290 $this->_choice = 'PRIMARY';
291 } elseif ('FULLTEXT' == $this->_type) {
292 $this->_choice = 'FULLTEXT';
293 } elseif ('SPATIAL' == $this->_type) {
294 $this->_choice = 'SPATIAL';
295 } elseif ('0' == $this->_non_unique) {
296 $this->_choice = 'UNIQUE';
297 } else {
298 $this->_choice = 'INDEX';
303 * Returns the number of columns of the index
305 * @return integer the number of the columns
307 public function getColumnCount()
309 return count($this->_columns);
313 * Returns the index comment
315 * @return string index comment
317 public function getComment()
319 return $this->_comment;
323 * Returns index remarks
325 * @return string index remarks
327 public function getRemarks()
329 return $this->_remarks;
333 * Returns concatenated remarks and comment
335 * @return string concatenated remarks and comment
337 public function getComments()
339 $comments = $this->getRemarks();
340 if (strlen($comments)) {
341 $comments .= "\n";
343 $comments .= $this->getComment();
345 return $comments;
349 * Returns index type ((BTREE, SPATIAL, FULLTEXT, HASH, RTREE)
351 * @return string index type
353 public function getType()
355 return $this->_type;
359 * Returns index choice (PRIMARY, UNIQUE, INDEX, SPATIAL, FULLTEXT)
361 * @return index choice
363 public function getChoice()
365 return $this->_choice;
369 * Return a list of all index choices
371 * @return array index choices
373 static public function getIndexChoices()
375 return array(
376 'PRIMARY',
377 'INDEX',
378 'UNIQUE',
379 'SPATIAL',
380 'FULLTEXT',
385 * Returns HTML for the index choice selector
387 * @return string HTML for the index choice selector
389 public function generateIndexSelector()
391 $html_options = '';
393 foreach (PMA_Index::getIndexChoices() as $each_index_choice) {
394 if ($each_index_choice === 'PRIMARY'
395 && $this->_choice !== 'PRIMARY'
396 && PMA_Index::getPrimary($this->_table, $this->_schema)
398 // skip PRIMARY if there is already one in the table
399 continue;
401 $html_options .= '<option value="' . $each_index_choice . '"'
402 . (($this->_choice == $each_index_choice) ? ' selected="selected"' : '')
403 . '>'. $each_index_choice . '</option>' . "\n";
406 return $html_options;
410 * Returns how the index is packed
412 * @return string how the index is packed
414 public function getPacked()
416 return $this->_packed;
420 * Returns 'No'/false if the index is not packed,
421 * how the index is packed if packed
423 * @param boolean $as_text whether to output should be in text
425 * @return mixed how index is paked
427 public function isPacked($as_text = false)
429 if ($as_text) {
430 $r = array(
431 '0' => __('No'),
432 '1' => __('Yes'),
434 } else {
435 $r = array(
436 '0' => false,
437 '1' => true,
441 if (null === $this->_packed) {
442 return $r[0];
445 return $this->_packed;
449 * Returns integer 0 if the index cannot contain duplicates, 1 if it can
451 * @return integer 0 if the index cannot contain duplicates, 1 if it can
453 public function getNonUnique()
455 return $this->_non_unique;
459 * Returns whether the index is a 'Unique' index
461 * @param boolean $as_text whether to output should be in text
463 * @return mixed whether the index is a 'Unique' index
465 public function isUnique($as_text = false)
467 if ($as_text) {
468 $r = array(
469 '0' => __('Yes'),
470 '1' => __('No'),
472 } else {
473 $r = array(
474 '0' => true,
475 '1' => false,
479 return $r[$this->_non_unique];
483 * Returns the name of the index
485 * @return string the name of the index
487 public function getName()
489 return $this->_name;
493 * Sets the name of the index
495 * @param string $name index name
497 * @return void
499 public function setName($name)
501 $this->_name = (string) $name;
505 * Returns the columns of the index
507 * @return array the columns of the index
509 public function getColumns()
511 return $this->_columns;
515 * Show index data
517 * @param string $table The table name
518 * @param string $schema The schema name
519 * @param boolean $print_mode Whether the output is for the print mode
521 * @return array Index collection array
523 * @access public
525 static public function getView($table, $schema, $print_mode = false)
527 $indexes = PMA_Index::getFromTable($table, $schema);
529 $no_indexes_class = count($indexes) > 0 ? ' hide' : '';
530 $no_indexes = "<div class='no_indexes_defined$no_indexes_class'>";
531 $no_indexes .= PMA_Message::notice(__('No index defined!'))->getDisplay();
532 $no_indexes .= '</div>';
534 if (! $print_mode) {
535 $r = '<fieldset class="index_info">';
536 $r .= '<legend id="index_header">' . __('Indexes');
537 $r .= PMA_Util::showMySQLDocu(
538 'optimization', 'optimizing-database-structure'
541 $r .= '</legend>';
542 $r .= $no_indexes;
543 if (count($indexes) < 1) {
544 $r .= '</fieldset>';
545 return $r;
547 $r .= PMA_Index::findDuplicates($table, $schema);
548 } else {
549 $r = '<h3>' . __('Indexes') . '</h3>';
550 $r .= $no_indexes;
551 if (count($indexes) < 1) {
552 return $r;
555 $r .= '<table id="table_index">';
556 $r .= '<thead>';
557 $r .= '<tr>';
558 if (! $print_mode) {
559 $r .= '<th colspan="2">' . __('Action') . '</th>';
561 $r .= '<th>' . __('Keyname') . '</th>';
562 $r .= '<th>' . __('Type') . '</th>';
563 $r .= '<th>' . __('Unique') . '</th>';
564 $r .= '<th>' . __('Packed') . '</th>';
565 $r .= '<th>' . __('Column') . '</th>';
566 $r .= '<th>' . __('Cardinality') . '</th>';
567 $r .= '<th>' . __('Collation') . '</th>';
568 $r .= '<th>' . __('Null') . '</th>';
569 if (PMA_MYSQL_INT_VERSION > 50500) {
570 $r .= '<th>' . __('Comment') . '</th>';
572 $r .= '</tr>';
573 $r .= '</thead>';
574 $r .= '<tbody>';
576 $odd_row = true;
577 foreach ($indexes as $index) {
578 $row_span = ' rowspan="' . $index->getColumnCount() . '" ';
580 $r .= '<tr class="noclick ' . ($odd_row ? 'odd' : 'even') . '">';
582 if (! $print_mode) {
583 $this_params = $GLOBALS['url_params'];
584 $this_params['index'] = $index->getName();
585 $r .= '<td class="edit_index';
586 $r .= ' ajax';
587 $r .= '" ' . $row_span . '>'
588 . ' <a class="';
589 $r .= 'ajax';
590 $r .= '" href="tbl_indexes.php' . PMA_generate_common_url($this_params)
591 . '">' . PMA_Util::getIcon('b_edit.png', __('Edit')) . '</a>'
592 . '</td>' . "\n";
593 $this_params = $GLOBALS['url_params'];
594 if ($index->getName() == 'PRIMARY') {
595 $this_params['sql_query'] = 'ALTER TABLE '
596 . PMA_Util::backquote($table)
597 . ' DROP PRIMARY KEY;';
598 $this_params['message_to_show']
599 = __('The primary key has been dropped');
600 $js_msg = PMA_jsFormat(
601 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY'
603 } else {
604 $this_params['sql_query'] = 'ALTER TABLE '
605 . PMA_Util::backquote($table) . ' DROP INDEX '
606 . PMA_Util::backquote($index->getName()) . ';';
607 $this_params['message_to_show'] = sprintf(
608 __('Index %s has been dropped'), $index->getName()
611 $js_msg = PMA_jsFormat(
612 'ALTER TABLE ' . $table . ' DROP INDEX '
613 . $index->getName() . ';'
618 $r .= '<td ' . $row_span . '>';
619 $r .= '<input type="hidden" class="drop_primary_key_index_msg"'
620 . ' value="' . $js_msg . '" />';
621 $r .= ' <a class="drop_primary_key_index_anchor';
622 $r .= ' ajax';
623 $r .= '" href="sql.php' . PMA_generate_common_url($this_params)
624 . '" >'
625 . PMA_Util::getIcon('b_drop.png', __('Drop')) . '</a>'
626 . '</td>' . "\n";
629 if (! $print_mode) {
630 $r .= '<th ' . $row_span . '>'
631 . htmlspecialchars($index->getName())
632 . '</th>';
633 } else {
634 $r .= '<td ' . $row_span . '>'
635 . htmlspecialchars($index->getName())
636 . '</td>';
638 $r .= '<td ' . $row_span . '>'
639 . htmlspecialchars($index->getType())
640 . '</td>';
641 $r .= '<td ' . $row_span . '>' . $index->isUnique(true) . '</td>';
642 $r .= '<td ' . $row_span . '>' . $index->isPacked(true) . '</td>';
644 foreach ($index->getColumns() as $column) {
645 if ($column->getSeqInIndex() > 1) {
646 $r .= '<tr class="noclick ' . ($odd_row ? 'odd' : 'even') . '">';
648 $r .= '<td>' . htmlspecialchars($column->getName());
649 if ($column->getSubPart()) {
650 $r .= ' (' . $column->getSubPart() . ')';
652 $r .= '</td>';
653 $r .= '<td>'
654 . htmlspecialchars($column->getCardinality())
655 . '</td>';
656 $r .= '<td>'
657 . htmlspecialchars($column->getCollation())
658 . '</td>';
659 $r .= '<td>'
660 . htmlspecialchars($column->getNull(true))
661 . '</td>';
663 if (PMA_MYSQL_INT_VERSION > 50500
664 && $column->getSeqInIndex() == 1) {
665 $r .= '<td ' . $row_span . '>'
666 . htmlspecialchars($index->getComments()) . '</td>';
668 $r .= '</tr>';
669 } // end foreach $index['Sequences']
671 $odd_row = ! $odd_row;
672 } // end while
673 $r .= '</tbody>';
674 $r .= '</table>';
675 if (! $print_mode) {
676 $r .= '</fieldset>';
679 return $r;
682 public function getCompareData()
684 $data = array(
685 // 'Non_unique' => $this->_non_unique,
686 'Packed' => $this->_packed,
687 'Index_type' => $this->_type,
690 foreach ($this->_columns as $column) {
691 $data['columns'][] = $column->getCompareData();
694 return $data;
698 * Function to check over array of indexes and look for common problems
700 * @param string $table table name
701 * @param string $schema schema name
703 * @return string Output HTML
704 * @access public
706 static public function findDuplicates($table, $schema)
708 $indexes = PMA_Index::getFromTable($table, $schema);
710 $output = '';
712 // count($indexes) < 2:
713 // there is no need to check if there less than two indexes
714 if (count($indexes) < 2) {
715 return $output;
718 // remove last index from stack and ...
719 while ($while_index = array_pop($indexes)) {
720 // ... compare with every remaining index in stack
721 foreach ($indexes as $each_index) {
722 if ($each_index->getCompareData() !== $while_index->getCompareData()) {
723 continue;
726 // did not find any difference
727 // so it makes no sense to have this two equal indexes
729 $message = PMA_Message::notice(
730 __('The indexes %1$s and %2$s seem to be equal and one of them could possibly be removed.')
732 $message->addParam($each_index->getName());
733 $message->addParam($while_index->getName());
734 $output .= $message->getDisplay();
736 // there is no need to check any further indexes if we have already
737 // found that this one has a duplicate
738 continue 2;
741 return $output;
746 * @package PhpMyAdmin
748 class PMA_Index_Column
751 * @var string The column name
753 private $_name = '';
756 * @var integer The column sequence number in the index, starting with 1.
758 private $_seq_in_index = 1;
761 * @var string How the column is sorted in the index. “A” (Ascending) or
762 * NULL (Not sorted)
764 private $_collation = null;
767 * The number of indexed characters if the column is only partly indexed,
768 * NULL if the entire column is indexed.
770 * @var integer
772 private $_sub_part = null;
775 * Contains YES if the column may contain NULL.
776 * If not, the column contains NO.
778 * @var string
780 private $_null = '';
783 * An estimate of the number of unique values in the index. This is updated
784 * by running ANALYZE TABLE or myisamchk -a. Cardinality is counted based on
785 * statistics stored as integers, so the value is not necessarily exact even
786 * for small tables. The higher the cardinality, the greater the chance that
787 * MySQL uses the index when doing joins.
789 * @var integer
791 private $_cardinality = null;
793 public function __construct($params = array())
795 $this->set($params);
798 public function set($params)
800 if (isset($params['Column_name'])) {
801 $this->_name = $params['Column_name'];
803 if (isset($params['Seq_in_index'])) {
804 $this->_seq_in_index = $params['Seq_in_index'];
806 if (isset($params['Collation'])) {
807 $this->_collation = $params['Collation'];
809 if (isset($params['Cardinality'])) {
810 $this->_cardinality = $params['Cardinality'];
812 if (isset($params['Sub_part'])) {
813 $this->_sub_part = $params['Sub_part'];
815 if (isset($params['Null'])) {
816 $this->_null = $params['Null'];
820 public function getName()
822 return $this->_name;
825 public function getCollation()
827 return $this->_collation;
830 public function getCardinality()
832 return $this->_cardinality;
835 public function getNull($as_text = false)
837 return $as_text
838 ? (!$this->_null || $this->_null == 'NO' ? __('No') : __('Yes'))
839 : $this->_null;
842 public function getSeqInIndex()
844 return $this->_seq_in_index;
847 public function getSubPart()
849 return $this->_sub_part;
852 public function getCompareData()
854 return array(
855 'Column_name' => $this->_name,
856 'Seq_in_index' => $this->_seq_in_index,
857 'Collation' => $this->_collation,
858 'Sub_part' => $this->_sub_part,
859 'Null' => $this->_null,