Normalization.php 41 KB

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