Minor improvement to prior commit.
[openemr.git] / phpmyadmin / libraries / Index.class.php
blob555bbabd534ae14b7be178318afdea07ae054a59
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 /**
13 * Index manipulation class
15 * @package PhpMyAdmin
16 * @since phpMyAdmin 3.0.0
18 class PMA_Index
20 const PRIMARY = 1;
21 const UNIQUE = 2;
22 const INDEX = 4;
23 const SPATIAL = 8;
24 const FULLTEXT = 16;
26 /**
27 * Class-wide storage container for indexes (caching, singleton)
29 * @var array
31 private static $_registry = array();
33 /**
34 * @var string The name of the schema
36 private $_schema = '';
38 /**
39 * @var string The name of the table
41 private $_table = '';
43 /**
44 * @var string The name of the index
46 private $_name = '';
48 /**
49 * Columns in index
51 * @var array
53 private $_columns = array();
55 /**
56 * The index method used (BTREE, HASH, RTREE).
58 * @var string
60 private $_type = '';
62 /**
63 * The index choice (PRIMARY, UNIQUE, INDEX, SPATIAL, FULLTEXT)
65 * @var string
67 private $_choice = '';
69 /**
70 * Various remarks.
72 * @var string
74 private $_remarks = '';
76 /**
77 * Any comment provided for the index with a COMMENT attribute when the
78 * index was created.
80 * @var string
82 private $_comment = '';
84 /**
85 * @var integer 0 if the index cannot contain duplicates, 1 if it can.
87 private $_non_unique = 0;
89 /**
90 * Indicates how the key is packed. NULL if it is not.
92 * @var string
94 private $_packed = null;
96 /**
97 * Block size for the index
99 * @var int
101 private $_key_block_size = null;
104 * Parser option for the index
106 * @var string
108 private $_parser = null;
111 * Constructor
113 * @param array $params parameters
115 public function __construct($params = array())
117 $this->set($params);
121 * Creates(if not already created) and returns the corresponding Index object
123 * @param string $schema database name
124 * @param string $table table name
125 * @param string $index_name index name
127 * @return PMA_Index corresponding Index object
129 static public function singleton($schema, $table, $index_name = '')
131 PMA_Index::_loadIndexes($table, $schema);
132 if (! isset(PMA_Index::$_registry[$schema][$table][$index_name])) {
133 $index = new PMA_Index;
134 if (/*overload*/mb_strlen($index_name)) {
135 $index->setName($index_name);
136 PMA_Index::$_registry[$schema][$table][$index->getName()] = $index;
138 return $index;
139 } else {
140 return PMA_Index::$_registry[$schema][$table][$index_name];
145 * returns an array with all indexes from the given table
147 * @param string $table table
148 * @param string $schema schema
150 * @return PMA_Index[] array of indexes
152 static public function getFromTable($table, $schema)
154 PMA_Index::_loadIndexes($table, $schema);
156 if (isset(PMA_Index::$_registry[$schema][$table])) {
157 return PMA_Index::$_registry[$schema][$table];
158 } else {
159 return array();
164 * Returns an array with all indexes from the given table of the requested types
166 * @param string $table table
167 * @param string $schema schema
168 * @param int $choices choices
170 * @return PMA_Index[] array of indexes
172 static public function getFromTableByChoice($table, $schema, $choices = 31)
174 $indexes = array();
175 foreach (self::getFromTable($table, $schema) as $index) {
176 if (($choices & PMA_Index::PRIMARY)
177 && $index->getChoice() == 'PRIMARY'
179 $indexes[] = $index;
181 if (($choices & PMA_Index::UNIQUE)
182 && $index->getChoice() == 'UNIQUE'
184 $indexes[] = $index;
186 if (($choices & PMA_Index::INDEX)
187 && $index->getChoice() == 'INDEX'
189 $indexes[] = $index;
191 if (($choices & PMA_Index::SPATIAL)
192 && $index->getChoice() == 'SPATIAL'
194 $indexes[] = $index;
196 if (($choices & PMA_Index::FULLTEXT)
197 && $index->getChoice() == 'FULLTEXT'
199 $indexes[] = $index;
202 return $indexes;
206 * return primary if set, false otherwise
208 * @param string $table table
209 * @param string $schema schema
211 * @return mixed primary index or false if no one exists
213 static public function getPrimary($table, $schema)
215 PMA_Index::_loadIndexes($table, $schema);
217 if (isset(PMA_Index::$_registry[$schema][$table]['PRIMARY'])) {
218 return PMA_Index::$_registry[$schema][$table]['PRIMARY'];
219 } else {
220 return false;
225 * Load index data for table
227 * @param string $table table
228 * @param string $schema schema
230 * @return boolean whether loading was successful
232 static private function _loadIndexes($table, $schema)
234 if (isset(PMA_Index::$_registry[$schema][$table])) {
235 return true;
238 $_raw_indexes = $GLOBALS['dbi']->getTableIndexes($schema, $table);
239 foreach ($_raw_indexes as $_each_index) {
240 $_each_index['Schema'] = $schema;
241 $keyName = $_each_index['Key_name'];
242 if (! isset(PMA_Index::$_registry[$schema][$table][$keyName])) {
243 $key = new PMA_Index($_each_index);
244 PMA_Index::$_registry[$schema][$table][$keyName] = $key;
245 } else {
246 $key = PMA_Index::$_registry[$schema][$table][$keyName];
249 $key->addColumn($_each_index);
252 return true;
256 * Add column to index
258 * @param array $params column params
260 * @return void
262 public function addColumn($params)
264 if (isset($params['Column_name'])
265 && /*overload*/mb_strlen($params['Column_name'])
267 $this->_columns[$params['Column_name']] = new PMA_Index_Column($params);
272 * Adds a list of columns to the index
274 * @param array $columns array containing details about the columns
276 * @return void
278 public function addColumns($columns)
280 $_columns = array();
282 if (isset($columns['names'])) {
283 // coming from form
284 // $columns[names][]
285 // $columns[sub_parts][]
286 foreach ($columns['names'] as $key => $name) {
287 $sub_part = isset($columns['sub_parts'][$key])
288 ? $columns['sub_parts'][$key] : '';
289 $_columns[] = array(
290 'Column_name' => $name,
291 'Sub_part' => $sub_part,
294 } else {
295 // coming from SHOW INDEXES
296 // $columns[][name]
297 // $columns[][sub_part]
298 // ...
299 $_columns = $columns;
302 foreach ($_columns as $column) {
303 $this->addColumn($column);
308 * Returns true if $column indexed in this index
310 * @param string $column the column
312 * @return boolean true if $column indexed in this index
314 public function hasColumn($column)
316 return isset($this->_columns[$column]);
320 * Sets index details
322 * @param array $params index details
324 * @return void
326 public function set($params)
328 if (isset($params['columns'])) {
329 $this->addColumns($params['columns']);
331 if (isset($params['Schema'])) {
332 $this->_schema = $params['Schema'];
334 if (isset($params['Table'])) {
335 $this->_table = $params['Table'];
337 if (isset($params['Key_name'])) {
338 $this->_name = $params['Key_name'];
340 if (isset($params['Index_type'])) {
341 $this->_type = $params['Index_type'];
343 if (isset($params['Comment'])) {
344 $this->_remarks = $params['Comment'];
346 if (isset($params['Index_comment'])) {
347 $this->_comment = $params['Index_comment'];
349 if (isset($params['Non_unique'])) {
350 $this->_non_unique = $params['Non_unique'];
352 if (isset($params['Packed'])) {
353 $this->_packed = $params['Packed'];
355 if (isset($params['Index_choice'])) {
356 $this->_choice = $params['Index_choice'];
357 } else {
358 if ('PRIMARY' == $this->_name) {
359 $this->_choice = 'PRIMARY';
360 } elseif ('FULLTEXT' == $this->_type) {
361 $this->_choice = 'FULLTEXT';
362 $this->_type = '';
363 } elseif ('SPATIAL' == $this->_type) {
364 $this->_choice = 'SPATIAL';
365 $this->_type = '';
366 } elseif ('0' == $this->_non_unique) {
367 $this->_choice = 'UNIQUE';
368 } else {
369 $this->_choice = 'INDEX';
372 if (isset($params['Key_block_size'])) {
373 $this->_key_block_size = $params['Key_block_size'];
375 if (isset($params['Parser'])) {
376 $this->_parser = $params['Parser'];
381 * Returns the number of columns of the index
383 * @return integer the number of the columns
385 public function getColumnCount()
387 return count($this->_columns);
391 * Returns the index comment
393 * @return string index comment
395 public function getComment()
397 return $this->_comment;
401 * Returns index remarks
403 * @return string index remarks
405 public function getRemarks()
407 return $this->_remarks;
411 * Return the key block size
413 * @return number
415 public function getKeyBlockSize()
417 return $this->_key_block_size;
421 * Return the parser
423 * @return string
425 public function getParser()
427 return $this->_parser;
431 * Returns concatenated remarks and comment
433 * @return string concatenated remarks and comment
435 public function getComments()
437 $comments = $this->getRemarks();
438 if (/*overload*/mb_strlen($comments)) {
439 $comments .= "\n";
441 $comments .= $this->getComment();
443 return $comments;
447 * Returns index type (BTREE, HASH, RTREE)
449 * @return string index type
451 public function getType()
453 return $this->_type;
457 * Returns index choice (PRIMARY, UNIQUE, INDEX, SPATIAL, FULLTEXT)
459 * @return string index choice
461 public function getChoice()
463 return $this->_choice;
467 * Return a list of all index choices
469 * @return string[] index choices
471 static public function getIndexChoices()
473 return array(
474 'PRIMARY',
475 'INDEX',
476 'UNIQUE',
477 'SPATIAL',
478 'FULLTEXT',
483 * Returns a lit of all index types
485 * @return string[] index types
487 static public function getIndexTypes()
489 return array(
490 'BTREE',
491 'HASH'
496 * Returns HTML for the index choice selector
498 * @param boolean $edit_table whether this is table editing
500 * @return string HTML for the index choice selector
502 public function generateIndexChoiceSelector($edit_table)
504 $html_options = '<select name="index[Index_choice]"'
505 . ' id="select_index_choice" '
506 . ($edit_table ? 'disabled="disabled"' : '') . '>';
508 foreach (PMA_Index::getIndexChoices() as $each_index_choice) {
509 if ($each_index_choice === 'PRIMARY'
510 && $this->_choice !== 'PRIMARY'
511 && PMA_Index::getPrimary($this->_table, $this->_schema)
513 // skip PRIMARY if there is already one in the table
514 continue;
516 $html_options .= '<option value="' . $each_index_choice . '"'
517 . (($this->_choice == $each_index_choice)
518 ? ' selected="selected"'
519 : '')
520 . '>' . $each_index_choice . '</option>' . "\n";
522 $html_options .= '</select>';
524 return $html_options;
528 * Returns HTML for the index type selector
530 * @return string HTML for the index type selector
532 public function generateIndexTypeSelector()
534 $types = array("" => "--");
535 foreach (PMA_Index::getIndexTypes() as $type) {
536 $types[$type] = $type;
539 return PMA_Util::getDropdown(
540 "index[Index_type]", $types,
541 $this->_type, "select_index_type"
546 * Returns how the index is packed
548 * @return string how the index is packed
550 public function getPacked()
552 return $this->_packed;
556 * Returns 'No'/false if the index is not packed,
557 * how the index is packed if packed
559 * @param boolean $as_text whether to output should be in text
561 * @return mixed how index is packed
563 public function isPacked($as_text = false)
565 if ($as_text) {
566 $r = array(
567 '0' => __('No'),
568 '1' => __('Yes'),
570 } else {
571 $r = array(
572 '0' => false,
573 '1' => true,
577 if (null === $this->_packed) {
578 return $r[0];
581 return $this->_packed;
585 * Returns integer 0 if the index cannot contain duplicates, 1 if it can
587 * @return integer 0 if the index cannot contain duplicates, 1 if it can
589 public function getNonUnique()
591 return $this->_non_unique;
595 * Returns whether the index is a 'Unique' index
597 * @param boolean $as_text whether to output should be in text
599 * @return mixed whether the index is a 'Unique' index
601 public function isUnique($as_text = false)
603 if ($as_text) {
604 $r = array(
605 '0' => __('Yes'),
606 '1' => __('No'),
608 } else {
609 $r = array(
610 '0' => true,
611 '1' => false,
615 return $r[$this->_non_unique];
619 * Returns the name of the index
621 * @return string the name of the index
623 public function getName()
625 return $this->_name;
629 * Sets the name of the index
631 * @param string $name index name
633 * @return void
635 public function setName($name)
637 $this->_name = (string) $name;
641 * Returns the columns of the index
643 * @return PMA_Index_Column[] the columns of the index
645 public function getColumns()
647 return $this->_columns;
651 * Show index data
653 * @param string $table The table name
654 * @param string $schema The schema name
655 * @param boolean $print_mode Whether the output is for the print mode
657 * @return string HTML for showing index
659 * @access public
661 static public function getHtmlForIndexes($table, $schema, $print_mode = false)
663 $indexes = PMA_Index::getFromTable($table, $schema);
665 $no_indexes_class = count($indexes) > 0 ? ' hide' : '';
666 $no_indexes = "<div class='no_indexes_defined$no_indexes_class'>";
667 $no_indexes .= PMA_Message::notice(__('No index defined!'))->getDisplay();
668 $no_indexes .= '</div>';
670 if (! $print_mode) {
671 $r = '<fieldset class="index_info">';
672 $r .= '<legend id="index_header">' . __('Indexes');
673 $r .= PMA_Util::showMySQLDocu('optimizing-database-structure');
675 $r .= '</legend>';
676 $r .= $no_indexes;
677 if (count($indexes) < 1) {
678 $r .= '</fieldset>';
679 return $r;
681 $r .= PMA_Index::findDuplicates($table, $schema);
682 } else {
683 $r = '<h3>' . __('Indexes') . '</h3>';
684 $r .= $no_indexes;
685 if (count($indexes) < 1) {
686 return $r;
689 $r .= '<table id="table_index">';
690 $r .= '<thead>';
691 $r .= '<tr>';
692 if (! $print_mode) {
693 $r .= '<th colspan="2" class="print_ignore">' . __('Action') . '</th>';
695 $r .= '<th>' . __('Keyname') . '</th>';
696 $r .= '<th>' . __('Type') . '</th>';
697 $r .= '<th>' . __('Unique') . '</th>';
698 $r .= '<th>' . __('Packed') . '</th>';
699 $r .= '<th>' . __('Column') . '</th>';
700 $r .= '<th>' . __('Cardinality') . '</th>';
701 $r .= '<th>' . __('Collation') . '</th>';
702 $r .= '<th>' . __('Null') . '</th>';
703 $r .= '<th>' . __('Comment') . '</th>';
704 $r .= '</tr>';
705 $r .= '</thead>';
706 $r .= '<tbody>';
708 $odd_row = true;
709 foreach ($indexes as $index) {
710 $row_span = ' rowspan="' . $index->getColumnCount() . '" ';
712 $r .= '<tr class="noclick ' . ($odd_row ? 'odd' : 'even') . '">';
714 if (! $print_mode) {
715 $this_params = $GLOBALS['url_params'];
716 $this_params['index'] = $index->getName();
717 $r .= '<td class="edit_index print_ignore';
718 $r .= ' ajax';
719 $r .= '" ' . $row_span . '>'
720 . ' <a class="';
721 $r .= 'ajax';
722 $r .= '" href="tbl_indexes.php' . PMA_URL_getCommon($this_params)
723 . '">' . PMA_Util::getIcon('b_edit.png', __('Edit')) . '</a>'
724 . '</td>' . "\n";
725 $this_params = $GLOBALS['url_params'];
726 if ($index->getName() == 'PRIMARY') {
727 $this_params['sql_query'] = 'ALTER TABLE '
728 . PMA_Util::backquote($table)
729 . ' DROP PRIMARY KEY;';
730 $this_params['message_to_show']
731 = __('The primary key has been dropped.');
732 $js_msg = PMA_jsFormat(
733 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY'
735 } else {
736 $this_params['sql_query'] = 'ALTER TABLE '
737 . PMA_Util::backquote($table) . ' DROP INDEX '
738 . PMA_Util::backquote($index->getName()) . ';';
739 $this_params['message_to_show'] = sprintf(
740 __('Index %s has been dropped.'), $index->getName()
743 $js_msg = PMA_jsFormat(
744 'ALTER TABLE ' . $table . ' DROP INDEX '
745 . $index->getName() . ';'
750 $r .= '<td ' . $row_span . ' class="print_ignore">';
751 $r .= '<input type="hidden" class="drop_primary_key_index_msg"'
752 . ' value="' . $js_msg . '" />';
753 $r .= ' <a class="drop_primary_key_index_anchor';
754 $r .= ' ajax';
755 $r .= '" href="sql.php' . PMA_URL_getCommon($this_params)
756 . '" >'
757 . PMA_Util::getIcon('b_drop.png', __('Drop')) . '</a>'
758 . '</td>' . "\n";
761 if (! $print_mode) {
762 $r .= '<th ' . $row_span . '>'
763 . htmlspecialchars($index->getName())
764 . '</th>';
765 } else {
766 $r .= '<td ' . $row_span . '>'
767 . htmlspecialchars($index->getName())
768 . '</td>';
770 $r .= '<td ' . $row_span . '>';
771 $type = $index->getType();
772 if (! empty($type)) {
773 $r .= htmlspecialchars($type);
774 } else {
775 $r .= htmlspecialchars($index->getChoice());
777 $r .= '</td>';
778 $r .= '<td ' . $row_span . '>' . $index->isUnique(true) . '</td>';
779 $r .= '<td ' . $row_span . '>' . $index->isPacked(true) . '</td>';
781 foreach ($index->getColumns() as $column) {
782 if ($column->getSeqInIndex() > 1) {
783 $r .= '<tr class="noclick ' . ($odd_row ? 'odd' : 'even') . '">';
785 $r .= '<td>' . htmlspecialchars($column->getName());
786 if ($column->getSubPart()) {
787 $r .= ' (' . $column->getSubPart() . ')';
789 $r .= '</td>';
790 $r .= '<td>'
791 . htmlspecialchars($column->getCardinality())
792 . '</td>';
793 $r .= '<td>'
794 . htmlspecialchars($column->getCollation())
795 . '</td>';
796 $r .= '<td>'
797 . htmlspecialchars($column->getNull(true))
798 . '</td>';
800 if ($column->getSeqInIndex() == 1
802 $r .= '<td ' . $row_span . '>'
803 . htmlspecialchars($index->getComments()) . '</td>';
805 $r .= '</tr>';
806 } // end foreach $index['Sequences']
808 $odd_row = ! $odd_row;
809 } // end while
810 $r .= '</tbody>';
811 $r .= '</table>';
812 if (! $print_mode) {
813 $r .= '</fieldset>';
816 return $r;
820 * Gets the properties in an array for comparison purposes
822 * @return array an array containing the properties of the index
824 public function getCompareData()
826 $data = array(
827 // 'Non_unique' => $this->_non_unique,
828 'Packed' => $this->_packed,
829 'Index_choice' => $this->_choice,
832 foreach ($this->_columns as $column) {
833 $data['columns'][] = $column->getCompareData();
836 return $data;
840 * Function to check over array of indexes and look for common problems
842 * @param string $table table name
843 * @param string $schema schema name
845 * @return string Output HTML
846 * @access public
848 static public function findDuplicates($table, $schema)
850 $indexes = PMA_Index::getFromTable($table, $schema);
852 $output = '';
854 // count($indexes) < 2:
855 // there is no need to check if there less than two indexes
856 if (count($indexes) < 2) {
857 return $output;
860 // remove last index from stack and ...
861 while ($while_index = array_pop($indexes)) {
862 // ... compare with every remaining index in stack
863 foreach ($indexes as $each_index) {
864 if ($each_index->getCompareData() !== $while_index->getCompareData()
866 continue;
869 // did not find any difference
870 // so it makes no sense to have this two equal indexes
872 $message = PMA_Message::notice(
874 'The indexes %1$s and %2$s seem to be equal and one of them '
875 . 'could possibly be removed.'
878 $message->addParam($each_index->getName());
879 $message->addParam($while_index->getName());
880 $output .= $message->getDisplay();
882 // there is no need to check any further indexes if we have already
883 // found that this one has a duplicate
884 continue 2;
887 return $output;
892 * Index column wrapper
894 * @package PhpMyAdmin
896 class PMA_Index_Column
899 * @var string The column name
901 private $_name = '';
904 * @var integer The column sequence number in the index, starting with 1.
906 private $_seq_in_index = 1;
909 * @var string How the column is sorted in the index. “A” (Ascending) or
910 * NULL (Not sorted)
912 private $_collation = null;
915 * The number of indexed characters if the column is only partly indexed,
916 * NULL if the entire column is indexed.
918 * @var integer
920 private $_sub_part = null;
923 * Contains YES if the column may contain NULL.
924 * If not, the column contains NO.
926 * @var string
928 private $_null = '';
931 * An estimate of the number of unique values in the index. This is updated
932 * by running ANALYZE TABLE or myisamchk -a. Cardinality is counted based on
933 * statistics stored as integers, so the value is not necessarily exact even
934 * for small tables. The higher the cardinality, the greater the chance that
935 * MySQL uses the index when doing joins.
937 * @var integer
939 private $_cardinality = null;
942 * Constructor
944 * @param array $params an array containing the parameters of the index column
946 public function __construct($params = array())
948 $this->set($params);
952 * Sets parameters of the index column
954 * @param array $params an array containing the parameters of the index column
956 * @return void
958 public function set($params)
960 if (isset($params['Column_name'])) {
961 $this->_name = $params['Column_name'];
963 if (isset($params['Seq_in_index'])) {
964 $this->_seq_in_index = $params['Seq_in_index'];
966 if (isset($params['Collation'])) {
967 $this->_collation = $params['Collation'];
969 if (isset($params['Cardinality'])) {
970 $this->_cardinality = $params['Cardinality'];
972 if (isset($params['Sub_part'])) {
973 $this->_sub_part = $params['Sub_part'];
975 if (isset($params['Null'])) {
976 $this->_null = $params['Null'];
981 * Returns the column name
983 * @return string column name
985 public function getName()
987 return $this->_name;
991 * Return the column collation
993 * @return string column collation
995 public function getCollation()
997 return $this->_collation;
1001 * Returns the cardinality of the column
1003 * @return int cardinality of the column
1005 public function getCardinality()
1007 return $this->_cardinality;
1011 * Returns whether the column is nullable
1013 * @param boolean $as_text whether to returned the string representation
1015 * @return mixed nullability of the column. True/false or Yes/No depending
1016 * on the value of the $as_text parameter
1018 public function getNull($as_text = false)
1020 return $as_text
1021 ? (!$this->_null || $this->_null == 'NO' ? __('No') : __('Yes'))
1022 : $this->_null;
1026 * Returns the sequence number of the column in the index
1028 * @return int sequence number of the column in the index
1030 public function getSeqInIndex()
1032 return $this->_seq_in_index;
1036 * Returns the number of indexed characters if the column is only
1037 * partly indexed
1039 * @return int the number of indexed characters
1041 public function getSubPart()
1043 return $this->_sub_part;
1047 * Gets the properties in an array for comparison purposes
1049 * @return array an array containing the properties of the index column
1051 public function getCompareData()
1053 return array(
1054 'Column_name' => $this->_name,
1055 'Seq_in_index' => $this->_seq_in_index,
1056 'Collation' => $this->_collation,
1057 'Sub_part' => $this->_sub_part,
1058 'Null' => $this->_null,