PageRenderTime 76ms CodeModel.GetById 38ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Normalization.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 1147 lines | 877 code | 89 blank | 181 comment | 59 complexity | 288b72373f78d21bdbf14f96b5a34271 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use PhpMyAdmin\Charsets\Charset;
  5. use PhpMyAdmin\Charsets\Collation;
  6. use PhpMyAdmin\Html\Generator;
  7. use PhpMyAdmin\Query\Compatibility;
  8. use function __;
  9. use function _pgettext;
  10. use function array_merge;
  11. use function array_pop;
  12. use function array_unique;
  13. use function count;
  14. use function explode;
  15. use function htmlspecialchars;
  16. use function implode;
  17. use function in_array;
  18. use function intval;
  19. use function is_array;
  20. use function json_encode;
  21. use function mb_strtoupper;
  22. use function sort;
  23. use function sprintf;
  24. use function str_replace;
  25. use function trim;
  26. /**
  27. * Set of functions used for normalization
  28. */
  29. class Normalization
  30. {
  31. /**
  32. * DatabaseInterface instance
  33. *
  34. * @var DatabaseInterface
  35. */
  36. private $dbi;
  37. /** @var Relation */
  38. private $relation;
  39. /** @var Transformations */
  40. private $transformations;
  41. /** @var Template */
  42. public $template;
  43. /**
  44. * @param DatabaseInterface $dbi DatabaseInterface instance
  45. * @param Relation $relation Relation instance
  46. * @param Transformations $transformations Transformations instance
  47. * @param Template $template Template instance
  48. */
  49. public function __construct(
  50. DatabaseInterface $dbi,
  51. Relation $relation,
  52. Transformations $transformations,
  53. Template $template
  54. ) {
  55. $this->dbi = $dbi;
  56. $this->relation = $relation;
  57. $this->transformations = $transformations;
  58. $this->template = $template;
  59. }
  60. /**
  61. * build the html for columns of $colTypeCategory category
  62. * in form of given $listType in a table
  63. *
  64. * @param string $db current database
  65. * @param string $table current table
  66. * @param string $colTypeCategory supported all|Numeric|String|Spatial
  67. * |Date and time using the _pgettext() format
  68. * @param string $listType type of list to build, supported dropdown|checkbox
  69. *
  70. * @return string HTML for list of columns in form of given list types
  71. */
  72. public function getHtmlForColumnsList(
  73. $db,
  74. $table,
  75. $colTypeCategory = 'all',
  76. $listType = 'dropdown'
  77. ) {
  78. $columnTypeList = [];
  79. if ($colTypeCategory !== 'all') {
  80. $types = $this->dbi->types->getColumns();
  81. $columnTypeList = $types[$colTypeCategory];
  82. if (! is_array($columnTypeList)) {
  83. $columnTypeList = [];
  84. }
  85. }
  86. $this->dbi->selectDb($db);
  87. $columns = $this->dbi->getColumns($db, $table, null, true);
  88. $type = '';
  89. $selectColHtml = '';
  90. foreach ($columns as $column => $def) {
  91. if (isset($def['Type'])) {
  92. $extractedColumnSpec = Util::extractColumnSpec($def['Type']);
  93. $type = $extractedColumnSpec['type'];
  94. }
  95. if (! empty($columnTypeList) && ! in_array(mb_strtoupper($type), $columnTypeList)) {
  96. continue;
  97. }
  98. if ($listType === 'checkbox') {
  99. $selectColHtml .= '<input type="checkbox" value="'
  100. . htmlspecialchars($column) . '">'
  101. . htmlspecialchars($column) . ' [ '
  102. . htmlspecialchars($def['Type']) . ' ]<br>';
  103. } else {
  104. $selectColHtml .= '<option value="' . htmlspecialchars($column) . ''
  105. . '">' . htmlspecialchars($column)
  106. . ' [ ' . htmlspecialchars($def['Type']) . ' ]'
  107. . '</option>';
  108. }
  109. }
  110. return $selectColHtml;
  111. }
  112. /**
  113. * get the html of the form to add the new column to given table
  114. *
  115. * @param int $numFields number of columns to add
  116. * @param string $db current database
  117. * @param string $table current table
  118. * @param array $columnMeta array containing default values for the fields
  119. *
  120. * @return string HTML
  121. */
  122. public function getHtmlForCreateNewColumn(
  123. $numFields,
  124. $db,
  125. $table,
  126. array $columnMeta = []
  127. ) {
  128. $cfgRelation = $this->relation->getRelationsParam();
  129. $contentCells = [];
  130. $availableMime = [];
  131. $mimeMap = [];
  132. if ($cfgRelation['mimework'] && $GLOBALS['cfg']['BrowseMIME']) {
  133. $mimeMap = $this->transformations->getMime($db, $table);
  134. $availableMimeTypes = $this->transformations->getAvailableMimeTypes();
  135. if ($availableMimeTypes !== null) {
  136. $availableMime = $availableMimeTypes;
  137. }
  138. }
  139. $commentsMap = $this->relation->getComments($db, $table);
  140. for ($columnNumber = 0; $columnNumber < $numFields; $columnNumber++) {
  141. $contentCells[$columnNumber] = [
  142. 'column_number' => $columnNumber,
  143. 'column_meta' => $columnMeta,
  144. 'type_upper' => '',
  145. 'length_values_input_size' => 8,
  146. 'length' => '',
  147. 'extracted_columnspec' => [],
  148. 'submit_attribute' => null,
  149. 'comments_map' => $commentsMap,
  150. 'fields_meta' => null,
  151. 'is_backup' => true,
  152. 'move_columns' => [],
  153. 'cfg_relation' => $cfgRelation,
  154. 'available_mime' => $availableMime,
  155. 'mime_map' => $mimeMap,
  156. ];
  157. }
  158. $charsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
  159. $collations = Charsets::getCollations($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']);
  160. $charsetsList = [];
  161. /** @var Charset $charset */
  162. foreach ($charsets as $charset) {
  163. $collationsList = [];
  164. /** @var Collation $collation */
  165. foreach ($collations[$charset->getName()] as $collation) {
  166. $collationsList[] = [
  167. 'name' => $collation->getName(),
  168. 'description' => $collation->getDescription(),
  169. ];
  170. }
  171. $charsetsList[] = [
  172. 'name' => $charset->getName(),
  173. 'description' => $charset->getDescription(),
  174. 'collations' => $collationsList,
  175. ];
  176. }
  177. return $this->template->render('columns_definitions/table_fields_definitions', [
  178. 'is_backup' => true,
  179. 'fields_meta' => null,
  180. 'mimework' => $cfgRelation['mimework'],
  181. 'content_cells' => $contentCells,
  182. 'change_column' => $_POST['change_column'] ?? $_GET['change_column'] ?? null,
  183. 'is_virtual_columns_supported' => Compatibility::isVirtualColumnsSupported($this->dbi->getVersion()),
  184. 'browse_mime' => $GLOBALS['cfg']['BrowseMIME'],
  185. 'supports_stored_keyword' => Compatibility::supportsStoredKeywordForVirtualColumns(
  186. $this->dbi->getVersion()
  187. ),
  188. 'server_version' => $this->dbi->getVersion(),
  189. 'max_rows' => intval($GLOBALS['cfg']['MaxRows']),
  190. 'char_editing' => $GLOBALS['cfg']['CharEditing'],
  191. 'attribute_types' => $this->dbi->types->getAttributes(),
  192. 'privs_available' => $GLOBALS['col_priv'] && $GLOBALS['is_reload_priv'],
  193. 'max_length' => $this->dbi->getVersion() >= 50503 ? 1024 : 255,
  194. 'charsets' => $charsetsList,
  195. ]);
  196. }
  197. /**
  198. * build the html for step 1.1 of normalization
  199. *
  200. * @param string $db current database
  201. * @param string $table current table
  202. * @param string $normalizedTo up to which step normalization will go,
  203. * possible values 1nf|2nf|3nf
  204. *
  205. * @return string HTML for step 1.1
  206. */
  207. public function getHtmlFor1NFStep1($db, $table, $normalizedTo)
  208. {
  209. $step = 1;
  210. $stepTxt = __('Make all columns atomic');
  211. $html = "<h3 class='text-center'>"
  212. . __('First step of normalization (1NF)') . '</h3>';
  213. $html .= "<div id='mainContent' data-normalizeto='" . $normalizedTo . "'>" .
  214. '<fieldset class="pma-fieldset">' .
  215. '<legend>' . __('Step 1.') . $step . ' ' . $stepTxt . '</legend>' .
  216. '<h4>' . __(
  217. 'Do you have any column which can be split into more than'
  218. . ' one column? '
  219. . 'For example: address can be split into street, city, country and zip.'
  220. )
  221. . "<br>(<a class='central_columns_dialog' data-maxrows='25' "
  222. . "data-pick=false href='#'> "
  223. . __('Show me the central list of columns that are not already in this table') . ' </a>)</h4>'
  224. . "<p class='cm-em'>" . __(
  225. 'Select a column which can be split into more '
  226. . 'than one (on select of \'no such column\', it\'ll move to next step).'
  227. )
  228. . '</p>'
  229. . "<div id='extra'>"
  230. . "<select id='selectNonAtomicCol' name='makeAtomic'>"
  231. . '<option selected="selected" disabled="disabled">'
  232. . __('Select one…') . '</option>'
  233. . "<option value='no_such_col'>" . __('No such column') . '</option>'
  234. . $this->getHtmlForColumnsList(
  235. $db,
  236. $table,
  237. _pgettext('string types', 'String')
  238. )
  239. . '</select>'
  240. . '<span>' . __('split into ')
  241. . "</span><input id='numField' type='number' value='2'>"
  242. . '<input type="submit" class="btn btn-primary" id="splitGo" value="' . __('Go') . '"></div>'
  243. . "<div id='newCols'></div>"
  244. . '</fieldset><fieldset class="pma-fieldset tblFooters">'
  245. . '</fieldset>'
  246. . '</div>';
  247. return $html;
  248. }
  249. /**
  250. * build the html contents of various html elements in step 1.2
  251. *
  252. * @param string $db current database
  253. * @param string $table current table
  254. *
  255. * @return string[] HTML contents for step 1.2
  256. */
  257. public function getHtmlContentsFor1NFStep2($db, $table)
  258. {
  259. $step = 2;
  260. $stepTxt = __('Have a primary key');
  261. $primary = Index::getPrimary($table, $db);
  262. $hasPrimaryKey = '0';
  263. $legendText = __('Step 1.') . $step . ' ' . $stepTxt;
  264. $extra = '';
  265. if ($primary !== false) {
  266. $headText = __('Primary key already exists.');
  267. $subText = __('Taking you to next step…');
  268. $hasPrimaryKey = '1';
  269. } else {
  270. $headText = __(
  271. 'There is no primary key; please add one.<br>'
  272. . 'Hint: A primary key is a column '
  273. . '(or combination of columns) that uniquely identify all rows.'
  274. );
  275. $subText = '<a href="#" id="createPrimaryKey">'
  276. . Generator::getIcon(
  277. 'b_index_add',
  278. __(
  279. 'Add a primary key on existing column(s)'
  280. )
  281. )
  282. . '</a>';
  283. $extra = __('If it\'s not possible to make existing column combinations as primary key') . '<br>'
  284. . '<a href="#" id="addNewPrimary">'
  285. . __('+ Add a new primary key column') . '</a>';
  286. }
  287. return [
  288. 'legendText' => $legendText,
  289. 'headText' => $headText,
  290. 'subText' => $subText,
  291. 'hasPrimaryKey' => $hasPrimaryKey,
  292. 'extra' => $extra,
  293. ];
  294. }
  295. /**
  296. * build the html contents of various html elements in step 1.4
  297. *
  298. * @param string $db current database
  299. * @param string $table current table
  300. *
  301. * @return string[] HTML contents for step 1.4
  302. */
  303. public function getHtmlContentsFor1NFStep4($db, $table)
  304. {
  305. $step = 4;
  306. $stepTxt = __('Remove redundant columns');
  307. $legendText = __('Step 1.') . $step . ' ' . $stepTxt;
  308. $headText = __(
  309. 'Do you have a group of columns which on combining gives an existing'
  310. . ' column? For example, if you have first_name, last_name and'
  311. . ' full_name then combining first_name and last_name gives full_name'
  312. . ' which is redundant.'
  313. );
  314. $subText = __(
  315. 'Check the columns which are redundant and click on remove. '
  316. . "If no redundant column, click on 'No redundant column'"
  317. );
  318. $extra = $this->getHtmlForColumnsList($db, $table, 'all', 'checkbox') . '<br>'
  319. . '<input class="btn btn-secondary" type="submit" id="removeRedundant" value="'
  320. . __('Remove selected') . '">'
  321. . '<input class="btn btn-secondary" type="submit" value="' . __('No redundant column')
  322. . '" onclick="goToFinish1NF();">';
  323. return [
  324. 'legendText' => $legendText,
  325. 'headText' => $headText,
  326. 'subText' => $subText,
  327. 'extra' => $extra,
  328. ];
  329. }
  330. /**
  331. * build the html contents of various html elements in step 1.3
  332. *
  333. * @param string $db current database
  334. * @param string $table current table
  335. *
  336. * @return string[] HTML contents for step 1.3
  337. */
  338. public function getHtmlContentsFor1NFStep3($db, $table)
  339. {
  340. $step = 3;
  341. $stepTxt = __('Move repeating groups');
  342. $legendText = __('Step 1.') . $step . ' ' . $stepTxt;
  343. $headText = __(
  344. 'Do you have a group of two or more columns that are closely '
  345. . 'related and are all repeating the same attribute? For example, '
  346. . 'a table that holds data on books might have columns such as book_id, '
  347. . 'author1, author2, author3 and so on which form a '
  348. . 'repeating group. In this case a new table (book_id, author) should '
  349. . 'be created.'
  350. );
  351. $subText = __(
  352. 'Check the columns which form a repeating group. If no such group, click on \'No repeating group\''
  353. );
  354. $extra = $this->getHtmlForColumnsList($db, $table, 'all', 'checkbox') . '<br>'
  355. . '<input class="btn btn-secondary" type="submit" id="moveRepeatingGroup" value="'
  356. . __('Done') . '">'
  357. . '<input class="btn btn-secondary" type="submit" value="' . __('No repeating group')
  358. . '" onclick="goToStep4();">';
  359. $primary = Index::getPrimary($table, $db);
  360. $primarycols = $primary === false ? [] : $primary->getColumns();
  361. $pk = [];
  362. foreach ($primarycols as $col) {
  363. $pk[] = $col->getName();
  364. }
  365. return [
  366. 'legendText' => $legendText,
  367. 'headText' => $headText,
  368. 'subText' => $subText,
  369. 'extra' => $extra,
  370. 'primary_key' => json_encode($pk),
  371. ];
  372. }
  373. /**
  374. * build html contents for 2NF step 2.1
  375. *
  376. * @param string $db current database
  377. * @param string $table current table
  378. *
  379. * @return string[] HTML contents for 2NF step 2.1
  380. */
  381. public function getHtmlFor2NFstep1($db, $table)
  382. {
  383. $legendText = __('Step 2.') . '1 ' . __('Find partial dependencies');
  384. $primary = Index::getPrimary($table, $db);
  385. $primarycols = $primary === false ? [] : $primary->getColumns();
  386. $pk = [];
  387. $subText = '';
  388. $selectPkForm = '';
  389. $extra = '';
  390. foreach ($primarycols as $col) {
  391. $pk[] = $col->getName();
  392. $selectPkForm .= '<input type="checkbox" name="pd" value="'
  393. . htmlspecialchars($col->getName()) . '">'
  394. . htmlspecialchars($col->getName());
  395. }
  396. $key = implode(', ', $pk);
  397. if (count($primarycols) > 1) {
  398. $this->dbi->selectDb($db);
  399. $columns = (array) $this->dbi->getColumnNames($db, $table);
  400. if (count($pk) == count($columns)) {
  401. $headText = sprintf(
  402. __(
  403. 'No partial dependencies possible as '
  404. . 'no non-primary column exists since primary key ( %1$s ) '
  405. . 'is composed of all the columns in the table.'
  406. ),
  407. htmlspecialchars($key)
  408. ) . '<br>';
  409. $extra = '<h3>' . __('Table is already in second normal form.')
  410. . '</h3>';
  411. } else {
  412. $headText = sprintf(
  413. __(
  414. 'The primary key ( %1$s ) consists of more than one column '
  415. . 'so we need to find the partial dependencies.'
  416. ),
  417. htmlspecialchars($key)
  418. ) . '<br>' . __('Please answer the following question(s) carefully to obtain a correct normalization.')
  419. . '<br><a href="#" id="showPossiblePd">' . __(
  420. '+ Show me the possible partial dependencies based on data in the table'
  421. ) . '</a>';
  422. $subText = __(
  423. 'For each column below, '
  424. . 'please select the <b>minimal set</b> of columns among given set '
  425. . 'whose values combined together are sufficient'
  426. . ' to determine the value of the column.'
  427. );
  428. $cnt = 0;
  429. foreach ($columns as $column) {
  430. if (in_array($column, $pk)) {
  431. continue;
  432. }
  433. $cnt++;
  434. $extra .= '<b>' . sprintf(
  435. __('\'%1$s\' depends on:'),
  436. htmlspecialchars($column)
  437. ) . '</b><br>';
  438. $extra .= '<form id="pk_' . $cnt . '" data-colname="'
  439. . htmlspecialchars($column) . '" class="smallIndent">'
  440. . $selectPkForm . '</form><br><br>';
  441. }
  442. }
  443. } else {
  444. $headText = sprintf(
  445. __(
  446. 'No partial dependencies possible as the primary key ( %1$s ) has just one column.'
  447. ),
  448. htmlspecialchars($key)
  449. ) . '<br>';
  450. $extra = '<h3>' . __('Table is already in second normal form.') . '</h3>';
  451. }
  452. return [
  453. 'legendText' => $legendText,
  454. 'headText' => $headText,
  455. 'subText' => $subText,
  456. 'extra' => $extra,
  457. 'primary_key' => $key,
  458. ];
  459. }
  460. /**
  461. * build the html for showing the tables to have in order to put current table in 2NF
  462. *
  463. * @param array $partialDependencies array containing all the dependencies
  464. * @param string $table current table
  465. *
  466. * @return string HTML
  467. */
  468. public function getHtmlForNewTables2NF(array $partialDependencies, $table)
  469. {
  470. $html = '<p><b>' . sprintf(
  471. __(
  472. 'In order to put the '
  473. . 'original table \'%1$s\' into Second normal form we need '
  474. . 'to create the following tables:'
  475. ),
  476. htmlspecialchars($table)
  477. ) . '</b></p>';
  478. $tableName = $table;
  479. $i = 1;
  480. foreach ($partialDependencies as $key => $dependents) {
  481. $html .= '<p><input type="text" name="' . htmlspecialchars($key)
  482. . '" value="' . htmlspecialchars($tableName) . '">'
  483. . '( <u>' . htmlspecialchars($key) . '</u>'
  484. . (count($dependents) > 0 ? ', ' : '')
  485. . htmlspecialchars(implode(', ', $dependents)) . ' )';
  486. $i++;
  487. $tableName = 'table' . $i;
  488. }
  489. return $html;
  490. }
  491. /**
  492. * create/alter the tables needed for 2NF
  493. *
  494. * @param array $partialDependencies array containing all the partial dependencies
  495. * @param object $tablesName name of new tables
  496. * @param string $table current table
  497. * @param string $db current database
  498. *
  499. * @return array
  500. */
  501. public function createNewTablesFor2NF(array $partialDependencies, $tablesName, $table, $db)
  502. {
  503. $dropCols = false;
  504. $nonPKCols = [];
  505. $queries = [];
  506. $error = false;
  507. $headText = '<h3>' . sprintf(
  508. __('The second step of normalization is complete for table \'%1$s\'.'),
  509. htmlspecialchars($table)
  510. ) . '</h3>';
  511. if (count((array) $partialDependencies) === 1) {
  512. return [
  513. 'legendText' => __('End of step'),
  514. 'headText' => $headText,
  515. 'queryError' => $error,
  516. ];
  517. }
  518. $message = '';
  519. $this->dbi->selectDb($db);
  520. foreach ($partialDependencies as $key => $dependents) {
  521. if ($tablesName->$key != $table) {
  522. $backquotedKey = implode(', ', Util::backquote(explode(', ', $key)));
  523. $queries[] = 'CREATE TABLE ' . Util::backquote($tablesName->$key)
  524. . ' SELECT DISTINCT ' . $backquotedKey
  525. . (count($dependents) > 0 ? ', ' : '')
  526. . implode(',', Util::backquote($dependents))
  527. . ' FROM ' . Util::backquote($table) . ';';
  528. $queries[] = 'ALTER TABLE ' . Util::backquote($tablesName->$key)
  529. . ' ADD PRIMARY KEY(' . $backquotedKey . ');';
  530. $nonPKCols = array_merge($nonPKCols, $dependents);
  531. } else {
  532. $dropCols = true;
  533. }
  534. }
  535. if ($dropCols) {
  536. $query = 'ALTER TABLE ' . Util::backquote($table);
  537. foreach ($nonPKCols as $col) {
  538. $query .= ' DROP ' . Util::backquote($col) . ',';
  539. }
  540. $query = trim($query, ', ');
  541. $query .= ';';
  542. $queries[] = $query;
  543. } else {
  544. $queries[] = 'DROP TABLE ' . Util::backquote($table);
  545. }
  546. foreach ($queries as $query) {
  547. if (! $this->dbi->tryQuery($query)) {
  548. $message = Message::error(__('Error in processing!'));
  549. $message->addMessage(
  550. Message::rawError(
  551. (string) $this->dbi->getError()
  552. ),
  553. '<br><br>'
  554. );
  555. $error = true;
  556. break;
  557. }
  558. }
  559. return [
  560. 'legendText' => __('End of step'),
  561. 'headText' => $headText,
  562. 'queryError' => $error,
  563. 'extra' => $message,
  564. ];
  565. }
  566. /**
  567. * build the html for showing the new tables to have in order
  568. * to put given tables in 3NF
  569. *
  570. * @param object $dependencies containing all the dependencies
  571. * @param array $tables tables formed after 2NF and need to convert to 3NF
  572. * @param string $db current database
  573. *
  574. * @return array containing html and the list of new tables
  575. */
  576. public function getHtmlForNewTables3NF($dependencies, array $tables, $db)
  577. {
  578. $html = '';
  579. $i = 1;
  580. $newTables = [];
  581. foreach ($tables as $table => $arrDependson) {
  582. if (count(array_unique($arrDependson)) === 1) {
  583. continue;
  584. }
  585. $primary = Index::getPrimary($table, $db);
  586. $primarycols = $primary === false ? [] : $primary->getColumns();
  587. $pk = [];
  588. foreach ($primarycols as $col) {
  589. $pk[] = $col->getName();
  590. }
  591. $html .= '<p><b>' . sprintf(
  592. __(
  593. 'In order to put the '
  594. . 'original table \'%1$s\' into Third normal form we need '
  595. . 'to create the following tables:'
  596. ),
  597. htmlspecialchars($table)
  598. ) . '</b></p>';
  599. $tableName = $table;
  600. $columnList = [];
  601. foreach ($arrDependson as $key) {
  602. $dependents = $dependencies->$key;
  603. if ($key == $table) {
  604. $key = implode(', ', $pk);
  605. }
  606. $tmpTableCols = array_merge(explode(', ', $key), $dependents);
  607. sort($tmpTableCols);
  608. if (in_array($tmpTableCols, $columnList)) {
  609. continue;
  610. }
  611. $columnList[] = $tmpTableCols;
  612. $html .= '<p><input type="text" name="'
  613. . htmlspecialchars($tableName)
  614. . '" value="' . htmlspecialchars($tableName) . '">'
  615. . '( <u>' . htmlspecialchars($key) . '</u>'
  616. . (count($dependents) > 0 ? ', ' : '')
  617. . htmlspecialchars(implode(', ', $dependents)) . ' )';
  618. $newTables[$table][$tableName] = [
  619. 'pk' => $key,
  620. 'nonpk' => implode(', ', $dependents),
  621. ];
  622. $i++;
  623. $tableName = 'table' . $i;
  624. }
  625. }
  626. return [
  627. 'html' => $html,
  628. 'newTables' => $newTables,
  629. 'success' => true,
  630. ];
  631. }
  632. /**
  633. * create new tables or alter existing to get 3NF
  634. *
  635. * @param array $newTables list of new tables to be created
  636. * @param string $db current database
  637. *
  638. * @return array
  639. */
  640. public function createNewTablesFor3NF(array $newTables, $db)
  641. {
  642. $queries = [];
  643. $dropCols = false;
  644. $error = false;
  645. $headText = '<h3>' .
  646. __('The third step of normalization is complete.')
  647. . '</h3>';
  648. if (count($newTables) === 0) {
  649. return [
  650. 'legendText' => __('End of step'),
  651. 'headText' => $headText,
  652. 'queryError' => $error,
  653. ];
  654. }
  655. $message = '';
  656. $this->dbi->selectDb($db);
  657. foreach ($newTables as $originalTable => $tablesList) {
  658. foreach ($tablesList as $table => $cols) {
  659. if ($table != $originalTable) {
  660. $quotedPk = implode(
  661. ', ',
  662. Util::backquote(explode(', ', $cols['pk']))
  663. );
  664. $quotedNonpk = implode(
  665. ', ',
  666. Util::backquote(explode(', ', $cols['nonpk']))
  667. );
  668. $queries[] = 'CREATE TABLE ' . Util::backquote($table)
  669. . ' SELECT DISTINCT ' . $quotedPk
  670. . ', ' . $quotedNonpk
  671. . ' FROM ' . Util::backquote($originalTable) . ';';
  672. $queries[] = 'ALTER TABLE ' . Util::backquote($table)
  673. . ' ADD PRIMARY KEY(' . $quotedPk . ');';
  674. } else {
  675. $dropCols = $cols;
  676. }
  677. }
  678. if ($dropCols) {
  679. $columns = (array) $this->dbi->getColumnNames($db, $originalTable);
  680. $colPresent = array_merge(
  681. explode(', ', $dropCols['pk']),
  682. explode(', ', $dropCols['nonpk'])
  683. );
  684. $query = 'ALTER TABLE ' . Util::backquote($originalTable);
  685. foreach ($columns as $col) {
  686. if (in_array($col, $colPresent)) {
  687. continue;
  688. }
  689. $query .= ' DROP ' . Util::backquote($col) . ',';
  690. }
  691. $query = trim($query, ', ');
  692. $query .= ';';
  693. $queries[] = $query;
  694. } else {
  695. $queries[] = 'DROP TABLE ' . Util::backquote($originalTable);
  696. }
  697. $dropCols = false;
  698. }
  699. foreach ($queries as $query) {
  700. if (! $this->dbi->tryQuery($query)) {
  701. $message = Message::error(__('Error in processing!'));
  702. $message->addMessage(
  703. Message::rawError(
  704. (string) $this->dbi->getError()
  705. ),
  706. '<br><br>'
  707. );
  708. $error = true;
  709. break;
  710. }
  711. }
  712. return [
  713. 'legendText' => __('End of step'),
  714. 'headText' => $headText,
  715. 'queryError' => $error,
  716. 'extra' => $message,
  717. ];
  718. }
  719. /**
  720. * move the repeating group of columns to a new table
  721. *
  722. * @param string $repeatingColumns comma separated list of repeating group columns
  723. * @param string $primaryColumns comma separated list of column in primary key
  724. * of $table
  725. * @param string $newTable name of the new table to be created
  726. * @param string $newColumn name of the new column in the new table
  727. * @param string $table current table
  728. * @param string $db current database
  729. *
  730. * @return array
  731. */
  732. public function moveRepeatingGroup(
  733. $repeatingColumns,
  734. $primaryColumns,
  735. $newTable,
  736. $newColumn,
  737. $table,
  738. $db
  739. ) {
  740. $repeatingColumnsArr = (array) Util::backquote(
  741. explode(', ', $repeatingColumns)
  742. );
  743. $primaryColumns = implode(
  744. ',',
  745. Util::backquote(explode(',', $primaryColumns))
  746. );
  747. $query1 = 'CREATE TABLE ' . Util::backquote($newTable);
  748. $query2 = 'ALTER TABLE ' . Util::backquote($table);
  749. $message = Message::success(
  750. sprintf(
  751. __('Selected repeating group has been moved to the table \'%s\''),
  752. htmlspecialchars($table)
  753. )
  754. );
  755. $first = true;
  756. $error = false;
  757. foreach ($repeatingColumnsArr as $repeatingColumn) {
  758. if (! $first) {
  759. $query1 .= ' UNION ';
  760. }
  761. $first = false;
  762. $query1 .= ' SELECT ' . $primaryColumns . ',' . $repeatingColumn
  763. . ' as ' . Util::backquote($newColumn)
  764. . ' FROM ' . Util::backquote($table);
  765. $query2 .= ' DROP ' . $repeatingColumn . ',';
  766. }
  767. $query2 = trim($query2, ',');
  768. $queries = [
  769. $query1,
  770. $query2,
  771. ];
  772. $this->dbi->selectDb($db);
  773. foreach ($queries as $query) {
  774. if (! $this->dbi->tryQuery($query)) {
  775. $message = Message::error(__('Error in processing!'));
  776. $message->addMessage(
  777. Message::rawError(
  778. (string) $this->dbi->getError()
  779. ),
  780. '<br><br>'
  781. );
  782. $error = true;
  783. break;
  784. }
  785. }
  786. return [
  787. 'queryError' => $error,
  788. 'message' => $message,
  789. ];
  790. }
  791. /**
  792. * build html for 3NF step 1 to find the transitive dependencies
  793. *
  794. * @param string $db current database
  795. * @param array $tables tables formed after 2NF and need to process for 3NF
  796. *
  797. * @return string[]
  798. */
  799. public function getHtmlFor3NFstep1($db, array $tables)
  800. {
  801. $legendText = __('Step 3.') . '1 ' . __('Find transitive dependencies');
  802. $extra = '';
  803. $headText = __('Please answer the following question(s) carefully to obtain a correct normalization.');
  804. $subText = __(
  805. 'For each column below, '
  806. . 'please select the <b>minimal set</b> of columns among given set '
  807. . 'whose values combined together are sufficient'
  808. . ' to determine the value of the column.<br>'
  809. . 'Note: A column may have no transitive dependency, '
  810. . 'in that case you don\'t have to select any.'
  811. );
  812. $cnt = 0;
  813. foreach ($tables as $table) {
  814. $primary = Index::getPrimary($table, $db);
  815. $primarycols = $primary === false ? [] : $primary->getColumns();
  816. $selectTdForm = '';
  817. $pk = [];
  818. foreach ($primarycols as $col) {
  819. $pk[] = $col->getName();
  820. }
  821. $this->dbi->selectDb($db);
  822. $columns = (array) $this->dbi->getColumnNames($db, $table);
  823. if (count($columns) - count($pk) <= 1) {
  824. continue;
  825. }
  826. foreach ($columns as $column) {
  827. if (in_array($column, $pk)) {
  828. continue;
  829. }
  830. $selectTdForm .= '<input type="checkbox" name="pd" value="'
  831. . htmlspecialchars($column) . '">'
  832. . '<span>' . htmlspecialchars($column) . '</span>';
  833. }
  834. foreach ($columns as $column) {
  835. if (in_array($column, $pk)) {
  836. continue;
  837. }
  838. $cnt++;
  839. $extra .= '<b>' . sprintf(
  840. __('\'%1$s\' depends on:'),
  841. htmlspecialchars($column)
  842. )
  843. . '</b><br>';
  844. $extra .= '<form id="td_' . $cnt . '" data-colname="'
  845. . htmlspecialchars($column) . '" data-tablename="'
  846. . htmlspecialchars($table) . '" class="smallIndent">'
  847. . $selectTdForm
  848. . '</form><br><br>';
  849. }
  850. }
  851. if ($extra == '') {
  852. $headText = __(
  853. 'No Transitive dependencies possible as the table doesn\'t have any non primary key columns'
  854. );
  855. $subText = '';
  856. $extra = '<h3>' . __('Table is already in Third normal form!') . '</h3>';
  857. }
  858. return [
  859. 'legendText' => $legendText,
  860. 'headText' => $headText,
  861. 'subText' => $subText,
  862. 'extra' => $extra,
  863. ];
  864. }
  865. /**
  866. * get html for options to normalize table
  867. *
  868. * @return string HTML
  869. */
  870. public function getHtmlForNormalizeTable()
  871. {
  872. $htmlOutput = '<form method="post" action="' . Url::getFromRoute('/normalization')
  873. . '" name="normalize" '
  874. . 'id="normalizeTable" '
  875. . '>'
  876. . Url::getHiddenInputs($GLOBALS['db'], $GLOBALS['table'])
  877. . '<input type="hidden" name="step1" value="1">';
  878. $htmlOutput .= '<fieldset class="pma-fieldset">';
  879. $htmlOutput .= '<legend>'
  880. . __('Improve table structure (Normalization):') . '</legend>';
  881. $htmlOutput .= '<h3>' . __('Select up to what step you want to normalize')
  882. . '</h3>';
  883. $htmlOutput .= '<div><input type="radio" name="normalizeTo" id="normalizeToRadio1" value="1nf" checked>';
  884. $htmlOutput .= ' <label for="normalizeToRadio1">';
  885. $htmlOutput .= __('First step of normalization (1NF)');
  886. $htmlOutput .= '</label></div>';
  887. $htmlOutput .= '<div><input type="radio" name="normalizeTo" id="normalizeToRadio2" value="2nf">';
  888. $htmlOutput .= ' <label for="normalizeToRadio2">';
  889. $htmlOutput .= __('Second step of normalization (1NF+2NF)');
  890. $htmlOutput .= '</label></div>';
  891. $htmlOutput .= '<div><input type="radio" name="normalizeTo" id="normalizeToRadio3" value="3nf">';
  892. $htmlOutput .= ' <label for="normalizeToRadio3">';
  893. $htmlOutput .= __('Third step of normalization (1NF+2NF+3NF)');
  894. $htmlOutput .= '</label></div>';
  895. $htmlOutput .= '</fieldset><fieldset class="pma-fieldset tblFooters">'
  896. . "<span class='float-start'>" . __(
  897. 'Hint: Please follow the procedure carefully in order to obtain correct normalization'
  898. ) . '</span>'
  899. . '<input class="btn btn-primary" type="submit" name="submit_normalize" value="' . __('Go') . '">'
  900. . '</fieldset>'
  901. . '</form>'
  902. . '</div>';
  903. return $htmlOutput;
  904. }
  905. /**
  906. * find all the possible partial dependencies based on data in the table.
  907. *
  908. * @param string $table current table
  909. * @param string $db current database
  910. *
  911. * @return string HTML containing the list of all the possible partial dependencies
  912. */
  913. public function findPartialDependencies($table, $db)
  914. {
  915. $dependencyList = [];
  916. $this->dbi->selectDb($db);
  917. $columns = (array) $this->dbi->getColumnNames($db, $table);
  918. $columns = (array) Util::backquote($columns);
  919. $totalRowsRes = $this->dbi->fetchResult(
  920. 'SELECT COUNT(*) FROM (SELECT * FROM '
  921. . Util::backquote($table) . ' LIMIT 500) as dt;'
  922. );
  923. $totalRows = $totalRowsRes[0];
  924. $primary = Index::getPrimary($table, $db);
  925. $primarycols = $primary === false ? [] : $primary->getColumns();
  926. $pk = [];
  927. foreach ($primarycols as $col) {
  928. $pk[] = Util::backquote($col->getName());
  929. }
  930. $partialKeys = $this->getAllCombinationPartialKeys($pk);
  931. $distinctValCount = $this->findDistinctValuesCount(
  932. array_unique(
  933. array_merge($columns, $partialKeys)
  934. ),
  935. $table
  936. );
  937. foreach ($columns as $column) {
  938. if (in_array($column, $pk)) {
  939. continue;
  940. }
  941. foreach ($partialKeys as $partialKey) {
  942. if (
  943. ! $partialKey
  944. || ! $this->checkPartialDependency(
  945. $partialKey,
  946. $column,
  947. $table,
  948. $distinctValCount[$partialKey],
  949. $distinctValCount[$column],
  950. $totalRows
  951. )
  952. ) {
  953. continue;
  954. }
  955. $dependencyList[$partialKey][] = $column;
  956. }
  957. }
  958. $html = __('This list is based on a subset of the table\'s data and is not necessarily accurate. ')
  959. . '<div class="dependencies_box">';
  960. foreach ($dependencyList as $dependon => $colList) {
  961. $html .= '<span class="d-block">'
  962. . '<input type="button" class="btn btn-secondary pickPd" value="' . __('Pick') . '">'
  963. . '<span class="determinants">'
  964. . htmlspecialchars(str_replace('`', '', (string) $dependon)) . '</span> -> '
  965. . '<span class="dependents">'
  966. . htmlspecialchars(str_replace('`', '', implode(', ', $colList)))
  967. . '</span>'
  968. . '</span>';
  969. }
  970. if (empty($dependencyList)) {
  971. $html .= '<p class="d-block desc">'
  972. . __('No partial dependencies found!') . '</p>';
  973. }
  974. $html .= '</div>';
  975. return $html;
  976. }
  977. /**
  978. * check whether a particular column is dependent on given subset of primary key
  979. *
  980. * @param string $partialKey the partial key, subset of primary key,
  981. * each column in key supposed to be backquoted
  982. * @param string $column backquoted column on whose dependency being checked
  983. * @param string $table current table
  984. * @param int $pkCnt distinct value count for given partial key
  985. * @param int $colCnt distinct value count for given column
  986. * @param int $totalRows total distinct rows count of the table
  987. */
  988. private function checkPartialDependency(
  989. $partialKey,
  990. $column,
  991. $table,
  992. $pkCnt,
  993. $colCnt,
  994. $totalRows
  995. ): bool {
  996. $query = 'SELECT '
  997. . 'COUNT(DISTINCT ' . $partialKey . ',' . $column . ') as pkColCnt '
  998. . 'FROM (SELECT * FROM ' . Util::backquote($table)
  999. . ' LIMIT 500) as dt;';
  1000. $res = $this->dbi->fetchResult($query, null, null);
  1001. $pkColCnt = $res[0];
  1002. if ($pkCnt && $pkCnt == $colCnt && $colCnt == $pkColCnt) {
  1003. return true;
  1004. }
  1005. return $totalRows && $totalRows == $pkCnt;
  1006. }
  1007. /**
  1008. * function to get distinct values count of all the column in the array $columns
  1009. *
  1010. * @param array $columns array of backquoted columns whose distinct values
  1011. * need to be counted.
  1012. * @param string $table table to which these columns belong
  1013. *
  1014. * @return array associative array containing the count
  1015. */
  1016. private function findDistinctValuesCount(array $columns, $table)
  1017. {
  1018. $result = [];
  1019. $query = 'SELECT ';
  1020. foreach ($columns as $column) {
  1021. if (! $column) {
  1022. continue;
  1023. }
  1024. //each column is already backquoted
  1025. $query .= 'COUNT(DISTINCT ' . $column . ') as \''
  1026. . $column . '_cnt\', ';
  1027. }
  1028. $query = trim($query, ', ');
  1029. $query .= ' FROM (SELECT * FROM ' . Util::backquote($table)
  1030. . ' LIMIT 500) as dt;';
  1031. $res = $this->dbi->fetchResult($query, null, null);
  1032. foreach ($columns as $column) {
  1033. if (! $column) {
  1034. continue;
  1035. }
  1036. $result[$column] = $res[0][$column . '_cnt'] ?? null;
  1037. }
  1038. return $result;
  1039. }
  1040. /**
  1041. * find all the possible partial keys
  1042. *
  1043. * @param array $primaryKey array containing all the column present in primary key
  1044. *
  1045. * @return array containing all the possible partial keys(subset of primary key)
  1046. */
  1047. private function getAllCombinationPartialKeys(array $primaryKey)
  1048. {
  1049. $results = [''];
  1050. foreach ($primaryKey as $element) {
  1051. foreach ($results as $combination) {
  1052. $results[] = trim($element . ',' . $combination, ',');
  1053. }
  1054. }
  1055. array_pop($results); //remove key which consist of all primary key columns
  1056. return $results;
  1057. }
  1058. }