Fix #15621 - Support CloudFront-Forwarded-Proto header
[phpmyadmin.git] / libraries / classes / Normalization.php
blob9acdb688a9aa45bec840052b5961185caa5e18d6
1 <?php
2 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 /**
4 * Holds the PhpMyAdmin\Normalization class
6 * @package PhpMyAdmin
7 */
8 namespace PhpMyAdmin;
10 use PhpMyAdmin\Index;
11 use PhpMyAdmin\Message;
12 use PhpMyAdmin\Relation;
13 use PhpMyAdmin\Template;
14 use PhpMyAdmin\Transformations;
15 use PhpMyAdmin\Url;
16 use PhpMyAdmin\Util;
18 /**
19 * Set of functions used for normalization
21 * @package PhpMyAdmin
23 class Normalization
25 /**
26 * DatabaseInterface instance
28 * @var DatabaseInterface
30 private $dbi;
32 /**
33 * @var Relation $relation
35 private $relation;
37 /**
38 * Constructor
40 * @param DatabaseInterface $dbi DatabaseInterface instance
42 public function __construct(DatabaseInterface $dbi)
44 $this->dbi = $dbi;
45 $this->relation = new Relation();
48 /**
49 * build the html for columns of $colTypeCategory category
50 * in form of given $listType in a table
52 * @param string $db current database
53 * @param string $table current table
54 * @param string $colTypeCategory supported all|Numeric|String|Spatial
55 * |Date and time using the _pgettext() format
56 * @param string $listType type of list to build, supported dropdown|checkbox
58 * @return string HTML for list of columns in form of given list types
60 public function getHtmlForColumnsList(
61 $db,
62 $table,
63 $colTypeCategory = 'all',
64 $listType = 'dropdown'
65 ) {
66 $columnTypeList = [];
67 if ($colTypeCategory != 'all') {
68 $types = $this->dbi->types->getColumns();
69 $columnTypeList = $types[$colTypeCategory];
71 $this->dbi->selectDb($db);
72 $columns = $this->dbi->getColumns(
73 $db,
74 $table,
75 null,
76 true
78 $type = "";
79 $selectColHtml = "";
80 foreach ($columns as $column => $def) {
81 if (isset($def['Type'])) {
82 $extractedColumnSpec = Util::extractColumnSpec($def['Type']);
83 $type = $extractedColumnSpec['type'];
85 if (empty($columnTypeList)
86 || in_array(mb_strtoupper($type), $columnTypeList)
87 ) {
88 if ($listType == 'checkbox') {
89 $selectColHtml .= '<input type="checkbox" value="'
90 . htmlspecialchars($column) . '"/>'
91 . htmlspecialchars($column) . ' [ '
92 . htmlspecialchars($def['Type']) . ' ]</br>';
93 } else {
94 $selectColHtml .= '<option value="' . htmlspecialchars($column) . ''
95 . '">' . htmlspecialchars($column)
96 . ' [ ' . htmlspecialchars($def['Type']) . ' ]'
97 . '</option>';
101 return $selectColHtml;
105 * get the html of the form to add the new column to given table
107 * @param integer $numFields number of columns to add
108 * @param string $db current database
109 * @param string $table current table
110 * @param array $columnMeta array containing default values for the fields
112 * @return string HTML
114 public function getHtmlForCreateNewColumn(
115 $numFields,
116 $db,
117 $table,
118 array $columnMeta = []
120 $cfgRelation = $this->relation->getRelationsParam();
121 $contentCells = [];
122 $availableMime = [];
123 $mimeMap = [];
124 if ($cfgRelation['mimework'] && $GLOBALS['cfg']['BrowseMIME']) {
125 $mimeMap = Transformations::getMIME($db, $table);
126 $availableMime = Transformations::getAvailableMIMEtypes();
128 $commentsMap = $this->relation->getComments($db, $table);
129 for ($columnNumber = 0; $columnNumber < $numFields; $columnNumber++) {
130 $contentCells[$columnNumber] = [
131 'column_number' => $columnNumber,
132 'column_meta' => $columnMeta,
133 'type_upper' => '',
134 'length_values_input_size' => 8,
135 'length' => '',
136 'extracted_columnspec' => [],
137 'submit_attribute' => null,
138 'comments_map' => $commentsMap,
139 'fields_meta' => null,
140 'is_backup' => true,
141 'move_columns' => [],
142 'cfg_relation' => $cfgRelation,
143 'available_mime' => isset($availableMime) ? $availableMime : [],
144 'mime_map' => $mimeMap
148 return Template::get(
149 'columns_definitions/table_fields_definitions'
150 )->render([
151 'is_backup' => true,
152 'fields_meta' => null,
153 'mimework' => $cfgRelation['mimework'],
154 'content_cells' => $contentCells,
155 'change_column' => $_POST['change_column'],
156 'is_virtual_columns_supported' => Util::isVirtualColumnsSupported(),
157 'browse_mime' => $GLOBALS['cfg']['BrowseMIME'],
158 'server_type' => Util::getServerType(),
159 'max_rows' => intval($GLOBALS['cfg']['MaxRows']),
160 'char_editing' => $GLOBALS['cfg']['CharEditing'],
161 'attribute_types' => $this->dbi->types->getAttributes(),
162 'privs_available' => $GLOBALS['col_priv'] && $GLOBALS['is_reload_priv'],
163 'max_length' => $this->dbi->getVersion() >= 50503 ? 1024 : 255,
164 'dbi' => $this->dbi,
165 'disable_is' => $GLOBALS['cfg']['Server']['DisableIS'],
170 * build the html for step 1.1 of normalization
172 * @param string $db current database
173 * @param string $table current table
174 * @param string $normalizedTo up to which step normalization will go,
175 * possible values 1nf|2nf|3nf
177 * @return string HTML for step 1.1
179 public function getHtmlFor1NFStep1($db, $table, $normalizedTo)
181 $step = 1;
182 $stepTxt = __('Make all columns atomic');
183 $html = "<h3 class='center'>"
184 . __('First step of normalization (1NF)') . "</h3>";
185 $html .= "<div id='mainContent' data-normalizeto='" . $normalizedTo . "'>" .
186 "<fieldset>" .
187 "<legend>" . __('Step 1.') . $step . " " . $stepTxt . "</legend>" .
188 "<h4>" . __(
189 'Do you have any column which can be split into more than'
190 . ' one column? '
191 . 'For example: address can be split into street, city, country and zip.'
193 . "</br>(<a class='central_columns_dialog' data-maxrows='25' "
194 . "data-pick=false href='#'> "
195 . __(
196 'Show me the central list of columns that are not already in this table'
197 ) . " </a>)</h4>"
198 . "<p class='cm-em'>" . __(
199 'Select a column which can be split into more '
200 . 'than one (on select of \'no such column\', it\'ll move to next step).'
202 . "</p>"
203 . "<div id='extra'>"
204 . "<select id='selectNonAtomicCol' name='makeAtomic'>"
205 . '<option selected="selected" disabled="disabled">'
206 . __('Select one…') . "</option>"
207 . "<option value='no_such_col'>" . __('No such column') . "</option>"
208 . $this->getHtmlForColumnsList(
209 $db,
210 $table,
211 _pgettext('string types', 'String')
213 . "</select>"
214 . "<span>" . __('split into ')
215 . "</span><input id='numField' type='number' value='2'>"
216 . "<input type='submit' id='splitGo' value='" . __('Go') . "'/></div>"
217 . "<div id='newCols'></div>"
218 . "</fieldset><fieldset class='tblFooters'>"
219 . "</fieldset>"
220 . "</div>";
221 return $html;
225 * build the html contents of various html elements in step 1.2
227 * @param string $db current database
228 * @param string $table current table
230 * @return string HTML contents for step 1.2
232 public function getHtmlContentsFor1NFStep2($db, $table)
234 $step = 2;
235 $stepTxt = __('Have a primary key');
236 $primary = Index::getPrimary($table, $db);
237 $hasPrimaryKey = "0";
238 $legendText = __('Step 1.') . $step . " " . $stepTxt;
239 $extra = '';
240 if ($primary) {
241 $headText = __("Primary key already exists.");
242 $subText = __("Taking you to next step…");
243 $hasPrimaryKey = "1";
244 } else {
245 $headText = __(
246 "There is no primary key; please add one.<br/>"
247 . "Hint: A primary key is a column "
248 . "(or combination of columns) that uniquely identify all rows."
250 $subText = '<a href="#" id="createPrimaryKey">'
251 . Util::getIcon(
252 'b_index_add',
254 'Add a primary key on existing column(s)'
257 . '</a>';
258 $extra = __(
259 "If it's not possible to make existing "
260 . "column combinations as primary key"
261 ) . "<br/>"
262 . '<a href="#" id="addNewPrimary">'
263 . __('+ Add a new primary key column') . '</a>';
265 $res = [
266 'legendText' => $legendText,
267 'headText' => $headText,
268 'subText' => $subText,
269 'hasPrimaryKey' => $hasPrimaryKey,
270 'extra' => $extra
272 return $res;
276 * build the html contents of various html elements in step 1.4
278 * @param string $db current database
279 * @param string $table current table
281 * @return string HTML contents for step 1.4
283 public function getHtmlContentsFor1NFStep4($db, $table)
285 $step = 4;
286 $stepTxt = __('Remove redundant columns');
287 $legendText = __('Step 1.') . $step . " " . $stepTxt;
288 $headText = __(
289 "Do you have a group of columns which on combining gives an existing"
290 . " column? For example, if you have first_name, last_name and"
291 . " full_name then combining first_name and last_name gives full_name"
292 . " which is redundant."
294 $subText = __(
295 "Check the columns which are redundant and click on remove. "
296 . "If no redundant column, click on 'No redundant column'"
298 $extra = $this->getHtmlForColumnsList($db, $table, 'all', "checkbox") . "</br>"
299 . '<input type="submit" id="removeRedundant" value="'
300 . __('Remove selected') . '"/>'
301 . '<input type="submit" value="' . __('No redundant column')
302 . '" onclick="goToFinish1NF();"'
303 . '/>';
304 $res = [
305 'legendText' => $legendText,
306 'headText' => $headText,
307 'subText' => $subText,
308 'extra' => $extra
310 return $res;
314 * build the html contents of various html elements in step 1.3
316 * @param string $db current database
317 * @param string $table current table
319 * @return string HTML contents for step 1.3
321 public function getHtmlContentsFor1NFStep3($db, $table)
323 $step = 3;
324 $stepTxt = __('Move repeating groups');
325 $legendText = __('Step 1.') . $step . " " . $stepTxt;
326 $headText = __(
327 "Do you have a group of two or more columns that are closely "
328 . "related and are all repeating the same attribute? For example, "
329 . "a table that holds data on books might have columns such as book_id, "
330 . "author1, author2, author3 and so on which form a "
331 . "repeating group. In this case a new table (book_id, author) should "
332 . "be created."
334 $subText = __(
335 "Check the columns which form a repeating group. "
336 . "If no such group, click on 'No repeating group'"
338 $extra = $this->getHtmlForColumnsList($db, $table, 'all', "checkbox") . "</br>"
339 . '<input type="submit" id="moveRepeatingGroup" value="'
340 . __('Done') . '"/>'
341 . '<input type="submit" value="' . __('No repeating group')
342 . '" onclick="goToStep4();"'
343 . '/>';
344 $primary = Index::getPrimary($table, $db);
345 $primarycols = $primary->getColumns();
346 $pk = [];
347 foreach ($primarycols as $col) {
348 $pk[] = $col->getName();
350 $res = [
351 'legendText' => $legendText,
352 'headText' => $headText,
353 'subText' => $subText,
354 'extra' => $extra,
355 'primary_key' => json_encode($pk)
357 return $res;
361 * build html contents for 2NF step 2.1
363 * @param string $db current database
364 * @param string $table current table
366 * @return string HTML contents for 2NF step 2.1
368 public function getHtmlFor2NFstep1($db, $table)
370 $legendText = __('Step 2.') . "1 " . __('Find partial dependencies');
371 $primary = Index::getPrimary($table, $db);
372 $primarycols = $primary->getColumns();
373 $pk = [];
374 $subText = '';
375 $selectPkForm = "";
376 $extra = "";
377 foreach ($primarycols as $col) {
378 $pk[] = $col->getName();
379 $selectPkForm .= '<input type="checkbox" name="pd" value="'
380 . htmlspecialchars($col->getName()) . '">'
381 . htmlspecialchars($col->getName());
383 $key = implode(', ', $pk);
384 if (count($primarycols) > 1) {
385 $this->dbi->selectDb($db);
386 $columns = (array) $this->dbi->getColumnNames(
387 $db,
388 $table
390 if (count($pk) == count($columns)) {
391 $headText = sprintf(
393 'No partial dependencies possible as '
394 . 'no non-primary column exists since primary key ( %1$s ) '
395 . 'is composed of all the columns in the table.'
397 htmlspecialchars($key)
398 ) . '<br/>';
399 $extra = '<h3>' . __('Table is already in second normal form.')
400 . '</h3>';
401 } else {
402 $headText = sprintf(
404 'The primary key ( %1$s ) consists of more than one column '
405 . 'so we need to find the partial dependencies.'
407 htmlspecialchars($key)
408 ) . '<br/>' . __(
409 'Please answer the following question(s) '
410 . 'carefully to obtain a correct normalization.'
412 . '<br/><a href="#" id="showPossiblePd">' . __(
413 '+ Show me the possible partial dependencies '
414 . 'based on data in the table'
415 ) . '</a>';
416 $subText = __(
417 'For each column below, '
418 . 'please select the <b>minimal set</b> of columns among given set '
419 . 'whose values combined together are sufficient'
420 . ' to determine the value of the column.'
422 $cnt = 0;
423 foreach ($columns as $column) {
424 if (!in_array($column, $pk)) {
425 $cnt++;
426 $extra .= "<b>" . sprintf(
427 __('\'%1$s\' depends on:'),
428 htmlspecialchars($column)
429 ) . "</b><br>";
430 $extra .= '<form id="pk_' . $cnt . '" data-colname="'
431 . htmlspecialchars($column) . '" class="smallIndent">'
432 . $selectPkForm . '</form><br/><br/>';
436 } else {
437 $headText = sprintf(
439 'No partial dependencies possible as the primary key'
440 . ' ( %1$s ) has just one column.'
442 htmlspecialchars($key)
443 ) . '<br/>';
444 $extra = '<h3>' . __('Table is already in second normal form.') . '</h3>';
446 $res = [
447 'legendText' => $legendText,
448 'headText' => $headText,
449 'subText' => $subText,
450 'extra' => $extra,
451 'primary_key' => $key
453 return $res;
457 * build the html for showing the tables to have in order to put current table in 2NF
459 * @param array $partialDependencies array containing all the dependencies
460 * @param string $table current table
462 * @return string HTML
464 public function getHtmlForNewTables2NF(array $partialDependencies, $table)
466 $html = '<p><b>' . sprintf(
468 'In order to put the '
469 . 'original table \'%1$s\' into Second normal form we need '
470 . 'to create the following tables:'
472 htmlspecialchars($table)
473 ) . '</b></p>';
474 $tableName = $table;
475 $i = 1;
476 foreach ($partialDependencies as $key => $dependents) {
477 $html .= '<p><input type="text" name="' . htmlspecialchars($key)
478 . '" value="' . htmlspecialchars($tableName) . '"/>'
479 . '( <u>' . htmlspecialchars($key) . '</u>'
480 . (count($dependents)>0?', ':'')
481 . htmlspecialchars(implode(', ', $dependents)) . ' )';
482 $i++;
483 $tableName = 'table' . $i;
485 return $html;
489 * create/alter the tables needed for 2NF
491 * @param array $partialDependencies array containing all the partial dependencies
492 * @param object $tablesName name of new tables
493 * @param string $table current table
494 * @param string $db current database
496 * @return array
498 public function createNewTablesFor2NF(array $partialDependencies, $tablesName, $table, $db)
500 $dropCols = false;
501 $nonPKCols = [];
502 $queries = [];
503 $error = false;
504 $headText = '<h3>' . sprintf(
505 __('The second step of normalization is complete for table \'%1$s\'.'),
506 htmlspecialchars($table)
507 ) . '</h3>';
508 if (count((array)$partialDependencies) == 1) {
509 return [
510 'legendText'=>__('End of step'), 'headText'=>$headText,
511 'queryError'=>$error
514 $message = '';
515 $this->dbi->selectDb($db);
516 foreach ($partialDependencies as $key => $dependents) {
517 if ($tablesName->$key != $table) {
518 $backquotedKey = implode(', ', Util::backquote(explode(', ', $key)));
519 $queries[] = 'CREATE TABLE ' . Util::backquote($tablesName->$key)
520 . ' SELECT DISTINCT ' . $backquotedKey
521 . (count($dependents)>0?', ':'')
522 . implode(',', Util::backquote($dependents))
523 . ' FROM ' . Util::backquote($table) . ';';
524 $queries[] = 'ALTER TABLE ' . Util::backquote($tablesName->$key)
525 . ' ADD PRIMARY KEY(' . $backquotedKey . ');';
526 $nonPKCols = array_merge($nonPKCols, $dependents);
527 } else {
528 $dropCols = true;
532 if ($dropCols) {
533 $query = 'ALTER TABLE ' . Util::backquote($table);
534 foreach ($nonPKCols as $col) {
535 $query .= ' DROP ' . Util::backquote($col) . ',';
537 $query = trim($query, ', ');
538 $query .= ';';
539 $queries[] = $query;
540 } else {
541 $queries[] = 'DROP TABLE ' . Util::backquote($table);
543 foreach ($queries as $query) {
544 if (!$this->dbi->tryQuery($query)) {
545 $message = Message::error(__('Error in processing!'));
546 $message->addMessage(
547 Message::rawError(
548 $this->dbi->getError()
550 '<br /><br />'
552 $error = true;
553 break;
556 return [
557 'legendText' => __('End of step'),
558 'headText' => $headText,
559 'queryError' => $error,
560 'extra' => $message
565 * build the html for showing the new tables to have in order
566 * to put given tables in 3NF
568 * @param object $dependencies containing all the dependencies
569 * @param array $tables tables formed after 2NF and need to convert to 3NF
570 * @param string $db current database
572 * @return array containing html and the list of new tables
574 public function getHtmlForNewTables3NF($dependencies, array $tables, $db)
576 $html = "";
577 $i = 1;
578 $newTables = [];
579 foreach ($tables as $table => $arrDependson) {
580 if (count(array_unique($arrDependson)) == 1) {
581 continue;
583 $primary = Index::getPrimary($table, $db);
584 $primarycols = $primary->getColumns();
585 $pk = [];
586 foreach ($primarycols as $col) {
587 $pk[] = $col->getName();
589 $html .= '<p><b>' . sprintf(
591 'In order to put the '
592 . 'original table \'%1$s\' into Third normal form we need '
593 . 'to create the following tables:'
595 htmlspecialchars($table)
596 ) . '</b></p>';
597 $tableName = $table;
598 $columnList = [];
599 foreach ($arrDependson as $key) {
600 $dependents = $dependencies->$key;
601 if ($key == $table) {
602 $key = implode(', ', $pk);
604 $tmpTableCols =array_merge(explode(', ', $key), $dependents);
605 sort($tmpTableCols);
606 if (!in_array($tmpTableCols, $columnList)) {
607 $columnList[] = $tmpTableCols;
608 $html .= '<p><input type="text" name="'
609 . htmlspecialchars($tableName)
610 . '" value="' . htmlspecialchars($tableName) . '"/>'
611 . '( <u>' . htmlspecialchars($key) . '</u>'
612 . (count($dependents)>0?', ':'')
613 . htmlspecialchars(implode(', ', $dependents)) . ' )';
614 $newTables[$table][$tableName] = [
615 "pk"=>$key, "nonpk"=>implode(', ', $dependents)
617 $i++;
618 $tableName = 'table' . $i;
622 return ['html' => $html, 'newTables' => $newTables, 'success' => true];
626 * create new tables or alter existing to get 3NF
628 * @param array $newTables list of new tables to be created
629 * @param string $db current database
631 * @return array
633 public function createNewTablesFor3NF(array $newTables, $db)
635 $queries = [];
636 $dropCols = false;
637 $error = false;
638 $headText = '<h3>' .
639 __('The third step of normalization is complete.')
640 . '</h3>';
641 if (count((array)$newTables) == 0) {
642 return [
643 'legendText'=>__('End of step'), 'headText'=>$headText,
644 'queryError'=>$error
647 $message = '';
648 $this->dbi->selectDb($db);
649 foreach ($newTables as $originalTable => $tablesList) {
650 foreach ($tablesList as $table => $cols) {
651 if ($table != $originalTable) {
652 $quotedPk = implode(
653 ', ',
654 Util::backquote(explode(', ', $cols->pk))
656 $quotedNonpk = implode(
657 ', ',
658 Util::backquote(explode(', ', $cols->nonpk))
660 $queries[] = 'CREATE TABLE ' . Util::backquote($table)
661 . ' SELECT DISTINCT ' . $quotedPk
662 . ', ' . $quotedNonpk
663 . ' FROM ' . Util::backquote($originalTable) . ';';
664 $queries[] = 'ALTER TABLE ' . Util::backquote($table)
665 . ' ADD PRIMARY KEY(' . $quotedPk . ');';
666 } else {
667 $dropCols = $cols;
670 if ($dropCols) {
671 $columns = (array) $this->dbi->getColumnNames(
672 $db,
673 $originalTable
675 $colPresent = array_merge(
676 explode(', ', $dropCols->pk),
677 explode(', ', $dropCols->nonpk)
679 $query = 'ALTER TABLE ' . Util::backquote($originalTable);
680 foreach ($columns as $col) {
681 if (!in_array($col, $colPresent)) {
682 $query .= ' DROP ' . Util::backquote($col) . ',';
685 $query = trim($query, ', ');
686 $query .= ';';
687 $queries[] = $query;
688 } else {
689 $queries[] = 'DROP TABLE ' . Util::backquote($originalTable);
691 $dropCols = false;
693 foreach ($queries as $query) {
694 if (!$this->dbi->tryQuery($query)) {
695 $message = Message::error(__('Error in processing!'));
696 $message->addMessage(
697 Message::rawError(
698 $this->dbi->getError()
700 '<br /><br />'
702 $error = true;
703 break;
706 return [
707 'legendText' => __('End of step'),
708 'headText' => $headText,
709 'queryError' => $error,
710 'extra' => $message
715 * move the repeating group of columns to a new table
717 * @param string $repeatingColumns comma separated list of repeating group columns
718 * @param string $primaryColumns comma separated list of column in primary key
719 * of $table
720 * @param string $newTable name of the new table to be created
721 * @param string $newColumn name of the new column in the new table
722 * @param string $table current table
723 * @param string $db current database
725 * @return array
727 public function moveRepeatingGroup(
728 $repeatingColumns,
729 $primaryColumns,
730 $newTable,
731 $newColumn,
732 $table,
735 $repeatingColumnsArr = (array)Util::backquote(
736 explode(', ', $repeatingColumns)
738 $primaryColumns = implode(
739 ',',
740 Util::backquote(explode(',', $primaryColumns))
742 $query1 = 'CREATE TABLE ' . Util::backquote($newTable);
743 $query2 = 'ALTER TABLE ' . Util::backquote($table);
744 $message = Message::success(
745 sprintf(
746 __('Selected repeating group has been moved to the table \'%s\''),
747 htmlspecialchars($table)
750 $first = true;
751 $error = false;
752 foreach ($repeatingColumnsArr as $repeatingColumn) {
753 if (!$first) {
754 $query1 .= ' UNION ';
756 $first = false;
757 $query1 .= ' SELECT ' . $primaryColumns . ',' . $repeatingColumn
758 . ' as ' . Util::backquote($newColumn)
759 . ' FROM ' . Util::backquote($table);
760 $query2 .= ' DROP ' . $repeatingColumn . ',';
762 $query2 = trim($query2, ',');
763 $queries = [$query1, $query2];
764 $this->dbi->selectDb($db);
765 foreach ($queries as $query) {
766 if (!$this->dbi->tryQuery($query)) {
767 $message = Message::error(__('Error in processing!'));
768 $message->addMessage(
769 Message::rawError(
770 $this->dbi->getError()
772 '<br /><br />'
774 $error = true;
775 break;
778 return [
779 'queryError' => $error, 'message' => $message
784 * build html for 3NF step 1 to find the transitive dependencies
786 * @param string $db current database
787 * @param array $tables tables formed after 2NF and need to process for 3NF
789 * @return string
791 public function getHtmlFor3NFstep1($db, array $tables)
793 $legendText = __('Step 3.') . "1 " . __('Find transitive dependencies');
794 $extra = "";
795 $headText = __(
796 'Please answer the following question(s) '
797 . 'carefully to obtain a correct normalization.'
799 $subText = __(
800 'For each column below, '
801 . 'please select the <b>minimal set</b> of columns among given set '
802 . 'whose values combined together are sufficient'
803 . ' to determine the value of the column.<br />'
804 . 'Note: A column may have no transitive dependency, '
805 . 'in that case you don\'t have to select any.'
807 $cnt = 0;
808 foreach ($tables as $table) {
809 $primary = Index::getPrimary($table, $db);
810 $primarycols = $primary->getColumns();
811 $selectTdForm = "";
812 $pk = [];
813 foreach ($primarycols as $col) {
814 $pk[] = $col->getName();
816 $this->dbi->selectDb($db);
817 $columns = (array) $this->dbi->getColumnNames(
818 $db,
819 $table
821 if (count($columns) - count($pk) <= 1) {
822 continue;
824 foreach ($columns as $column) {
825 if (!in_array($column, $pk)) {
826 $selectTdForm .= '<input type="checkbox" name="pd" value="'
827 . htmlspecialchars($column) . '">'
828 . '<span>' . htmlspecialchars($column) . '</span>';
831 foreach ($columns as $column) {
832 if (!in_array($column, $pk)) {
833 $cnt++;
834 $extra .= "<b>" . sprintf(
835 __('\'%1$s\' depends on:'),
836 htmlspecialchars($column)
838 . "</b><br>";
839 $extra .= '<form id="td_' . $cnt . '" data-colname="'
840 . htmlspecialchars($column) . '" data-tablename="'
841 . htmlspecialchars($table) . '" class="smallIndent">'
842 . $selectTdForm
843 . '</form><br/><br/>';
847 if ($extra == "") {
848 $headText = __(
849 "No Transitive dependencies possible as the table "
850 . "doesn't have any non primary key columns"
852 $subText = "";
853 $extra = "<h3>" . __("Table is already in Third normal form!") . "</h3>";
855 $res = [
856 'legendText' => $legendText,
857 'headText' => $headText,
858 'subText' => $subText,
859 'extra' => $extra
861 return $res;
865 * get html for options to normalize table
867 * @return string HTML
869 public function getHtmlForNormalizeTable()
871 $htmlOutput = '<form method="post" action="normalization.php" '
872 . 'name="normalize" '
873 . 'id="normalizeTable" '
874 . '>'
875 . Url::getHiddenInputs($GLOBALS['db'], $GLOBALS['table'])
876 . '<input type="hidden" name="step1" value="1">';
877 $htmlOutput .= '<fieldset>';
878 $htmlOutput .= '<legend>'
879 . __('Improve table structure (Normalization):') . '</legend>';
880 $htmlOutput .= '<h3>' . __('Select up to what step you want to normalize')
881 . '</h3>';
882 $choices = [
883 '1nf' => __('First step of normalization (1NF)'),
884 '2nf' => __('Second step of normalization (1NF+2NF)'),
885 '3nf' => __('Third step of normalization (1NF+2NF+3NF)')];
887 $htmlOutput .= Util::getRadioFields(
888 'normalizeTo',
889 $choices,
890 '1nf',
891 true
893 $htmlOutput .= '</fieldset><fieldset class="tblFooters">'
894 . "<span class='floatleft'>" . __(
895 'Hint: Please follow the procedure carefully in order '
896 . 'to obtain correct normalization'
897 ) . "</span>"
898 . '<input type="submit" name="submit_normalize" value="' . __('Go') . '" />'
899 . '</fieldset>'
900 . '</form>'
901 . '</div>';
903 return $htmlOutput;
907 * find all the possible partial dependencies based on data in the table.
909 * @param string $table current table
910 * @param string $db current database
912 * @return string HTML containing the list of all the possible partial dependencies
914 public function findPartialDependencies($table, $db)
916 $dependencyList = [];
917 $this->dbi->selectDb($db);
918 $columns = (array) $this->dbi->getColumnNames(
919 $db,
920 $table
922 $columns = (array)Util::backquote($columns);
923 $totalRowsRes = $this->dbi->fetchResult(
924 'SELECT COUNT(*) FROM (SELECT * FROM '
925 . Util::backquote($table) . ' LIMIT 500) as dt;'
927 $totalRows = $totalRowsRes[0];
928 $primary = Index::getPrimary($table, $db);
929 $primarycols = $primary->getColumns();
930 $pk = [];
931 foreach ($primarycols as $col) {
932 $pk[] = Util::backquote($col->getName());
934 $partialKeys = $this->getAllCombinationPartialKeys($pk);
935 $distinctValCount = $this->findDistinctValuesCount(
936 array_unique(
937 array_merge($columns, $partialKeys)
939 $table
941 foreach ($columns as $column) {
942 if (!in_array($column, $pk)) {
943 foreach ($partialKeys as $partialKey) {
944 if ($partialKey
945 && $this->checkPartialDependency(
946 $partialKey,
947 $column,
948 $table,
949 $distinctValCount[$partialKey],
950 $distinctValCount[$column],
951 $totalRows
954 $dependencyList[$partialKey][] = $column;
960 $html = __(
961 'This list is based on a subset of the table\'s data '
962 . 'and is not necessarily accurate. '
964 . '<div class="dependencies_box">';
965 foreach ($dependencyList as $dependon => $colList) {
966 $html .= '<span class="displayblock">'
967 . '<input type="button" class="pickPd" value="' . __('Pick') . '"/>'
968 . '<span class="determinants">'
969 . htmlspecialchars(str_replace('`', '', $dependon)) . '</span> -> '
970 . '<span class="dependents">'
971 . htmlspecialchars(str_replace('`', '', implode(', ', $colList)))
972 . '</span>'
973 . '</span>';
975 if (empty($dependencyList)) {
976 $html .= '<p class="displayblock desc">'
977 . __('No partial dependencies found!') . '</p>';
979 $html .= '</div>';
980 return $html;
984 * check whether a particular column is dependent on given subset of primary key
986 * @param string $partialKey the partial key, subset of primary key,
987 * each column in key supposed to be backquoted
988 * @param string $column backquoted column on whose dependency being checked
989 * @param string $table current table
990 * @param integer $pkCnt distinct value count for given partial key
991 * @param integer $colCnt distinct value count for given column
992 * @param integer $totalRows total distinct rows count of the table
994 * @return boolean TRUE if $column is dependent on $partialKey, False otherwise
996 private function checkPartialDependency(
997 $partialKey,
998 $column,
999 $table,
1000 $pkCnt,
1001 $colCnt,
1002 $totalRows
1004 $query = 'SELECT '
1005 . 'COUNT(DISTINCT ' . $partialKey . ',' . $column . ') as pkColCnt '
1006 . 'FROM (SELECT * FROM ' . Util::backquote($table)
1007 . ' LIMIT 500) as dt' . ';';
1008 $res = $this->dbi->fetchResult($query, null, null);
1009 $pkColCnt = $res[0];
1010 if ($pkCnt && $pkCnt == $colCnt && $colCnt == $pkColCnt) {
1011 return true;
1013 if ($totalRows && $totalRows == $pkCnt) {
1014 return true;
1016 return false;
1020 * function to get distinct values count of all the column in the array $columns
1022 * @param array $columns array of backquoted columns whose distinct values
1023 * need to be counted.
1024 * @param string $table table to which these columns belong
1026 * @return array associative array containing the count
1028 private function findDistinctValuesCount(array $columns, $table)
1030 $result = [];
1031 $query = 'SELECT ';
1032 foreach ($columns as $column) {
1033 if ($column) { //each column is already backquoted
1034 $query .= 'COUNT(DISTINCT ' . $column . ') as \''
1035 . $column . '_cnt\', ';
1038 $query = trim($query, ', ');
1039 $query .= ' FROM (SELECT * FROM ' . Util::backquote($table)
1040 . ' LIMIT 500) as dt' . ';';
1041 $res = $this->dbi->fetchResult($query, null, null);
1042 foreach ($columns as $column) {
1043 if ($column) {
1044 $result[$column] = isset($res[0][$column . '_cnt']) ? $res[0][$column . '_cnt'] : null;
1047 return $result;
1051 * find all the possible partial keys
1053 * @param array $primaryKey array containing all the column present in primary key
1055 * @return array containing all the possible partial keys(subset of primary key)
1057 private function getAllCombinationPartialKeys(array $primaryKey)
1059 $results = [''];
1060 foreach ($primaryKey as $element) {
1061 foreach ($results as $combination) {
1062 array_push(
1063 $results,
1064 trim($element . ',' . $combination, ',')
1068 array_pop($results); //remove key which consist of all primary key columns
1069 return $results;