Normalization.php 39 KB

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