3 declare(strict_types
=1);
7 use PhpMyAdmin\ConfigStorage\Relation
;
8 use PhpMyAdmin\Html\Generator
;
9 use PhpMyAdmin\Query\Compatibility
;
12 use function _pgettext
;
13 use function array_merge
;
14 use function array_pop
;
15 use function array_unique
;
18 use function htmlspecialchars
;
20 use function in_array
;
21 use function is_array
;
22 use function json_encode
;
23 use function mb_strtoupper
;
26 use function str_replace
;
30 * Set of functions used for normalization
34 private readonly Config
$config;
36 public function __construct(
37 private DatabaseInterface
$dbi,
38 private Relation
$relation,
39 private Transformations
$transformations,
40 public Template
$template,
42 $this->config
= Config
::getInstance();
46 * build the html for columns of $colTypeCategory category
47 * in form of given $listType in a table
49 * @param string $db current database
50 * @param string $table current table
51 * @param string $colTypeCategory supported all|Numeric|String|Spatial
52 * |Date and time using the _pgettext() format
53 * @param string $listType type of list to build, supported dropdown|checkbox
55 * @return string HTML for list of columns in form of given list types
57 public function getHtmlForColumnsList(
60 string $colTypeCategory = 'all',
61 string $listType = 'dropdown',
64 if ($colTypeCategory !== 'all') {
65 $types = $this->dbi
->types
->getColumns();
66 $columnTypeList = $types[$colTypeCategory];
67 if (! is_array($columnTypeList)) {
72 $this->dbi
->selectDb($db);
73 $columns = $this->dbi
->getColumns($db, $table, true);
75 foreach ($columns as $def) {
76 $column = $def->field
;
77 $extractedColumnSpec = Util
::extractColumnSpec($def->type
);
80 $columnTypeList !== [] && ! in_array(mb_strtoupper(
81 $extractedColumnSpec['type'],
82 ), $columnTypeList, true)
87 if ($listType === 'checkbox') {
88 $selectColHtml .= '<input type="checkbox" value="'
89 . htmlspecialchars($column) . '">'
90 . htmlspecialchars($column) . ' [ '
91 . htmlspecialchars($def->type
) . ' ]<br>';
93 $selectColHtml .= '<option value="' . htmlspecialchars($column)
94 . '">' . htmlspecialchars($column)
95 . ' [ ' . htmlspecialchars($def->type
) . ' ]'
100 return $selectColHtml;
104 * get the html of the form to add the new column to given table
106 * @param int $numFields number of columns to add
107 * @param string $db current database
108 * @param string $table current table
109 * @param mixed[] $columnMeta array containing default values for the fields
111 * @return string HTML
113 public function getHtmlForCreateNewColumn(
114 UserPrivileges
$userPrivileges,
118 array $columnMeta = [],
120 $relationParameters = $this->relation
->getRelationParameters();
124 if ($relationParameters->browserTransformationFeature
!== null && $this->config
->settings
['BrowseMIME']) {
125 $mimeMap = $this->transformations
->getMime($db, $table);
126 $availableMime = $this->transformations
->getAvailableMimeTypes();
129 $commentsMap = $this->relation
->getComments($db, $table);
130 /** @infection-ignore-all */
131 for ($columnNumber = 0; $columnNumber < $numFields; $columnNumber++
) {
132 $contentCells[$columnNumber] = [
133 'column_number' => $columnNumber,
134 'column_meta' => $columnMeta,
136 'length_values_input_size' => 8,
138 'extracted_columnspec' => [],
139 'submit_attribute' => null,
140 'comments_map' => $commentsMap,
141 'fields_meta' => null,
143 'move_columns' => [],
144 'available_mime' => $availableMime,
145 'mime_map' => $mimeMap,
149 $charsets = Charsets
::getCharsets($this->dbi
, $this->config
->selectedServer
['DisableIS']);
150 $collations = Charsets
::getCollations($this->dbi
, $this->config
->selectedServer
['DisableIS']);
152 foreach ($charsets as $charset) {
153 $collationsList = [];
154 foreach ($collations[$charset->getName()] as $collation) {
155 $collationsList[] = ['name' => $collation->getName(), 'description' => $collation->getDescription()];
159 'name' => $charset->getName(),
160 'description' => $charset->getDescription(),
161 'collations' => $collationsList,
165 return $this->template
->render('columns_definitions/table_fields_definitions', [
167 'fields_meta' => null,
168 'relation_parameters' => $relationParameters,
169 'content_cells' => $contentCells,
170 'change_column' => $_POST['change_column'] ??
$_GET['change_column'] ??
null,
171 'is_virtual_columns_supported' => Compatibility
::isVirtualColumnsSupported($this->dbi
->getVersion()),
172 'browse_mime' => $this->config
->settings
['BrowseMIME'],
173 'supports_stored_keyword' => Compatibility
::supportsStoredKeywordForVirtualColumns(
174 $this->dbi
->getVersion(),
176 'server_version' => $this->dbi
->getVersion(),
177 'max_rows' => (int) $this->config
->settings
['MaxRows'],
178 'char_editing' => $this->config
->settings
['CharEditing'],
179 'attribute_types' => $this->dbi
->types
->getAttributes(),
180 'privs_available' => $userPrivileges->column
&& $userPrivileges->isReload
,
181 'max_length' => $this->dbi
->getVersion() >= 50503 ?
1024 : 255,
182 'charsets' => $charsetsList,
187 * build the html for step 1.1 of normalization
189 * @param string $db current database
190 * @param string $table current table
191 * @param string $normalizedTo up to which step normalization will go,
192 * possible values 1nf|2nf|3nf
194 * @return string HTML for step 1.1
196 public function getHtmlFor1NFStep1(string $db, string $table, string $normalizedTo): string
199 $stepTxt = __('Make all columns atomic');
200 $html = '<h3>' . __('First step of normalization (1NF)') . '</h3>';
201 $html .= '<div class="card" id="mainContent" data-normalizeto="' . $normalizedTo . '">'
202 . '<div class="card-header">' . __('Step 1.') . $step . ' ' . $stepTxt . '</div>'
203 . '<div class="card-body">'
205 'Do you have any column which can be split into more than one column?'
206 . ' For example: address can be split into street, city, country and zip.',
208 . "<br>(<a class='central_columns_dialog' data-maxrows='25' "
209 . "data-pick=false href='#'> "
210 . __('Show me the central list of columns that are not already in this table') . ' </a>)</h4>'
211 . "<p class='cm-em'>" . __(
212 'Select a column which can be split into more '
213 . 'than one (on select of \'no such column\', it\'ll move to next step).',
217 . "<select id='selectNonAtomicCol' name='makeAtomic'>"
218 . '<option selected="selected" disabled="disabled">'
219 . __('Select one…') . '</option>'
220 . "<option value='no_such_col'>" . __('No such column') . '</option>'
221 . $this->getHtmlForColumnsList(
224 _pgettext('string types', 'String'),
227 . '<span>' . __('split into ')
228 . "</span><input id='numField' type='number' value='2'>"
229 . '<input type="submit" class="btn btn-primary" id="splitGo" value="' . __('Go') . '"></div>'
230 . "<div id='newCols'></div>"
231 . '</div><div class="card-footer"></div>'
238 * build the html contents of various html elements in step 1.2
240 * @param string $db current database
241 * @param string $table current table
243 * @return array{legendText: string, headText: string, subText: string, hasPrimaryKey: string, extra: string}
245 public function getHtmlContentsFor1NFStep2(string $db, string $table): array
248 $stepTxt = __('Have a primary key');
249 $primary = Index
::getPrimary($this->dbi
, $table, $db);
250 $hasPrimaryKey = '0';
251 $legendText = __('Step 1.') . $step . ' ' . $stepTxt;
253 if ($primary !== null) {
254 $headText = __('Primary key already exists.');
255 $subText = __('Taking you to next step…');
256 $hasPrimaryKey = '1';
259 'There is no primary key; please add one.<br>'
260 . 'Hint: A primary key is a column '
261 . '(or combination of columns) that uniquely identify all rows.',
263 $subText = '<a href="#" id="createPrimaryKey">'
264 . Generator
::getIcon(
267 'Add a primary key on existing column(s)',
271 $extra = __('If it\'s not possible to make existing column combinations as primary key') . '<br>'
272 . '<a href="#" id="addNewPrimary">'
273 . __('+ Add a new primary key column') . '</a>';
277 'legendText' => $legendText,
278 'headText' => $headText,
279 'subText' => $subText,
280 'hasPrimaryKey' => $hasPrimaryKey,
286 * build the html contents of various html elements in step 1.4
288 * @param string $db current database
289 * @param string $table current table
291 * @return array{legendText: string, headText: string, subText: string, extra: string} HTML contents for step 1.4
293 public function getHtmlContentsFor1NFStep4(string $db, string $table): array
296 $stepTxt = __('Remove redundant columns');
297 $legendText = __('Step 1.') . $step . ' ' . $stepTxt;
299 'Do you have a group of columns which on combining gives an existing'
300 . ' column? For example, if you have first_name, last_name and'
301 . ' full_name then combining first_name and last_name gives full_name'
302 . ' which is redundant.',
305 'Check the columns which are redundant and click on remove. '
306 . "If no redundant column, click on 'No redundant column'",
308 $extra = $this->getHtmlForColumnsList($db, $table, 'all', 'checkbox') . '<br>'
309 . '<input class="btn btn-secondary" type="submit" id="removeRedundant" value="'
310 . __('Remove selected') . '">'
311 . '<input class="btn btn-secondary" type="submit" id="noRedundantColumn" value="'
312 . __('No redundant column') . '">';
314 return ['legendText' => $legendText, 'headText' => $headText, 'subText' => $subText, 'extra' => $extra];
318 * build the html contents of various html elements in step 1.3
320 * @param string $db current database
321 * @param string $table current table
323 * @return array{legendText: string, headText: string, subText: string, extra: string, primary_key: false|string}
325 public function getHtmlContentsFor1NFStep3(string $db, string $table): array
328 $stepTxt = __('Move repeating groups');
329 $legendText = __('Step 1.') . $step . ' ' . $stepTxt;
331 'Do you have a group of two or more columns that are closely '
332 . 'related and are all repeating the same attribute? For example, '
333 . 'a table that holds data on books might have columns such as book_id, '
334 . 'author1, author2, author3 and so on which form a '
335 . 'repeating group. In this case a new table (book_id, author) should '
339 'Check the columns which form a repeating group. If no such group, click on \'No repeating group\'',
341 $extra = $this->getHtmlForColumnsList($db, $table, 'all', 'checkbox') . '<br>'
342 . '<input class="btn btn-secondary" type="submit" id="moveRepeatingGroup" value="'
344 . '<input class="btn btn-secondary" type="submit" value="' . __('No repeating group')
345 . '" id="noRepeatingGroup">';
346 $primary = Index
::getPrimary($this->dbi
, $table, $db);
347 $primarycols = $primary === null ?
[] : $primary->getColumns();
349 foreach ($primarycols as $col) {
350 $pk[] = $col->getName();
354 'legendText' => $legendText,
355 'headText' => $headText,
356 'subText' => $subText,
358 'primary_key' => json_encode($pk),
363 * build html contents for 2NF step 2.1
365 * @param string $db current database
366 * @param string $table current table
368 * @return array{legendText: string, headText: string, subText: string, extra: string, primary_key: string}
370 public function getHtmlFor2NFstep1(string $db, string $table): array
372 $legendText = __('Step 2.') . '1 ' . __('Find partial dependencies');
373 $primary = Index
::getPrimary($this->dbi
, $table, $db);
374 $primarycols = $primary === null ?
[] : $primary->getColumns();
379 foreach ($primarycols as $col) {
380 $pk[] = $col->getName();
381 $selectPkForm .= '<input type="checkbox" name="pd" value="'
382 . htmlspecialchars($col->getName()) . '">'
383 . htmlspecialchars($col->getName());
386 $key = implode(', ', $pk);
387 if (count($primarycols) > 1) {
388 $this->dbi
->selectDb($db);
389 $columns = $this->dbi
->getColumnNames($db, $table);
390 if (count($pk) === count($columns)) {
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),
399 $extra = '<h3>' . __('Table is already in second normal form.')
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>' . __('Please answer the following question(s) carefully to obtain a correct normalization.')
409 . '<br><a href="#" id="showPossiblePd">' . __(
410 '+ Show me the possible partial dependencies based on data in the table',
413 'For each column below, '
414 . 'please select the <b>minimal set</b> of columns among given set '
415 . 'whose values combined together are sufficient'
416 . ' to determine the value of the column.',
419 foreach ($columns as $column) {
420 if (in_array($column, $pk, true)) {
425 $extra .= '<b>' . sprintf(
426 __('\'%1$s\' depends on:'),
427 htmlspecialchars($column),
429 $extra .= '<form id="pk_' . $cnt . '" data-colname="'
430 . htmlspecialchars($column) . '" class="smallIndent">'
431 . $selectPkForm . '</form><br><br>';
437 'No partial dependencies possible as the primary key ( %1$s ) has just one column.',
439 htmlspecialchars($key),
441 $extra = '<h3>' . __('Table is already in second normal form.') . '</h3>';
445 'legendText' => $legendText,
446 'headText' => $headText,
447 'subText' => $subText,
449 'primary_key' => $key,
454 * build the html for showing the tables to have in order to put current table in 2NF
456 * @param mixed[] $partialDependencies array containing all the dependencies
457 * @param string $table current table
459 * @return string HTML
461 public function getHtmlForNewTables2NF(array $partialDependencies, string $table): string
463 $html = '<p><b>' . sprintf(
465 'In order to put the '
466 . 'original table \'%1$s\' into Second normal form we need '
467 . 'to create the following tables:',
469 htmlspecialchars($table),
473 foreach ($partialDependencies as $key => $dependents) {
474 $html .= '<p><input type="text" name="' . htmlspecialchars($key)
475 . '" value="' . htmlspecialchars($tableName) . '">'
476 . '( <u>' . htmlspecialchars($key) . '</u>'
477 . (count($dependents) > 0 ?
', ' : '')
478 . htmlspecialchars(implode(', ', $dependents)) . ' )';
480 $tableName = 'table' . $i;
487 * create/alter the tables needed for 2NF
489 * @param mixed[] $partialDependencies array containing all the partial dependencies
490 * @param object $tablesName name of new tables
491 * @param string $table current table
492 * @param string $db current database
494 * @return array{legendText: string, headText: string, queryError: bool, extra: Message}
496 public function createNewTablesFor2NF(
497 array $partialDependencies,
506 $headText = '<h3>' . sprintf(
507 __('The second step of normalization is complete for table \'%1$s\'.'),
508 htmlspecialchars($table),
510 if (count($partialDependencies) === 1) {
511 return ['legendText' => __('End of step'), 'headText' => $headText, 'queryError' => false];
515 $this->dbi
->selectDb($db);
516 foreach ($partialDependencies as $key => $dependents) {
517 if ($tablesName->$key != $table) {
518 $keys = explode(', ', $key);
520 foreach ($keys as $eachKey) {
521 $quotedKeys[] = Util
::backquote($eachKey);
524 $backquotedKey = implode(', ', $quotedKeys);
526 $quotedDependents = [];
527 foreach ($dependents as $dependent) {
528 $quotedDependents[] = Util
::backquote($dependent);
531 $queries[] = 'CREATE TABLE ' . Util
::backquote($tablesName->$key)
532 . ' SELECT DISTINCT ' . $backquotedKey
533 . (count($dependents) > 0 ?
', ' : '')
534 . implode(',', $quotedDependents)
535 . ' FROM ' . Util
::backquote($table) . ';';
536 $queries[] = 'ALTER TABLE ' . Util
::backquote($tablesName->$key)
537 . ' ADD PRIMARY KEY(' . $backquotedKey . ');';
538 $nonPKCols = array_merge($nonPKCols, $dependents);
545 $query = 'ALTER TABLE ' . Util
::backquote($table);
546 foreach ($nonPKCols as $col) {
547 $query .= ' DROP ' . Util
::backquote($col) . ',';
550 $query = trim($query, ', ');
554 $queries[] = 'DROP TABLE ' . Util
::backquote($table);
557 foreach ($queries as $query) {
558 if (! $this->dbi
->tryQuery($query)) {
559 $message = Message
::error(__('Error in processing!'));
560 $message->addMessage(
561 Message
::rawError($this->dbi
->getError()),
570 'legendText' => __('End of step'),
571 'headText' => $headText,
572 'queryError' => $error,
578 * build the html for showing the new tables to have in order
579 * to put given tables in 3NF
581 * @param object $dependencies containing all the dependencies
582 * @param mixed[] $tables tables formed after 2NF and need to convert to 3NF
583 * @param string $db current database
585 * @return array{html:string, newTables:mixed[], success:true} containing html and the list of new tables
587 public function getHtmlForNewTables3NF(object $dependencies, array $tables, string $db): array
592 foreach ($tables as $table => $arrDependson) {
593 if (count(array_unique($arrDependson)) === 1) {
597 $primary = Index
::getPrimary($this->dbi
, $table, $db);
598 $primarycols = $primary === null ?
[] : $primary->getColumns();
600 foreach ($primarycols as $col) {
601 $pk[] = $col->getName();
604 $html .= '<p><b>' . sprintf(
606 'In order to put the '
607 . 'original table \'%1$s\' into Third normal form we need '
608 . 'to create the following tables:',
610 htmlspecialchars($table),
614 foreach ($arrDependson as $key) {
615 $dependents = $dependencies->$key;
616 if ($key == $table) {
617 $key = implode(', ', $pk);
620 $tmpTableCols = array_merge(explode(', ', $key), $dependents);
622 if (in_array($tmpTableCols, $columnList)) {
626 $columnList[] = $tmpTableCols;
627 $html .= '<p><input type="text" name="'
628 . htmlspecialchars($tableName)
629 . '" value="' . htmlspecialchars($tableName) . '">'
630 . '( <u>' . htmlspecialchars($key) . '</u>'
631 . (count($dependents) > 0 ?
', ' : '')
632 . htmlspecialchars(implode(', ', $dependents)) . ' )';
633 $newTables[$table][$tableName] = ['pk' => $key, 'nonpk' => implode(', ', $dependents)];
635 $tableName = 'table' . $i;
639 return ['html' => $html, 'newTables' => $newTables, 'success' => true];
643 * create new tables or alter existing to get 3NF
645 * @param mixed[] $newTables list of new tables to be created
646 * @param string $db current database
648 * @return array{legendText: string, headText: string, queryError: string|false, extra?: string}
650 public function createNewTablesFor3NF(array $newTables, string $db): array
655 $headText = '<h3>' . __('The third step of normalization is complete.') . '</h3>';
656 if ($newTables === []) {
657 return ['legendText' => __('End of step'), 'headText' => $headText, 'queryError' => false];
661 $this->dbi
->selectDb($db);
662 foreach ($newTables as $originalTable => $tablesList) {
663 foreach ($tablesList as $table => $cols) {
664 if ($table != $originalTable) {
665 $pkArray = explode(', ', $cols['pk']);
667 foreach ($pkArray as $pk) {
668 $quotedPkArray[] = Util
::backquote($pk);
671 $quotedPk = implode(', ', $quotedPkArray);
673 $nonpkArray = explode(', ', $cols['nonpk']);
674 $quotedNonpkArray = [];
675 foreach ($nonpkArray as $nonpk) {
676 $quotedNonpkArray[] = Util
::backquote($nonpk);
679 $quotedNonpk = implode(', ', $quotedNonpkArray);
681 $queries[] = 'CREATE TABLE ' . Util
::backquote($table)
682 . ' SELECT DISTINCT ' . $quotedPk
683 . ', ' . $quotedNonpk
684 . ' FROM ' . Util
::backquote($originalTable) . ';';
685 $queries[] = 'ALTER TABLE ' . Util
::backquote($table)
686 . ' ADD PRIMARY KEY(' . $quotedPk . ');';
693 $columns = $this->dbi
->getColumnNames($db, $originalTable);
694 $colPresent = array_merge(
695 explode(', ', $dropCols['pk']),
696 explode(', ', $dropCols['nonpk']),
698 $query = 'ALTER TABLE ' . Util
::backquote($originalTable);
699 foreach ($columns as $col) {
700 if (in_array($col, $colPresent, true)) {
704 $query .= ' DROP ' . Util
::backquote($col) . ',';
707 $query = trim($query, ', ');
711 $queries[] = 'DROP TABLE ' . Util
::backquote($originalTable);
717 foreach ($queries as $query) {
718 if (! $this->dbi
->tryQuery($query)) {
719 $message = Message
::error(__('Error in processing!'));
720 $message->addMessage(
721 Message
::rawError($this->dbi
->getError()),
730 'legendText' => __('End of step'),
731 'headText' => $headText,
732 'queryError' => $error,
738 * move the repeating group of columns to a new table
740 * @param string $repeatingColumns comma separated list of repeating group columns
741 * @param string $primaryColumns comma separated list of column in primary key
743 * @param string $newTable name of the new table to be created
744 * @param string $newColumn name of the new column in the new table
745 * @param string $table current table
746 * @param string $db current database
748 * @return array{queryError: bool, message: Message}
750 public function moveRepeatingGroup(
751 string $repeatingColumns,
752 string $primaryColumns,
758 $repeatingColumnsArr = explode(', ', $repeatingColumns);
759 $primaryColumnsArray = explode(',', $primaryColumns);
761 foreach ($primaryColumnsArray as $column) {
762 $columns[] = Util
::backquote($column);
765 $primaryColumns = implode(',', $columns);
766 $query1 = 'CREATE TABLE ' . Util
::backquote($newTable);
767 $query2 = 'ALTER TABLE ' . Util
::backquote($table);
768 $message = Message
::success(
770 __('Selected repeating group has been moved to the table \'%s\''),
771 htmlspecialchars($table),
776 foreach ($repeatingColumnsArr as $repeatingColumn) {
778 $query1 .= ' UNION ';
782 $quotedRepeatingColumn = Util
::backquote($repeatingColumn);
783 $query1 .= ' SELECT ' . $primaryColumns . ',' . $quotedRepeatingColumn
784 . ' as ' . Util
::backquote($newColumn)
785 . ' FROM ' . Util
::backquote($table);
786 $query2 .= ' DROP ' . $quotedRepeatingColumn . ',';
789 $query2 = trim($query2, ',');
790 $queries = [$query1, $query2];
791 $this->dbi
->selectDb($db);
792 foreach ($queries as $query) {
793 if (! $this->dbi
->tryQuery($query)) {
794 $message = Message
::error(__('Error in processing!'));
795 $message->addMessage(
796 Message
::rawError($this->dbi
->getError()),
804 return ['queryError' => $error, 'message' => $message];
808 * build html for 3NF step 1 to find the transitive dependencies
810 * @param string $db current database
811 * @param mixed[] $tables tables formed after 2NF and need to process for 3NF
813 * @return array{legendText: string, headText: string, subText: string, extra: string}
815 public function getHtmlFor3NFstep1(string $db, array $tables): array
817 $legendText = __('Step 3.') . '1 ' . __('Find transitive dependencies');
819 $headText = __('Please answer the following question(s) carefully to obtain a correct normalization.');
821 'For each column below, '
822 . 'please select the <b>minimal set</b> of columns among given set '
823 . 'whose values combined together are sufficient'
824 . ' to determine the value of the column.<br>'
825 . 'Note: A column may have no transitive dependency, '
826 . 'in that case you don\'t have to select any.',
829 $this->dbi
->selectDb($db);
830 foreach ($tables as $table) {
831 $primary = Index
::getPrimary($this->dbi
, $table, $db);
832 $primarycols = $primary === null ?
[] : $primary->getColumns();
835 foreach ($primarycols as $col) {
836 $pk[] = $col->getName();
839 $columns = $this->dbi
->getColumnNames($db, $table);
840 if (count($columns) - count($pk) <= 1) {
844 foreach ($columns as $column) {
845 if (in_array($column, $pk, true)) {
849 $selectTdForm .= '<input type="checkbox" name="pd" value="'
850 . htmlspecialchars($column) . '">'
851 . '<span>' . htmlspecialchars($column) . '</span>';
854 foreach ($columns as $column) {
855 if (in_array($column, $pk, true)) {
860 $extra .= '<b>' . sprintf(
861 __('\'%1$s\' depends on:'),
862 htmlspecialchars($column),
865 $extra .= '<form id="td_' . $cnt . '" data-colname="'
866 . htmlspecialchars($column) . '" data-tablename="'
867 . htmlspecialchars($table) . '" class="smallIndent">'
875 'No Transitive dependencies possible as the table doesn\'t have any non primary key columns',
878 $extra = '<h3>' . __('Table is already in Third normal form!') . '</h3>';
881 return ['legendText' => $legendText, 'headText' => $headText, 'subText' => $subText, 'extra' => $extra];
885 * find all the possible partial dependencies based on data in the table.
887 * @param string $table current table
888 * @param string $db current database
890 * @return string HTML containing the list of all the possible partial dependencies
892 public function findPartialDependencies(string $table, string $db): string
894 $dependencyList = [];
895 $this->dbi
->selectDb($db);
896 $columnNames = $this->dbi
->getColumnNames($db, $table);
898 foreach ($columnNames as $column) {
899 $columns[] = Util
::backquote($column);
902 $totalRowsRes = $this->dbi
->fetchResult(
903 'SELECT COUNT(*) FROM (SELECT * FROM '
904 . Util
::backquote($table) . ' LIMIT 500) as dt;',
906 $totalRows = $totalRowsRes[0];
907 $primary = Index
::getPrimary($this->dbi
, $table, $db);
908 $primarycols = $primary === null ?
[] : $primary->getColumns();
910 foreach ($primarycols as $col) {
911 $pk[] = Util
::backquote($col->getName());
914 $partialKeys = $this->getAllCombinationPartialKeys($pk);
915 $distinctValCount = $this->findDistinctValuesCount(
917 array_merge($columns, $partialKeys),
921 foreach ($columns as $column) {
922 if (in_array($column, $pk, true)) {
926 foreach ($partialKeys as $partialKey) {
929 ||
! $this->checkPartialDependency(
933 $distinctValCount[$partialKey],
934 $distinctValCount[$column],
941 $dependencyList[$partialKey][] = $column;
945 $html = __('This list is based on a subset of the table\'s data and is not necessarily accurate. ')
946 . '<div class="dependencies_box">';
947 foreach ($dependencyList as $dependon => $colList) {
948 $html .= '<span class="d-block">'
949 . '<input type="button" class="btn btn-secondary pickPd" value="' . __('Pick') . '">'
950 . '<span class="determinants">'
951 . htmlspecialchars(str_replace('`', '', (string) $dependon)) . '</span> -> '
952 . '<span class="dependents">'
953 . htmlspecialchars(str_replace('`', '', implode(', ', $colList)))
958 if ($dependencyList === []) {
959 $html .= '<p class="d-block m-1">'
960 . __('No partial dependencies found!') . '</p>';
969 * check whether a particular column is dependent on given subset of primary key
971 * @param string $partialKey the partial key, subset of primary key,
972 * each column in key supposed to be backquoted
973 * @param string $column backquoted column on whose dependency being checked
974 * @param string $table current table
975 * @param int $pkCnt distinct value count for given partial key
976 * @param int $colCnt distinct value count for given column
977 * @param int $totalRows total distinct rows count of the table
979 private function checkPartialDependency(
988 . 'COUNT(DISTINCT ' . $partialKey . ',' . $column . ') as pkColCnt '
989 . 'FROM (SELECT * FROM ' . Util
::backquote($table)
990 . ' LIMIT 500) as dt;';
991 $res = $this->dbi
->fetchResult($query);
993 if ($pkCnt !== 0 && $pkCnt === $colCnt && $colCnt == $pkColCnt) {
997 return $totalRows !== 0 && $totalRows === $pkCnt;
1001 * function to get distinct values count of all the column in the array $columns
1003 * @param string[] $columns array of backquoted columns whose distinct values
1004 * need to be counted.
1005 * @param string $table table to which these columns belong
1007 * @return int[] associative array containing the count
1009 private function findDistinctValuesCount(array $columns, string $table): array
1013 foreach ($columns as $column) {
1014 if ($column === '') {
1018 //each column is already backquoted
1019 $query .= 'COUNT(DISTINCT ' . $column . ') as \''
1020 . $column . '_cnt\', ';
1023 $query = trim($query, ', ');
1024 $query .= ' FROM (SELECT * FROM ' . Util
::backquote($table)
1025 . ' LIMIT 500) as dt;';
1026 $res = $this->dbi
->fetchResult($query);
1027 foreach ($columns as $column) {
1028 if ($column === '') {
1032 $result[$column] = (int) ($res[0][$column . '_cnt'] ??
null);
1039 * find all the possible partial keys
1041 * @param mixed[] $primaryKey array containing all the column present in primary key
1043 * @return string[] containing all the possible partial keys(subset of primary key)
1045 private function getAllCombinationPartialKeys(array $primaryKey): array
1048 foreach ($primaryKey as $element) {
1049 foreach ($results as $combination) {
1050 $results[] = trim($element . ',' . $combination, ',');
1054 array_pop($results); //remove key which consist of all primary key columns