Normalization.php 40 KB

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