StructureController.php 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Controllers\Table;
  4. use PhpMyAdmin\Charsets;
  5. use PhpMyAdmin\CheckUserPrivileges;
  6. use PhpMyAdmin\Config\PageSettings;
  7. use PhpMyAdmin\Controllers\SqlController;
  8. use PhpMyAdmin\Core;
  9. use PhpMyAdmin\CreateAddField;
  10. use PhpMyAdmin\Database\CentralColumns;
  11. use PhpMyAdmin\DatabaseInterface;
  12. use PhpMyAdmin\DbTableExists;
  13. use PhpMyAdmin\Engines\Innodb;
  14. use PhpMyAdmin\Html\Generator;
  15. use PhpMyAdmin\Index;
  16. use PhpMyAdmin\Message;
  17. use PhpMyAdmin\Operations;
  18. use PhpMyAdmin\ParseAnalyze;
  19. use PhpMyAdmin\Partition;
  20. use PhpMyAdmin\Query\Utilities;
  21. use PhpMyAdmin\Relation;
  22. use PhpMyAdmin\RelationCleanup;
  23. use PhpMyAdmin\Response;
  24. use PhpMyAdmin\Sql;
  25. use PhpMyAdmin\SqlParser\Context;
  26. use PhpMyAdmin\SqlParser\Parser;
  27. use PhpMyAdmin\SqlParser\Statements\CreateStatement;
  28. use PhpMyAdmin\StorageEngine;
  29. use PhpMyAdmin\Table;
  30. use PhpMyAdmin\Table\ColumnsDefinition;
  31. use PhpMyAdmin\TablePartitionDefinition;
  32. use PhpMyAdmin\Template;
  33. use PhpMyAdmin\Tracker;
  34. use PhpMyAdmin\Transformations;
  35. use PhpMyAdmin\Url;
  36. use PhpMyAdmin\Util;
  37. use stdClass;
  38. use function array_keys;
  39. use function array_splice;
  40. use function count;
  41. use function implode;
  42. use function in_array;
  43. use function is_array;
  44. use function is_string;
  45. use function mb_strpos;
  46. use function mb_strtoupper;
  47. use function sprintf;
  48. use function str_replace;
  49. use function strlen;
  50. use function strpos;
  51. use function strrpos;
  52. use function substr;
  53. use function trim;
  54. /**
  55. * Displays table structure infos like columns, indexes, size, rows
  56. * and allows manipulation of indexes and columns.
  57. */
  58. class StructureController extends AbstractController
  59. {
  60. /** @var Table The table object */
  61. protected $tableObj;
  62. /** @var CreateAddField */
  63. private $createAddField;
  64. /** @var Relation */
  65. private $relation;
  66. /** @var Transformations */
  67. private $transformations;
  68. /** @var RelationCleanup */
  69. private $relationCleanup;
  70. /** @var DatabaseInterface */
  71. private $dbi;
  72. /**
  73. * @param Response $response
  74. * @param string $db Database name
  75. * @param string $table Table name
  76. * @param DatabaseInterface $dbi
  77. */
  78. public function __construct(
  79. $response,
  80. Template $template,
  81. $db,
  82. $table,
  83. Relation $relation,
  84. Transformations $transformations,
  85. CreateAddField $createAddField,
  86. RelationCleanup $relationCleanup,
  87. $dbi
  88. ) {
  89. parent::__construct($response, $template, $db, $table);
  90. $this->createAddField = $createAddField;
  91. $this->relation = $relation;
  92. $this->transformations = $transformations;
  93. $this->relationCleanup = $relationCleanup;
  94. $this->dbi = $dbi;
  95. $this->tableObj = $this->dbi->getTable($this->db, $this->table);
  96. }
  97. public function index(): void
  98. {
  99. global $reread_info, $showtable, $db, $table, $cfg, $err_url;
  100. global $tbl_is_view, $tbl_storage_engine, $tbl_collation, $table_info_num_rows;
  101. $this->dbi->selectDb($this->db);
  102. $reread_info = $this->tableObj->getStatusInfo(null, true);
  103. $showtable = $this->tableObj->getStatusInfo(
  104. null,
  105. (isset($reread_info) && $reread_info)
  106. );
  107. if ($this->tableObj->isView()) {
  108. $tbl_is_view = true;
  109. $tbl_storage_engine = __('View');
  110. } else {
  111. $tbl_is_view = false;
  112. $tbl_storage_engine = $this->tableObj->getStorageEngine();
  113. }
  114. $tbl_collation = $this->tableObj->getCollation();
  115. $table_info_num_rows = $this->tableObj->getNumRows();
  116. $pageSettings = new PageSettings('TableStructure');
  117. $this->response->addHTML($pageSettings->getErrorHTML());
  118. $this->response->addHTML($pageSettings->getHTML());
  119. $checkUserPrivileges = new CheckUserPrivileges($this->dbi);
  120. $checkUserPrivileges->getPrivileges();
  121. $this->addScriptFiles(['table/structure.js', 'indexes.js']);
  122. $cfgRelation = $this->relation->getRelationsParam();
  123. Util::checkParameters(['db', 'table']);
  124. $isSystemSchema = Utilities::isSystemSchema($db);
  125. $url_params = ['db' => $db, 'table' => $table];
  126. $err_url = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
  127. $err_url .= Url::getCommon($url_params, '&');
  128. DbTableExists::check();
  129. $primary = Index::getPrimary($this->table, $this->db);
  130. $columns_with_index = $this->dbi
  131. ->getTable($this->db, $this->table)
  132. ->getColumnsWithIndex(
  133. Index::UNIQUE | Index::INDEX | Index::SPATIAL
  134. | Index::FULLTEXT
  135. );
  136. $columns_with_unique_index = $this->dbi
  137. ->getTable($this->db, $this->table)
  138. ->getColumnsWithIndex(Index::UNIQUE);
  139. $fields = (array) $this->dbi->getColumns(
  140. $this->db,
  141. $this->table,
  142. null,
  143. true
  144. );
  145. $this->response->addHTML($this->displayStructure(
  146. $cfgRelation,
  147. $columns_with_unique_index,
  148. $primary,
  149. $fields,
  150. $columns_with_index,
  151. $isSystemSchema
  152. ));
  153. }
  154. public function save(): void
  155. {
  156. $regenerate = $this->updateColumns();
  157. if (! $regenerate) {
  158. // continue to show the table's structure
  159. unset($_POST['selected']);
  160. }
  161. $this->index();
  162. }
  163. public function addKey(): void
  164. {
  165. global $containerBuilder, $reload;
  166. /** @var SqlController $controller */
  167. $controller = $containerBuilder->get(SqlController::class);
  168. $controller->index();
  169. $reload = true;
  170. $this->index();
  171. }
  172. public function browse(): void
  173. {
  174. global $PMA_Theme;
  175. if (empty($_POST['selected_fld'])) {
  176. $this->response->setRequestStatus(false);
  177. $this->response->addJSON('message', __('No column selected.'));
  178. return;
  179. }
  180. $this->displayTableBrowseForSelectedColumns(
  181. $GLOBALS['goto'],
  182. $PMA_Theme->getImgPath()
  183. );
  184. }
  185. public function change(): void
  186. {
  187. if (isset($_GET['change_column'])) {
  188. $this->displayHtmlForColumnChange(null);
  189. return;
  190. }
  191. $selected = $_POST['selected_fld'] ?? [];
  192. if (empty($selected)) {
  193. $this->response->setRequestStatus(false);
  194. $this->response->addJSON('message', __('No column selected.'));
  195. return;
  196. }
  197. $this->displayHtmlForColumnChange($selected);
  198. }
  199. public function addToCentralColumns(): void
  200. {
  201. global $sql_query, $message;
  202. $selected = $_POST['selected_fld'] ?? [];
  203. if (empty($selected)) {
  204. $this->response->setRequestStatus(false);
  205. $this->response->addJSON('message', __('No column selected.'));
  206. return;
  207. }
  208. $centralColumns = new CentralColumns($this->dbi);
  209. $centralColsError = $centralColumns->syncUniqueColumns(
  210. $selected,
  211. false
  212. );
  213. if ($centralColsError instanceof Message) {
  214. $message = $centralColsError;
  215. }
  216. if (empty($message)) {
  217. $message = Message::success();
  218. }
  219. $this->index();
  220. }
  221. public function removeFromCentralColumns(): void
  222. {
  223. global $sql_query, $db, $message;
  224. $selected = $_POST['selected_fld'] ?? [];
  225. if (empty($selected)) {
  226. $this->response->setRequestStatus(false);
  227. $this->response->addJSON('message', __('No column selected.'));
  228. return;
  229. }
  230. $centralColumns = new CentralColumns($this->dbi);
  231. $centralColsError = $centralColumns->deleteColumnsFromList(
  232. $db,
  233. $selected,
  234. false
  235. );
  236. if ($centralColsError instanceof Message) {
  237. $message = $centralColsError;
  238. }
  239. if (empty($message)) {
  240. $message = Message::success();
  241. }
  242. $this->index();
  243. }
  244. public function fulltext(): void
  245. {
  246. global $sql_query, $db, $table, $message;
  247. $selected = $_POST['selected_fld'] ?? [];
  248. if (empty($selected)) {
  249. $this->response->setRequestStatus(false);
  250. $this->response->addJSON('message', __('No column selected.'));
  251. return;
  252. }
  253. $i = 1;
  254. $selectedCount = count($selected);
  255. $sql_query = 'ALTER TABLE ' . Util::backquote($table) . ' ADD FULLTEXT(';
  256. foreach ($selected as $field) {
  257. $sql_query .= Util::backquote($field);
  258. $sql_query .= $i++ === $selectedCount ? ');' : ', ';
  259. }
  260. $this->dbi->selectDb($db);
  261. $result = $this->dbi->tryQuery($sql_query);
  262. if (! $result) {
  263. $message = Message::error((string) $this->dbi->getError());
  264. }
  265. if (empty($message)) {
  266. $message = Message::success();
  267. }
  268. $this->index();
  269. }
  270. public function spatial(): void
  271. {
  272. global $sql_query, $db, $table, $message;
  273. $selected = $_POST['selected_fld'] ?? [];
  274. if (empty($selected)) {
  275. $this->response->setRequestStatus(false);
  276. $this->response->addJSON('message', __('No column selected.'));
  277. return;
  278. }
  279. $i = 1;
  280. $selectedCount = count($selected);
  281. $sql_query = 'ALTER TABLE ' . Util::backquote($table) . ' ADD SPATIAL(';
  282. foreach ($selected as $field) {
  283. $sql_query .= Util::backquote($field);
  284. $sql_query .= $i++ === $selectedCount ? ');' : ', ';
  285. }
  286. $this->dbi->selectDb($db);
  287. $result = $this->dbi->tryQuery($sql_query);
  288. if (! $result) {
  289. $message = Message::error((string) $this->dbi->getError());
  290. }
  291. if (empty($message)) {
  292. $message = Message::success();
  293. }
  294. $this->index();
  295. }
  296. public function unique(): void
  297. {
  298. global $sql_query, $db, $table, $message;
  299. $selected = $_POST['selected_fld'] ?? [];
  300. if (empty($selected)) {
  301. $this->response->setRequestStatus(false);
  302. $this->response->addJSON('message', __('No column selected.'));
  303. return;
  304. }
  305. $i = 1;
  306. $selectedCount = count($selected);
  307. $sql_query = 'ALTER TABLE ' . Util::backquote($table) . ' ADD UNIQUE(';
  308. foreach ($selected as $field) {
  309. $sql_query .= Util::backquote($field);
  310. $sql_query .= $i++ === $selectedCount ? ');' : ', ';
  311. }
  312. $this->dbi->selectDb($db);
  313. $result = $this->dbi->tryQuery($sql_query);
  314. if (! $result) {
  315. $message = Message::error((string) $this->dbi->getError());
  316. }
  317. if (empty($message)) {
  318. $message = Message::success();
  319. }
  320. $this->index();
  321. }
  322. public function addIndex(): void
  323. {
  324. global $sql_query, $db, $table, $message;
  325. $selected = $_POST['selected_fld'] ?? [];
  326. if (empty($selected)) {
  327. $this->response->setRequestStatus(false);
  328. $this->response->addJSON('message', __('No column selected.'));
  329. return;
  330. }
  331. $i = 1;
  332. $selectedCount = count($selected);
  333. $sql_query = 'ALTER TABLE ' . Util::backquote($table) . ' ADD INDEX(';
  334. foreach ($selected as $field) {
  335. $sql_query .= Util::backquote($field);
  336. $sql_query .= $i++ === $selectedCount ? ');' : ', ';
  337. }
  338. $this->dbi->selectDb($db);
  339. $result = $this->dbi->tryQuery($sql_query);
  340. if (! $result) {
  341. $message = Message::error((string) $this->dbi->getError());
  342. }
  343. if (empty($message)) {
  344. $message = Message::success();
  345. }
  346. $this->index();
  347. }
  348. public function primary(): void
  349. {
  350. global $db, $table, $message, $sql_query, $url_params, $err_url, $cfg;
  351. $selected = $_POST['selected'] ?? [];
  352. $selected_fld = $_POST['selected_fld'] ?? [];
  353. if (empty($selected) && empty($selected_fld)) {
  354. $this->response->setRequestStatus(false);
  355. $this->response->addJSON('message', __('No column selected.'));
  356. return;
  357. }
  358. $primary = $this->getKeyForTablePrimary();
  359. if (empty($primary) && ! empty($selected_fld)) {
  360. // no primary key, so we can safely create new
  361. $mult_btn = __('Yes');
  362. $selected = $selected_fld;
  363. }
  364. $mult_btn = $_POST['mult_btn'] ?? $mult_btn ?? '';
  365. if (! empty($selected_fld) && ! empty($primary)) {
  366. Util::checkParameters(['db', 'table']);
  367. $url_params = ['db' => $db, 'table' => $table];
  368. $err_url = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
  369. $err_url .= Url::getCommon($url_params, '&');
  370. DbTableExists::check();
  371. $this->render('table/structure/primary', [
  372. 'db' => $db,
  373. 'table' => $table,
  374. 'selected' => $selected_fld,
  375. ]);
  376. return;
  377. }
  378. if ($mult_btn === __('Yes')) {
  379. $sql_query = 'ALTER TABLE ' . Util::backquote($table);
  380. if (! empty($primary)) {
  381. $sql_query .= ' DROP PRIMARY KEY,';
  382. }
  383. $sql_query .= ' ADD PRIMARY KEY(';
  384. $i = 1;
  385. $selectedCount = count($selected);
  386. foreach ($selected as $field) {
  387. $sql_query .= Util::backquote($field);
  388. $sql_query .= $i++ === $selectedCount ? ');' : ', ';
  389. }
  390. $this->dbi->selectDb($db);
  391. $result = $this->dbi->tryQuery($sql_query);
  392. if (! $result) {
  393. $message = Message::error((string) $this->dbi->getError());
  394. }
  395. }
  396. if (empty($message)) {
  397. $message = Message::success();
  398. }
  399. $this->index();
  400. }
  401. public function drop(): void
  402. {
  403. global $db, $table, $message, $sql_query;
  404. $selected = $_POST['selected'] ?? [];
  405. if (empty($selected)) {
  406. $this->response->setRequestStatus(false);
  407. $this->response->addJSON('message', __('No column selected.'));
  408. return;
  409. }
  410. $sql_query = '';
  411. if (($_POST['mult_btn'] ?? '') === __('Yes')) {
  412. $i = 1;
  413. $selectedCount = count($selected);
  414. $sql_query = 'ALTER TABLE ' . Util::backquote($table);
  415. foreach ($selected as $field) {
  416. $this->relationCleanup->column($db, $table, $field);
  417. $sql_query .= ' DROP ' . Util::backquote($field);
  418. $sql_query .= $i++ === $selectedCount ? ';' : ',';
  419. }
  420. $this->dbi->selectDb($db);
  421. $result = $this->dbi->tryQuery($sql_query);
  422. if (! $result) {
  423. $message = Message::error((string) $this->dbi->getError());
  424. }
  425. }
  426. if (empty($message)) {
  427. $message = Message::success();
  428. }
  429. $this->index();
  430. }
  431. public function dropConfirm(): void
  432. {
  433. global $db, $table, $url_params, $err_url, $cfg;
  434. $selected = $_POST['selected_fld'] ?? null;
  435. if (empty($selected)) {
  436. $this->response->setRequestStatus(false);
  437. $this->response->addJSON('message', __('No column selected.'));
  438. return;
  439. }
  440. Util::checkParameters(['db', 'table']);
  441. $url_params = ['db' => $db, 'table' => $table];
  442. $err_url = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
  443. $err_url .= Url::getCommon($url_params, '&');
  444. DbTableExists::check();
  445. $this->render('table/structure/drop_confirm', [
  446. 'db' => $db,
  447. 'table' => $table,
  448. 'fields' => $selected,
  449. ]);
  450. }
  451. /**
  452. * Moves columns in the table's structure based on $_REQUEST
  453. */
  454. public function moveColumns(): void
  455. {
  456. if (! isset($_POST['move_columns'])
  457. || ! is_array($_POST['move_columns'])
  458. || ! $this->response->isAjax()
  459. ) {
  460. return;
  461. }
  462. $this->dbi->selectDb($this->db);
  463. /**
  464. * load the definitions for all columns
  465. */
  466. $columns = $this->dbi->getColumnsFull($this->db, $this->table);
  467. $column_names = array_keys($columns);
  468. $changes = [];
  469. // @see https://mariadb.com/kb/en/library/changes-improvements-in-mariadb-102/#information-schema
  470. $usesLiteralNull = $this->dbi->isMariaDB() && $this->dbi->getVersion() >= 100200;
  471. $defaultNullValue = $usesLiteralNull ? 'NULL' : null;
  472. // move columns from first to last
  473. for ($i = 0, $l = count($_POST['move_columns']); $i < $l; $i++) {
  474. $column = $_POST['move_columns'][$i];
  475. // is this column already correctly placed?
  476. if ($column_names[$i] == $column) {
  477. continue;
  478. }
  479. // it is not, let's move it to index $i
  480. $data = $columns[$column];
  481. $extracted_columnspec = Util::extractColumnSpec($data['Type']);
  482. if (isset($data['Extra'])
  483. && $data['Extra'] === 'on update CURRENT_TIMESTAMP'
  484. ) {
  485. $extracted_columnspec['attribute'] = $data['Extra'];
  486. unset($data['Extra']);
  487. }
  488. $timeType = $data['Type'] === 'timestamp' || $data['Type'] === 'datetime';
  489. $timeDefault = $data['Default'] === 'CURRENT_TIMESTAMP' || $data['Default'] === 'current_timestamp()';
  490. $current_timestamp = $timeType && $timeDefault;
  491. // @see https://mariadb.com/kb/en/library/information-schema-columns-table/#examples
  492. if ($data['Null'] === 'YES' && in_array($data['Default'], [$defaultNullValue, null])) {
  493. $default_type = 'NULL';
  494. } elseif ($current_timestamp) {
  495. $default_type = 'CURRENT_TIMESTAMP';
  496. } elseif ($data['Default'] === null) {
  497. $default_type = 'NONE';
  498. } else {
  499. $default_type = 'USER_DEFINED';
  500. }
  501. $virtual = [
  502. 'VIRTUAL',
  503. 'PERSISTENT',
  504. 'VIRTUAL GENERATED',
  505. 'STORED GENERATED',
  506. ];
  507. $data['Virtuality'] = '';
  508. $data['Expression'] = '';
  509. if (isset($data['Extra']) && in_array($data['Extra'], $virtual)) {
  510. $data['Virtuality'] = str_replace(' GENERATED', '', $data['Extra']);
  511. $expressions = $this->tableObj->getColumnGenerationExpression($column);
  512. $data['Expression'] = is_array($expressions) ? $expressions[$column] : null;
  513. }
  514. $changes[] = 'CHANGE ' . Table::generateAlter(
  515. $column,
  516. $column,
  517. mb_strtoupper($extracted_columnspec['type']),
  518. $extracted_columnspec['spec_in_brackets'],
  519. $extracted_columnspec['attribute'],
  520. $data['Collation'] ?? '',
  521. $data['Null'] === 'YES' ? 'YES' : 'NO',
  522. $default_type,
  523. $current_timestamp ? '' : $data['Default'],
  524. isset($data['Extra']) && $data['Extra'] !== '' ? $data['Extra']
  525. : false,
  526. isset($data['COLUMN_COMMENT']) && $data['COLUMN_COMMENT'] !== ''
  527. ? $data['COLUMN_COMMENT'] : false,
  528. $data['Virtuality'],
  529. $data['Expression'],
  530. $i === 0 ? '-first' : $column_names[$i - 1]
  531. );
  532. // update current column_names array, first delete old position
  533. for ($j = 0, $ll = count($column_names); $j < $ll; $j++) {
  534. if ($column_names[$j] != $column) {
  535. continue;
  536. }
  537. unset($column_names[$j]);
  538. }
  539. // insert moved column
  540. array_splice($column_names, $i, 0, $column);
  541. }
  542. if (empty($changes) && ! isset($_REQUEST['preview_sql'])) { // should never happen
  543. $this->response->setRequestStatus(false);
  544. return;
  545. }
  546. // query for moving the columns
  547. $sql_query = sprintf(
  548. 'ALTER TABLE %s %s',
  549. Util::backquote($this->table),
  550. implode(', ', $changes)
  551. );
  552. if (isset($_REQUEST['preview_sql'])) { // preview sql
  553. $this->response->addJSON(
  554. 'sql_data',
  555. $this->template->render('preview_sql', ['query_data' => $sql_query])
  556. );
  557. } else { // move column
  558. $this->dbi->tryQuery($sql_query);
  559. $tmp_error = $this->dbi->getError();
  560. if (is_string($tmp_error)) {
  561. $this->response->setRequestStatus(false);
  562. $this->response->addJSON('message', Message::error($tmp_error));
  563. } else {
  564. $message = Message::success(
  565. __('The columns have been moved successfully.')
  566. );
  567. $this->response->addJSON('message', $message);
  568. $this->response->addJSON('columns', $column_names);
  569. }
  570. }
  571. }
  572. /**
  573. * Displays HTML for changing one or more columns
  574. *
  575. * @param array|null $selected the selected columns
  576. */
  577. protected function displayHtmlForColumnChange($selected): void
  578. {
  579. global $action, $num_fields;
  580. if (empty($selected)) {
  581. $selected[] = $_REQUEST['field'];
  582. $selected_cnt = 1;
  583. } else { // from a multiple submit
  584. $selected_cnt = count($selected);
  585. }
  586. /**
  587. * @todo optimize in case of multiple fields to modify
  588. */
  589. $fields_meta = [];
  590. for ($i = 0; $i < $selected_cnt; $i++) {
  591. $value = $this->dbi->getColumns(
  592. $this->db,
  593. $this->table,
  594. $selected[$i],
  595. true
  596. );
  597. if (count($value) === 0) {
  598. $message = Message::error(
  599. __('Failed to get description of column %s!')
  600. );
  601. $message->addParam($selected[$i]);
  602. $this->response->addHTML($message);
  603. } else {
  604. $fields_meta[] = $value;
  605. }
  606. }
  607. $num_fields = count($fields_meta);
  608. $action = Url::getFromRoute('/table/structure/save');
  609. /**
  610. * Form for changing properties.
  611. */
  612. $checkUserPrivileges = new CheckUserPrivileges($this->dbi);
  613. $checkUserPrivileges->getPrivileges();
  614. $this->addScriptFiles(['vendor/jquery/jquery.uitablefilter.js', 'indexes.js']);
  615. $templateData = ColumnsDefinition::displayForm(
  616. $this->transformations,
  617. $this->relation,
  618. $this->dbi,
  619. $action,
  620. $num_fields,
  621. null,
  622. $selected,
  623. $fields_meta
  624. );
  625. $this->render('columns_definitions/column_definitions_form', $templateData);
  626. }
  627. public function partitioning(): void
  628. {
  629. if (isset($_POST['save_partitioning'])) {
  630. $this->dbi->selectDb($this->db);
  631. $this->updatePartitioning();
  632. $this->index();
  633. return;
  634. }
  635. $pageSettings = new PageSettings('TableStructure');
  636. $this->response->addHTML($pageSettings->getErrorHTML());
  637. $this->response->addHTML($pageSettings->getHTML());
  638. $this->addScriptFiles(['table/structure.js', 'indexes.js']);
  639. $partitionDetails = null;
  640. if (! isset($_POST['partition_by'])) {
  641. $partitionDetails = $this->extractPartitionDetails();
  642. }
  643. $storageEngines = StorageEngine::getArray();
  644. $partitionDetails = TablePartitionDefinition::getDetails($partitionDetails);
  645. $this->render('table/structure/partition_definition_form', [
  646. 'db' => $this->db,
  647. 'table' => $this->table,
  648. 'partition_details' => $partitionDetails,
  649. 'storage_engines' => $storageEngines,
  650. ]);
  651. }
  652. /**
  653. * Extracts partition details from CREATE TABLE statement
  654. *
  655. * @return array[]|null array of partition details
  656. */
  657. private function extractPartitionDetails(): ?array
  658. {
  659. $createTable = (new Table($this->table, $this->db))->showCreate();
  660. if (! $createTable) {
  661. return null;
  662. }
  663. $parser = new Parser($createTable);
  664. /**
  665. * @var CreateStatement $stmt
  666. */
  667. $stmt = $parser->statements[0];
  668. $partitionDetails = [];
  669. $partitionDetails['partition_by'] = '';
  670. $partitionDetails['partition_expr'] = '';
  671. $partitionDetails['partition_count'] = '';
  672. if (! empty($stmt->partitionBy)) {
  673. $openPos = strpos($stmt->partitionBy, '(');
  674. $closePos = strrpos($stmt->partitionBy, ')');
  675. $partitionDetails['partition_by']
  676. = trim(substr($stmt->partitionBy, 0, $openPos));
  677. $partitionDetails['partition_expr']
  678. = trim(substr($stmt->partitionBy, $openPos + 1, $closePos - ($openPos + 1)));
  679. if (isset($stmt->partitionsNum)) {
  680. $count = $stmt->partitionsNum;
  681. } else {
  682. $count = count($stmt->partitions);
  683. }
  684. $partitionDetails['partition_count'] = $count;
  685. }
  686. $partitionDetails['subpartition_by'] = '';
  687. $partitionDetails['subpartition_expr'] = '';
  688. $partitionDetails['subpartition_count'] = '';
  689. if (! empty($stmt->subpartitionBy)) {
  690. $openPos = strpos($stmt->subpartitionBy, '(');
  691. $closePos = strrpos($stmt->subpartitionBy, ')');
  692. $partitionDetails['subpartition_by']
  693. = trim(substr($stmt->subpartitionBy, 0, $openPos));
  694. $partitionDetails['subpartition_expr']
  695. = trim(substr($stmt->subpartitionBy, $openPos + 1, $closePos - ($openPos + 1)));
  696. if (isset($stmt->subpartitionsNum)) {
  697. $count = $stmt->subpartitionsNum;
  698. } else {
  699. $count = count($stmt->partitions[0]->subpartitions);
  700. }
  701. $partitionDetails['subpartition_count'] = $count;
  702. }
  703. // Only LIST and RANGE type parameters allow subpartitioning
  704. $partitionDetails['can_have_subpartitions']
  705. = $partitionDetails['partition_count'] > 1
  706. && ($partitionDetails['partition_by'] === 'RANGE'
  707. || $partitionDetails['partition_by'] === 'RANGE COLUMNS'
  708. || $partitionDetails['partition_by'] === 'LIST'
  709. || $partitionDetails['partition_by'] === 'LIST COLUMNS');
  710. // Values are specified only for LIST and RANGE type partitions
  711. $partitionDetails['value_enabled'] = isset($partitionDetails['partition_by'])
  712. && ($partitionDetails['partition_by'] === 'RANGE'
  713. || $partitionDetails['partition_by'] === 'RANGE COLUMNS'
  714. || $partitionDetails['partition_by'] === 'LIST'
  715. || $partitionDetails['partition_by'] === 'LIST COLUMNS');
  716. $partitionDetails['partitions'] = [];
  717. for ($i = 0, $iMax = (int) $partitionDetails['partition_count']; $i < $iMax; $i++) {
  718. if (! isset($stmt->partitions[$i])) {
  719. $partitionDetails['partitions'][$i] = [
  720. 'name' => 'p' . $i,
  721. 'value_type' => '',
  722. 'value' => '',
  723. 'engine' => '',
  724. 'comment' => '',
  725. 'data_directory' => '',
  726. 'index_directory' => '',
  727. 'max_rows' => '',
  728. 'min_rows' => '',
  729. 'tablespace' => '',
  730. 'node_group' => '',
  731. ];
  732. } else {
  733. $p = $stmt->partitions[$i];
  734. $type = $p->type;
  735. $expr = trim((string) $p->expr, '()');
  736. if ($expr === 'MAXVALUE') {
  737. $type .= ' MAXVALUE';
  738. $expr = '';
  739. }
  740. $partitionDetails['partitions'][$i] = [
  741. 'name' => $p->name,
  742. 'value_type' => $type,
  743. 'value' => $expr,
  744. 'engine' => $p->options->has('ENGINE', true),
  745. 'comment' => trim((string) $p->options->has('COMMENT', true), "'"),
  746. 'data_directory' => trim((string) $p->options->has('DATA DIRECTORY', true), "'"),
  747. 'index_directory' => trim((string) $p->options->has('INDEX_DIRECTORY', true), "'"),
  748. 'max_rows' => $p->options->has('MAX_ROWS', true),
  749. 'min_rows' => $p->options->has('MIN_ROWS', true),
  750. 'tablespace' => $p->options->has('TABLESPACE', true),
  751. 'node_group' => $p->options->has('NODEGROUP', true),
  752. ];
  753. }
  754. $partition =& $partitionDetails['partitions'][$i];
  755. $partition['prefix'] = 'partitions[' . $i . ']';
  756. if ($partitionDetails['subpartition_count'] <= 1) {
  757. continue;
  758. }
  759. $partition['subpartition_count'] = $partitionDetails['subpartition_count'];
  760. $partition['subpartitions'] = [];
  761. for ($j = 0, $jMax = (int) $partitionDetails['subpartition_count']; $j < $jMax; $j++) {
  762. if (! isset($stmt->partitions[$i]->subpartitions[$j])) {
  763. $partition['subpartitions'][$j] = [
  764. 'name' => $partition['name'] . '_s' . $j,
  765. 'engine' => '',
  766. 'comment' => '',
  767. 'data_directory' => '',
  768. 'index_directory' => '',
  769. 'max_rows' => '',
  770. 'min_rows' => '',
  771. 'tablespace' => '',
  772. 'node_group' => '',
  773. ];
  774. } else {
  775. $sp = $stmt->partitions[$i]->subpartitions[$j];
  776. $partition['subpartitions'][$j] = [
  777. 'name' => $sp->name,
  778. 'engine' => $sp->options->has('ENGINE', true),
  779. 'comment' => trim((string) $sp->options->has('COMMENT', true), "'"),
  780. 'data_directory' => trim((string) $sp->options->has('DATA DIRECTORY', true), "'"),
  781. 'index_directory' => trim((string) $sp->options->has('INDEX_DIRECTORY', true), "'"),
  782. 'max_rows' => $sp->options->has('MAX_ROWS', true),
  783. 'min_rows' => $sp->options->has('MIN_ROWS', true),
  784. 'tablespace' => $sp->options->has('TABLESPACE', true),
  785. 'node_group' => $sp->options->has('NODEGROUP', true),
  786. ];
  787. }
  788. $subpartition =& $partition['subpartitions'][$j];
  789. $subpartition['prefix'] = 'partitions[' . $i . ']'
  790. . '[subpartitions][' . $j . ']';
  791. }
  792. }
  793. return $partitionDetails;
  794. }
  795. private function updatePartitioning(): void
  796. {
  797. $sql_query = 'ALTER TABLE ' . Util::backquote($this->table) . ' '
  798. . $this->createAddField->getPartitionsDefinition();
  799. // Execute alter query
  800. $result = $this->dbi->tryQuery($sql_query);
  801. if ($result === false) {
  802. $this->response->setRequestStatus(false);
  803. $this->response->addJSON(
  804. 'message',
  805. Message::rawError(
  806. __('Query error') . ':<br>' . $this->dbi->getError()
  807. )
  808. );
  809. return;
  810. }
  811. $message = Message::success(
  812. __('Table %1$s has been altered successfully.')
  813. );
  814. $message->addParam($this->table);
  815. $this->response->addHTML(
  816. Generator::getMessage($message, $sql_query, 'success')
  817. );
  818. }
  819. /**
  820. * Function to display table browse for selected columns
  821. *
  822. * @param string $goto goto page url
  823. * @param string $themeImagePath URI of the pma theme image
  824. *
  825. * @return void
  826. */
  827. protected function displayTableBrowseForSelectedColumns($goto, $themeImagePath)
  828. {
  829. $GLOBALS['active_page'] = Url::getFromRoute('/sql');
  830. $fields = [];
  831. foreach ($_POST['selected_fld'] as $sval) {
  832. $fields[] = Util::backquote($sval);
  833. }
  834. $sql_query = sprintf(
  835. 'SELECT %s FROM %s.%s',
  836. implode(', ', $fields),
  837. Util::backquote($this->db),
  838. Util::backquote($this->table)
  839. );
  840. // Parse and analyze the query
  841. $db = &$this->db;
  842. [
  843. $analyzed_sql_results,
  844. $db,
  845. ] = ParseAnalyze::sqlQuery($sql_query, $db);
  846. $sql = new Sql(
  847. $this->dbi,
  848. $this->relation,
  849. $this->relationCleanup,
  850. new Operations($this->dbi, $this->relation),
  851. $this->transformations,
  852. $this->template
  853. );
  854. $this->response->addHTML(
  855. $sql->executeQueryAndGetQueryResponse(
  856. $analyzed_sql_results ?? '',
  857. false, // is_gotofile
  858. $this->db, // db
  859. $this->table, // table
  860. null, // find_real_end
  861. null, // sql_query_for_bookmark
  862. null, // extra_data
  863. null, // message_to_show
  864. null, // sql_data
  865. $goto, // goto
  866. $themeImagePath,
  867. null, // disp_query
  868. null, // disp_message
  869. $sql_query, // sql_query
  870. null // complete_query
  871. )
  872. );
  873. }
  874. /**
  875. * Update the table's structure based on $_REQUEST
  876. *
  877. * @return bool true if error occurred
  878. */
  879. protected function updateColumns()
  880. {
  881. $err_url = Url::getFromRoute('/table/structure', [
  882. 'db' => $this->db,
  883. 'table' => $this->table,
  884. ]);
  885. $regenerate = false;
  886. $field_cnt = count($_POST['field_name'] ?? []);
  887. $changes = [];
  888. $adjust_privileges = [];
  889. $columns_with_index = $this->dbi
  890. ->getTable($this->db, $this->table)
  891. ->getColumnsWithIndex(
  892. Index::PRIMARY | Index::UNIQUE
  893. );
  894. for ($i = 0; $i < $field_cnt; $i++) {
  895. if (! $this->columnNeedsAlterTable($i)) {
  896. continue;
  897. }
  898. $changes[] = 'CHANGE ' . Table::generateAlter(
  899. Util::getValueByKey($_POST, "field_orig.${i}", ''),
  900. $_POST['field_name'][$i],
  901. $_POST['field_type'][$i],
  902. $_POST['field_length'][$i],
  903. $_POST['field_attribute'][$i],
  904. Util::getValueByKey($_POST, "field_collation.${i}", ''),
  905. Util::getValueByKey($_POST, "field_null.${i}", 'NO'),
  906. $_POST['field_default_type'][$i],
  907. $_POST['field_default_value'][$i],
  908. Util::getValueByKey($_POST, "field_extra.${i}", false),
  909. Util::getValueByKey($_POST, "field_comments.${i}", ''),
  910. Util::getValueByKey($_POST, "field_virtuality.${i}", ''),
  911. Util::getValueByKey($_POST, "field_expression.${i}", ''),
  912. Util::getValueByKey($_POST, "field_move_to.${i}", ''),
  913. $columns_with_index
  914. );
  915. // find the remembered sort expression
  916. $sorted_col = $this->tableObj->getUiProp(
  917. Table::PROP_SORTED_COLUMN
  918. );
  919. // if the old column name is part of the remembered sort expression
  920. if (mb_strpos(
  921. (string) $sorted_col,
  922. Util::backquote($_POST['field_orig'][$i])
  923. ) !== false) {
  924. // delete the whole remembered sort expression
  925. $this->tableObj->removeUiProp(Table::PROP_SORTED_COLUMN);
  926. }
  927. if (! isset($_POST['field_adjust_privileges'][$i])
  928. || empty($_POST['field_adjust_privileges'][$i])
  929. || $_POST['field_orig'][$i] == $_POST['field_name'][$i]
  930. ) {
  931. continue;
  932. }
  933. $adjust_privileges[$_POST['field_orig'][$i]]
  934. = $_POST['field_name'][$i];
  935. }
  936. if (count($changes) > 0 || isset($_POST['preview_sql'])) {
  937. // Builds the primary keys statements and updates the table
  938. $key_query = '';
  939. /**
  940. * this is a little bit more complex
  941. *
  942. * @todo if someone selects A_I when altering a column we need to check:
  943. * - no other column with A_I
  944. * - the column has an index, if not create one
  945. */
  946. // To allow replication, we first select the db to use
  947. // and then run queries on this db.
  948. if (! $this->dbi->selectDb($this->db)) {
  949. Generator::mysqlDie(
  950. $this->dbi->getError(),
  951. 'USE ' . Util::backquote($this->db) . ';',
  952. false,
  953. $err_url
  954. );
  955. }
  956. $sql_query = 'ALTER TABLE ' . Util::backquote($this->table) . ' ';
  957. $sql_query .= implode(', ', $changes) . $key_query;
  958. if (isset($_POST['online_transaction'])) {
  959. $sql_query .= ', ALGORITHM=INPLACE, LOCK=NONE';
  960. }
  961. $sql_query .= ';';
  962. // If there is a request for SQL previewing.
  963. if (isset($_POST['preview_sql'])) {
  964. Core::previewSQL(count($changes) > 0 ? $sql_query : '');
  965. exit;
  966. }
  967. $columns_with_index = $this->dbi
  968. ->getTable($this->db, $this->table)
  969. ->getColumnsWithIndex(
  970. Index::PRIMARY | Index::UNIQUE | Index::INDEX
  971. | Index::SPATIAL | Index::FULLTEXT
  972. );
  973. $changedToBlob = [];
  974. // While changing the Column Collation
  975. // First change to BLOB
  976. for ($i = 0; $i < $field_cnt; $i++) {
  977. if (isset($_POST['field_collation'][$i], $_POST['field_collation_orig'][$i])
  978. && $_POST['field_collation'][$i] !== $_POST['field_collation_orig'][$i]
  979. && ! in_array($_POST['field_orig'][$i], $columns_with_index)
  980. ) {
  981. $secondary_query = 'ALTER TABLE ' . Util::backquote(
  982. $this->table
  983. )
  984. . ' CHANGE ' . Util::backquote(
  985. $_POST['field_orig'][$i]
  986. )
  987. . ' ' . Util::backquote($_POST['field_orig'][$i])
  988. . ' BLOB';
  989. if (isset($_POST['field_virtuality'][$i], $_POST['field_expression'][$i])) {
  990. if ($_POST['field_virtuality'][$i]) {
  991. $secondary_query .= ' AS (' . $_POST['field_expression'][$i] . ') '
  992. . $_POST['field_virtuality'][$i];
  993. }
  994. }
  995. $secondary_query .= ';';
  996. $this->dbi->query($secondary_query);
  997. $changedToBlob[$i] = true;
  998. } else {
  999. $changedToBlob[$i] = false;
  1000. }
  1001. }
  1002. // Then make the requested changes
  1003. $result = $this->dbi->tryQuery($sql_query);
  1004. if ($result !== false) {
  1005. $changed_privileges = $this->adjustColumnPrivileges(
  1006. $adjust_privileges
  1007. );
  1008. if ($changed_privileges) {
  1009. $message = Message::success(
  1010. __(
  1011. 'Table %1$s has been altered successfully. Privileges ' .
  1012. 'have been adjusted.'
  1013. )
  1014. );
  1015. } else {
  1016. $message = Message::success(
  1017. __('Table %1$s has been altered successfully.')
  1018. );
  1019. }
  1020. $message->addParam($this->table);
  1021. $this->response->addHTML(
  1022. Generator::getMessage($message, $sql_query, 'success')
  1023. );
  1024. } else {
  1025. // An error happened while inserting/updating a table definition
  1026. // Save the Original Error
  1027. $orig_error = $this->dbi->getError();
  1028. $changes_revert = [];
  1029. // Change back to Original Collation and data type
  1030. for ($i = 0; $i < $field_cnt; $i++) {
  1031. if (! $changedToBlob[$i]) {
  1032. continue;
  1033. }
  1034. $changes_revert[] = 'CHANGE ' . Table::generateAlter(
  1035. Util::getValueByKey($_POST, "field_orig.${i}", ''),
  1036. $_POST['field_name'][$i],
  1037. $_POST['field_type_orig'][$i],
  1038. $_POST['field_length_orig'][$i],
  1039. $_POST['field_attribute_orig'][$i],
  1040. Util::getValueByKey($_POST, "field_collation_orig.${i}", ''),
  1041. Util::getValueByKey($_POST, "field_null_orig.${i}", 'NO'),
  1042. $_POST['field_default_type_orig'][$i],
  1043. $_POST['field_default_value_orig'][$i],
  1044. Util::getValueByKey($_POST, "field_extra_orig.${i}", false),
  1045. Util::getValueByKey($_POST, "field_comments_orig.${i}", ''),
  1046. Util::getValueByKey($_POST, "field_virtuality_orig.${i}", ''),
  1047. Util::getValueByKey($_POST, "field_expression_orig.${i}", ''),
  1048. Util::getValueByKey($_POST, "field_move_to_orig.${i}", '')
  1049. );
  1050. }
  1051. $revert_query = 'ALTER TABLE ' . Util::backquote($this->table)
  1052. . ' ';
  1053. $revert_query .= implode(', ', $changes_revert) . '';
  1054. $revert_query .= ';';
  1055. // Column reverted back to original
  1056. $this->dbi->query($revert_query);
  1057. $this->response->setRequestStatus(false);
  1058. $this->response->addJSON(
  1059. 'message',
  1060. Message::rawError(
  1061. __('Query error') . ':<br>' . $orig_error
  1062. )
  1063. );
  1064. $regenerate = true;
  1065. }
  1066. }
  1067. // update field names in relation
  1068. if (isset($_POST['field_orig']) && is_array($_POST['field_orig'])) {
  1069. foreach ($_POST['field_orig'] as $fieldindex => $fieldcontent) {
  1070. if ($_POST['field_name'][$fieldindex] == $fieldcontent) {
  1071. continue;
  1072. }
  1073. $this->relation->renameField(
  1074. $this->db,
  1075. $this->table,
  1076. $fieldcontent,
  1077. $_POST['field_name'][$fieldindex]
  1078. );
  1079. }
  1080. }
  1081. // update mime types
  1082. if (isset($_POST['field_mimetype'])
  1083. && is_array($_POST['field_mimetype'])
  1084. && $GLOBALS['cfg']['BrowseMIME']
  1085. ) {
  1086. foreach ($_POST['field_mimetype'] as $fieldindex => $mimetype) {
  1087. if (! isset($_POST['field_name'][$fieldindex])
  1088. || strlen($_POST['field_name'][$fieldindex]) <= 0
  1089. ) {
  1090. continue;
  1091. }
  1092. $this->transformations->setMime(
  1093. $this->db,
  1094. $this->table,
  1095. $_POST['field_name'][$fieldindex],
  1096. $mimetype,
  1097. $_POST['field_transformation'][$fieldindex],
  1098. $_POST['field_transformation_options'][$fieldindex],
  1099. $_POST['field_input_transformation'][$fieldindex],
  1100. $_POST['field_input_transformation_options'][$fieldindex]
  1101. );
  1102. }
  1103. }
  1104. return $regenerate;
  1105. }
  1106. /**
  1107. * Adjusts the Privileges for all the columns whose names have changed
  1108. *
  1109. * @param array $adjust_privileges assoc array of old col names mapped to new
  1110. * cols
  1111. *
  1112. * @return bool boolean whether at least one column privileges
  1113. * adjusted
  1114. */
  1115. protected function adjustColumnPrivileges(array $adjust_privileges)
  1116. {
  1117. $changed = false;
  1118. if (Util::getValueByKey($GLOBALS, 'col_priv', false)
  1119. && Util::getValueByKey($GLOBALS, 'is_reload_priv', false)
  1120. ) {
  1121. $this->dbi->selectDb('mysql');
  1122. // For Column specific privileges
  1123. foreach ($adjust_privileges as $oldCol => $newCol) {
  1124. $this->dbi->query(
  1125. sprintf(
  1126. 'UPDATE %s SET Column_name = "%s"
  1127. WHERE Db = "%s"
  1128. AND Table_name = "%s"
  1129. AND Column_name = "%s";',
  1130. Util::backquote('columns_priv'),
  1131. $newCol,
  1132. $this->db,
  1133. $this->table,
  1134. $oldCol
  1135. )
  1136. );
  1137. // i.e. if atleast one column privileges adjusted
  1138. $changed = true;
  1139. }
  1140. if ($changed) {
  1141. // Finally FLUSH the new privileges
  1142. $this->dbi->query('FLUSH PRIVILEGES;');
  1143. }
  1144. }
  1145. return $changed;
  1146. }
  1147. /**
  1148. * Verifies if some elements of a column have changed
  1149. *
  1150. * @param int $i column index in the request
  1151. *
  1152. * @return bool true if we need to generate ALTER TABLE
  1153. */
  1154. protected function columnNeedsAlterTable($i)
  1155. {
  1156. // these two fields are checkboxes so might not be part of the
  1157. // request; therefore we define them to avoid notices below
  1158. if (! isset($_POST['field_null'][$i])) {
  1159. $_POST['field_null'][$i] = 'NO';
  1160. }
  1161. if (! isset($_POST['field_extra'][$i])) {
  1162. $_POST['field_extra'][$i] = '';
  1163. }
  1164. // field_name does not follow the convention (corresponds to field_orig)
  1165. if ($_POST['field_name'][$i] != $_POST['field_orig'][$i]) {
  1166. return true;
  1167. }
  1168. $fields = [
  1169. 'field_attribute',
  1170. 'field_collation',
  1171. 'field_comments',
  1172. 'field_default_value',
  1173. 'field_default_type',
  1174. 'field_extra',
  1175. 'field_length',
  1176. 'field_null',
  1177. 'field_type',
  1178. ];
  1179. foreach ($fields as $field) {
  1180. if ($_POST[$field][$i] != $_POST[$field . '_orig'][$i]) {
  1181. return true;
  1182. }
  1183. }
  1184. return ! empty($_POST['field_move_to'][$i]);
  1185. }
  1186. /**
  1187. * Displays the table structure ('show table' works correct since 3.23.03)
  1188. *
  1189. * @param array $cfgRelation current relation parameters
  1190. * @param array $columns_with_unique_index Columns with unique index
  1191. * @param Index|false $primary_index primary index or false if
  1192. * no one exists
  1193. * @param array $fields Fields
  1194. * @param array $columns_with_index Columns with index
  1195. *
  1196. * @return string
  1197. */
  1198. protected function displayStructure(
  1199. array $cfgRelation,
  1200. array $columns_with_unique_index,
  1201. $primary_index,
  1202. array $fields,
  1203. array $columns_with_index,
  1204. bool $isSystemSchema
  1205. ) {
  1206. global $route, $tbl_is_view, $tbl_storage_engine, $PMA_Theme;
  1207. // prepare comments
  1208. $comments_map = [];
  1209. $mime_map = [];
  1210. if ($GLOBALS['cfg']['ShowPropertyComments']) {
  1211. $comments_map = $this->relation->getComments($this->db, $this->table);
  1212. if ($cfgRelation['mimework'] && $GLOBALS['cfg']['BrowseMIME']) {
  1213. $mime_map = $this->transformations->getMime($this->db, $this->table, true);
  1214. }
  1215. }
  1216. $centralColumns = new CentralColumns($this->dbi);
  1217. $central_list = $centralColumns->getFromTable(
  1218. $this->db,
  1219. $this->table
  1220. );
  1221. /**
  1222. * Displays Space usage and row statistics
  1223. */
  1224. // BEGIN - Calc Table Space
  1225. // Get valid statistics whatever is the table type
  1226. if ($GLOBALS['cfg']['ShowStats']) {
  1227. //get table stats in HTML format
  1228. $tablestats = $this->getTableStats($isSystemSchema);
  1229. //returning the response in JSON format to be used by Ajax
  1230. $this->response->addJSON('tableStat', $tablestats);
  1231. }
  1232. // END - Calc Table Space
  1233. $hideStructureActions = false;
  1234. if ($GLOBALS['cfg']['HideStructureActions'] === true) {
  1235. $hideStructureActions = true;
  1236. }
  1237. // logic removed from Template
  1238. $rownum = 0;
  1239. $columns_list = [];
  1240. $attributes = [];
  1241. $displayed_fields = [];
  1242. $row_comments = [];
  1243. $extracted_columnspecs = [];
  1244. $collations = [];
  1245. foreach ($fields as &$field) {
  1246. ++$rownum;
  1247. $columns_list[] = $field['Field'];
  1248. $extracted_columnspecs[$rownum] = Util::extractColumnSpec($field['Type']);
  1249. $attributes[$rownum] = $extracted_columnspecs[$rownum]['attribute'];
  1250. if (strpos($field['Extra'], 'on update CURRENT_TIMESTAMP') !== false) {
  1251. $attributes[$rownum] = 'on update CURRENT_TIMESTAMP';
  1252. }
  1253. $displayed_fields[$rownum] = new stdClass();
  1254. $displayed_fields[$rownum]->text = $field['Field'];
  1255. $displayed_fields[$rownum]->icon = '';
  1256. $row_comments[$rownum] = '';
  1257. if (isset($comments_map[$field['Field']])) {
  1258. $displayed_fields[$rownum]->comment = $comments_map[$field['Field']];
  1259. $row_comments[$rownum] = $comments_map[$field['Field']];
  1260. }
  1261. if ($primary_index && $primary_index->hasColumn($field['Field'])) {
  1262. $displayed_fields[$rownum]->icon .=
  1263. Generator::getImage('b_primary', __('Primary'));
  1264. }
  1265. if (in_array($field['Field'], $columns_with_index)) {
  1266. $displayed_fields[$rownum]->icon .=
  1267. Generator::getImage('bd_primary', __('Index'));
  1268. }
  1269. $collation = Charsets::findCollationByName(
  1270. $this->dbi,
  1271. $GLOBALS['cfg']['Server']['DisableIS'],
  1272. $field['Collation'] ?? ''
  1273. );
  1274. if ($collation === null) {
  1275. continue;
  1276. }
  1277. $collations[$collation->getName()] = [
  1278. 'name' => $collation->getName(),
  1279. 'description' => $collation->getDescription(),
  1280. ];
  1281. }
  1282. $engine = $this->tableObj->getStorageEngine();
  1283. return $this->template->render('table/structure/display_structure', [
  1284. 'collations' => $collations,
  1285. 'is_foreign_key_supported' => Util::isForeignKeySupported($engine),
  1286. 'indexes' => Index::getFromTable($this->table, $this->db),
  1287. 'indexes_duplicates' => Index::findDuplicates($this->table, $this->db),
  1288. 'cfg_relation' => $this->relation->getRelationsParam(),
  1289. 'hide_structure_actions' => $hideStructureActions,
  1290. 'db' => $this->db,
  1291. 'table' => $this->table,
  1292. 'db_is_system_schema' => $isSystemSchema,
  1293. 'tbl_is_view' => $tbl_is_view,
  1294. 'mime_map' => $mime_map,
  1295. 'tbl_storage_engine' => $tbl_storage_engine,
  1296. 'primary' => $primary_index,
  1297. 'columns_with_unique_index' => $columns_with_unique_index,
  1298. 'columns_list' => $columns_list,
  1299. 'table_stats' => $tablestats ?? null,
  1300. 'fields' => $fields,
  1301. 'extracted_columnspecs' => $extracted_columnspecs,
  1302. 'columns_with_index' => $columns_with_index,
  1303. 'central_list' => $central_list,
  1304. 'comments_map' => $comments_map,
  1305. 'browse_mime' => $GLOBALS['cfg']['BrowseMIME'],
  1306. 'show_column_comments' => $GLOBALS['cfg']['ShowColumnComments'],
  1307. 'show_stats' => $GLOBALS['cfg']['ShowStats'],
  1308. 'relation_commwork' => $GLOBALS['cfgRelation']['commwork'],
  1309. 'relation_mimework' => $GLOBALS['cfgRelation']['mimework'],
  1310. 'central_columns_work' => $GLOBALS['cfgRelation']['centralcolumnswork'],
  1311. 'mysql_int_version' => $this->dbi->getVersion(),
  1312. 'is_mariadb' => $this->dbi->isMariaDB(),
  1313. 'theme_image_path' => $PMA_Theme->getImgPath(),
  1314. 'text_dir' => $GLOBALS['text_dir'],
  1315. 'is_active' => Tracker::isActive(),
  1316. 'have_partitioning' => Partition::havePartitioning(),
  1317. 'partitions' => Partition::getPartitions($this->db, $this->table),
  1318. 'partition_names' => Partition::getPartitionNames($this->db, $this->table),
  1319. 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
  1320. 'attributes' => $attributes,
  1321. 'displayed_fields' => $displayed_fields,
  1322. 'row_comments' => $row_comments,
  1323. 'route' => $route,
  1324. ]);
  1325. }
  1326. /**
  1327. * Get HTML snippet for display table statistics
  1328. *
  1329. * @return string
  1330. */
  1331. protected function getTableStats(bool $isSystemSchema)
  1332. {
  1333. global $showtable, $tbl_is_view;
  1334. global $tbl_storage_engine, $table_info_num_rows, $tbl_collation;
  1335. if (empty($showtable)) {
  1336. $showtable = $this->dbi->getTable(
  1337. $this->db,
  1338. $this->table
  1339. )->getStatusInfo(null, true);
  1340. }
  1341. if (is_string($showtable)) {
  1342. $showtable = [];
  1343. }
  1344. if (empty($showtable['Data_length'])) {
  1345. $showtable['Data_length'] = 0;
  1346. }
  1347. if (empty($showtable['Index_length'])) {
  1348. $showtable['Index_length'] = 0;
  1349. }
  1350. $is_innodb = (isset($showtable['Type'])
  1351. && $showtable['Type'] === 'InnoDB');
  1352. $mergetable = $this->tableObj->isMerge();
  1353. // this is to display for example 261.2 MiB instead of 268k KiB
  1354. $max_digits = 3;
  1355. $decimals = 1;
  1356. [$data_size, $data_unit] = Util::formatByteDown(
  1357. $showtable['Data_length'],
  1358. $max_digits,
  1359. $decimals
  1360. );
  1361. if ($mergetable === false) {
  1362. [$index_size, $index_unit] = Util::formatByteDown(
  1363. $showtable['Index_length'],
  1364. $max_digits,
  1365. $decimals
  1366. );
  1367. }
  1368. if (isset($showtable['Data_free'])) {
  1369. [$free_size, $free_unit] = Util::formatByteDown(
  1370. $showtable['Data_free'],
  1371. $max_digits,
  1372. $decimals
  1373. );
  1374. [$effect_size, $effect_unit] = Util::formatByteDown(
  1375. $showtable['Data_length']
  1376. + $showtable['Index_length']
  1377. - $showtable['Data_free'],
  1378. $max_digits,
  1379. $decimals
  1380. );
  1381. } else {
  1382. [$effect_size, $effect_unit] = Util::formatByteDown(
  1383. $showtable['Data_length']
  1384. + $showtable['Index_length'],
  1385. $max_digits,
  1386. $decimals
  1387. );
  1388. }
  1389. [$tot_size, $tot_unit] = Util::formatByteDown(
  1390. $showtable['Data_length'] + $showtable['Index_length'],
  1391. $max_digits,
  1392. $decimals
  1393. );
  1394. if ($table_info_num_rows > 0) {
  1395. [$avg_size, $avg_unit] = Util::formatByteDown(
  1396. ($showtable['Data_length']
  1397. + $showtable['Index_length'])
  1398. / $showtable['Rows'],
  1399. 6,
  1400. 1
  1401. );
  1402. } else {
  1403. $avg_size = $avg_unit = '';
  1404. }
  1405. /** @var Innodb $innodbEnginePlugin */
  1406. $innodbEnginePlugin = StorageEngine::getEngine('Innodb');
  1407. $innodb_file_per_table = $innodbEnginePlugin->supportsFilePerTable();
  1408. $engine = $this->dbi->getTable($this->db, $this->table)->getStorageEngine();
  1409. $tableCollation = [];
  1410. $collation = Charsets::findCollationByName(
  1411. $this->dbi,
  1412. $GLOBALS['cfg']['Server']['DisableIS'],
  1413. $tbl_collation
  1414. );
  1415. if ($collation !== null) {
  1416. $tableCollation = [
  1417. 'name' => $collation->getName(),
  1418. 'description' => $collation->getDescription(),
  1419. ];
  1420. }
  1421. return $this->template->render('table/structure/display_table_stats', [
  1422. 'db' => $GLOBALS['db'],
  1423. 'table' => $GLOBALS['table'],
  1424. 'is_foreign_key_supported' => Util::isForeignKeySupported($engine),
  1425. 'cfg_relation' => $this->relation->getRelationsParam(),
  1426. 'showtable' => $showtable,
  1427. 'table_info_num_rows' => $table_info_num_rows,
  1428. 'tbl_is_view' => $tbl_is_view,
  1429. 'db_is_system_schema' => $isSystemSchema,
  1430. 'tbl_storage_engine' => $tbl_storage_engine,
  1431. 'table_collation' => $tableCollation,
  1432. 'is_innodb' => $is_innodb,
  1433. 'mergetable' => $mergetable,
  1434. 'avg_size' => $avg_size ?? null,
  1435. 'avg_unit' => $avg_unit ?? null,
  1436. 'data_size' => $data_size,
  1437. 'data_unit' => $data_unit,
  1438. 'index_size' => $index_size ?? null,
  1439. 'index_unit' => $index_unit ?? null,
  1440. 'innodb_file_per_table' => $innodb_file_per_table,
  1441. 'free_size' => $free_size ?? null,
  1442. 'free_unit' => $free_unit ?? null,
  1443. 'effect_size' => $effect_size,
  1444. 'effect_unit' => $effect_unit,
  1445. 'tot_size' => $tot_size,
  1446. 'tot_unit' => $tot_unit,
  1447. ]);
  1448. }
  1449. /**
  1450. * Gets table primary key
  1451. *
  1452. * @return string
  1453. */
  1454. protected function getKeyForTablePrimary()
  1455. {
  1456. $this->dbi->selectDb($this->db);
  1457. $result = $this->dbi->query(
  1458. 'SHOW KEYS FROM ' . Util::backquote($this->table) . ';'
  1459. );
  1460. $primary = '';
  1461. while ($row = $this->dbi->fetchAssoc($result)) {
  1462. // Backups the list of primary keys
  1463. if (! is_array($row) || $row['Key_name'] !== 'PRIMARY') {
  1464. continue;
  1465. }
  1466. $primary .= $row['Column_name'] . ', ';
  1467. }
  1468. $this->dbi->freeResult($result);
  1469. return $primary;
  1470. }
  1471. /**
  1472. * Handles MySQL reserved words columns check.
  1473. */
  1474. public function reservedWordCheck(): void
  1475. {
  1476. if ($GLOBALS['cfg']['ReservedWordDisableWarning'] !== false) {
  1477. $this->response->setRequestStatus(false);
  1478. return;
  1479. }
  1480. $columns_names = $_POST['field_name'];
  1481. $reserved_keywords_names = [];
  1482. foreach ($columns_names as $column) {
  1483. if (! Context::isKeyword(trim($column), true)) {
  1484. continue;
  1485. }
  1486. $reserved_keywords_names[] = trim($column);
  1487. }
  1488. if (Context::isKeyword(trim($this->table), true)) {
  1489. $reserved_keywords_names[] = trim($this->table);
  1490. }
  1491. if (count($reserved_keywords_names) === 0) {
  1492. $this->response->setRequestStatus(false);
  1493. }
  1494. $this->response->addJSON(
  1495. 'message',
  1496. sprintf(
  1497. _ngettext(
  1498. 'The name \'%s\' is a MySQL reserved keyword.',
  1499. 'The names \'%s\' are MySQL reserved keywords.',
  1500. count($reserved_keywords_names)
  1501. ),
  1502. implode(',', $reserved_keywords_names)
  1503. )
  1504. );
  1505. }
  1506. }