CreateAddField.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use PhpMyAdmin\Html\Generator;
  5. use function array_merge;
  6. use function count;
  7. use function implode;
  8. use function in_array;
  9. use function intval;
  10. use function json_decode;
  11. use function min;
  12. use function preg_replace;
  13. use function strlen;
  14. use function trim;
  15. /**
  16. * Set of functions for /table/create and /table/add-field
  17. */
  18. class CreateAddField
  19. {
  20. /** @var DatabaseInterface */
  21. private $dbi;
  22. /**
  23. * @param DatabaseInterface $dbi DatabaseInterface interface
  24. */
  25. public function __construct(DatabaseInterface $dbi)
  26. {
  27. $this->dbi = $dbi;
  28. }
  29. /**
  30. * Transforms the radio button field_key into 4 arrays
  31. *
  32. * @return array An array of arrays which represents column keys for each index type
  33. */
  34. private function getIndexedColumns(): array
  35. {
  36. $fieldCount = count($_POST['field_name']);
  37. $fieldPrimary = json_decode($_POST['primary_indexes'], true);
  38. $fieldIndex = json_decode($_POST['indexes'], true);
  39. $fieldUnique = json_decode($_POST['unique_indexes'], true);
  40. $fieldFullText = json_decode($_POST['fulltext_indexes'], true);
  41. $fieldSpatial = json_decode($_POST['spatial_indexes'], true);
  42. return [
  43. $fieldCount,
  44. $fieldPrimary,
  45. $fieldIndex,
  46. $fieldUnique,
  47. $fieldFullText,
  48. $fieldSpatial,
  49. ];
  50. }
  51. /**
  52. * Initiate the column creation statement according to the table creation or
  53. * add columns to a existing table
  54. *
  55. * @param int $fieldCount number of columns
  56. * @param bool $isCreateTable true if requirement is to get the statement
  57. * for table creation
  58. *
  59. * @return array An array of initial sql statements
  60. * according to the request
  61. */
  62. private function buildColumnCreationStatement(
  63. int $fieldCount,
  64. bool $isCreateTable = true
  65. ): array {
  66. $definitions = [];
  67. $previousField = -1;
  68. for ($i = 0; $i < $fieldCount; ++$i) {
  69. // '0' is also empty for php :-(
  70. if (strlen($_POST['field_name'][$i]) === 0) {
  71. continue;
  72. }
  73. $definition = $this->getStatementPrefix($isCreateTable) .
  74. Table::generateFieldSpec(
  75. trim($_POST['field_name'][$i]),
  76. $_POST['field_type'][$i],
  77. $_POST['field_length'][$i],
  78. $_POST['field_attribute'][$i],
  79. $_POST['field_collation'][$i] ?? '',
  80. $_POST['field_null'][$i] ?? 'NO',
  81. $_POST['field_default_type'][$i],
  82. $_POST['field_default_value'][$i],
  83. $_POST['field_extra'][$i] ?? false,
  84. $_POST['field_comments'][$i] ?? '',
  85. $_POST['field_virtuality'][$i] ?? '',
  86. $_POST['field_expression'][$i] ?? ''
  87. );
  88. $definition .= $this->setColumnCreationStatementSuffix(
  89. $previousField,
  90. $isCreateTable
  91. );
  92. $previousField = $i;
  93. $definitions[] = $definition;
  94. }
  95. return $definitions;
  96. }
  97. /**
  98. * Set column creation suffix according to requested position of the new column
  99. *
  100. * @param int $previousField previous field for ALTER statement
  101. * @param bool $isCreateTable true if requirement is to get the statement
  102. * for table creation
  103. *
  104. * @return string suffix
  105. */
  106. private function setColumnCreationStatementSuffix(
  107. int $previousField,
  108. bool $isCreateTable = true
  109. ): string {
  110. // no suffix is needed if request is a table creation
  111. $sqlSuffix = ' ';
  112. if ($isCreateTable) {
  113. return $sqlSuffix;
  114. }
  115. if ((string) $_POST['field_where'] === 'last') {
  116. return $sqlSuffix;
  117. }
  118. // Only the first field can be added somewhere other than at the end
  119. if ($previousField == -1) {
  120. if ((string) $_POST['field_where'] === 'first') {
  121. $sqlSuffix .= ' FIRST';
  122. } elseif (! empty($_POST['after_field'])) {
  123. $sqlSuffix .= ' AFTER '
  124. . Util::backquote($_POST['after_field']);
  125. }
  126. } else {
  127. $sqlSuffix .= ' AFTER '
  128. . Util::backquote(
  129. $_POST['field_name'][$previousField]
  130. );
  131. }
  132. return $sqlSuffix;
  133. }
  134. /**
  135. * Create relevant index statements
  136. *
  137. * @param array $index an array of index columns
  138. * @param string $indexChoice index choice that which represents
  139. * the index type of $indexed_fields
  140. * @param bool $isCreateTable true if requirement is to get the statement
  141. * for table creation
  142. *
  143. * @return array an array of sql statements for indexes
  144. */
  145. private function buildIndexStatements(
  146. array $index,
  147. string $indexChoice,
  148. bool $isCreateTable = true
  149. ): array {
  150. $statement = [];
  151. if (! count($index)) {
  152. return $statement;
  153. }
  154. $sqlQuery = $this->getStatementPrefix($isCreateTable)
  155. . ' ' . $indexChoice;
  156. if (! empty($index['Key_name']) && $index['Key_name'] !== 'PRIMARY') {
  157. $sqlQuery .= ' ' . Util::backquote($index['Key_name']);
  158. }
  159. $indexFields = [];
  160. foreach ($index['columns'] as $key => $column) {
  161. $indexFields[$key] = Util::backquote(
  162. $_POST['field_name'][$column['col_index']]
  163. );
  164. if (! $column['size']) {
  165. continue;
  166. }
  167. $indexFields[$key] .= '(' . $column['size'] . ')';
  168. }
  169. $sqlQuery .= ' (' . implode(', ', $indexFields) . ')';
  170. $keyBlockSizes = $index['Key_block_size'];
  171. if (! empty($keyBlockSizes)) {
  172. $sqlQuery .= ' KEY_BLOCK_SIZE = '
  173. . $this->dbi->escapeString($keyBlockSizes);
  174. }
  175. // specifying index type is allowed only for primary, unique and index only
  176. $type = $index['Index_type'];
  177. if ($index['Index_choice'] !== 'SPATIAL'
  178. && $index['Index_choice'] !== 'FULLTEXT'
  179. && in_array($type, Index::getIndexTypes())
  180. ) {
  181. $sqlQuery .= ' USING ' . $type;
  182. }
  183. $parser = $index['Parser'];
  184. if ($index['Index_choice'] === 'FULLTEXT' && ! empty($parser)) {
  185. $sqlQuery .= ' WITH PARSER ' . $this->dbi->escapeString($parser);
  186. }
  187. $comment = $index['Index_comment'];
  188. if (! empty($comment)) {
  189. $sqlQuery .= " COMMENT '" . $this->dbi->escapeString($comment)
  190. . "'";
  191. }
  192. $statement[] = $sqlQuery;
  193. return $statement;
  194. }
  195. /**
  196. * Statement prefix for the buildColumnCreationStatement()
  197. *
  198. * @param bool $isCreateTable true if requirement is to get the statement
  199. * for table creation
  200. *
  201. * @return string prefix
  202. */
  203. private function getStatementPrefix(bool $isCreateTable = true): string
  204. {
  205. $sqlPrefix = ' ';
  206. if (! $isCreateTable) {
  207. $sqlPrefix = ' ADD ';
  208. }
  209. return $sqlPrefix;
  210. }
  211. /**
  212. * Merge index definitions for one type of index
  213. *
  214. * @param array $definitions the index definitions to merge to
  215. * @param bool $isCreateTable true if requirement is to get the statement
  216. * for table creation
  217. * @param array $indexedColumns the columns for one type of index
  218. * @param string $indexKeyword the index keyword to use in the definition
  219. *
  220. * @return array
  221. */
  222. private function mergeIndexStatements(
  223. array $definitions,
  224. bool $isCreateTable,
  225. array $indexedColumns,
  226. string $indexKeyword
  227. ): array {
  228. foreach ($indexedColumns as $index) {
  229. $statements = $this->buildIndexStatements(
  230. $index,
  231. ' ' . $indexKeyword . ' ',
  232. $isCreateTable
  233. );
  234. $definitions = array_merge($definitions, $statements);
  235. }
  236. return $definitions;
  237. }
  238. /**
  239. * Returns sql statement according to the column and index specifications as
  240. * requested
  241. *
  242. * @param bool $isCreateTable true if requirement is to get the statement
  243. * for table creation
  244. *
  245. * @return string sql statement
  246. */
  247. private function getColumnCreationStatements(bool $isCreateTable = true): string
  248. {
  249. $sqlStatement = '';
  250. [
  251. $fieldCount,
  252. $fieldPrimary,
  253. $fieldIndex,
  254. $fieldUnique,
  255. $fieldFullText,
  256. $fieldSpatial,
  257. ] = $this->getIndexedColumns();
  258. $definitions = $this->buildColumnCreationStatement(
  259. $fieldCount,
  260. $isCreateTable
  261. );
  262. // Builds the PRIMARY KEY statements
  263. $primaryKeyStatements = $this->buildIndexStatements(
  264. $fieldPrimary[0] ?? [],
  265. ' PRIMARY KEY ',
  266. $isCreateTable
  267. );
  268. $definitions = array_merge($definitions, $primaryKeyStatements);
  269. // Builds the INDEX statements
  270. $definitions = $this->mergeIndexStatements(
  271. $definitions,
  272. $isCreateTable,
  273. $fieldIndex,
  274. 'INDEX'
  275. );
  276. // Builds the UNIQUE statements
  277. $definitions = $this->mergeIndexStatements(
  278. $definitions,
  279. $isCreateTable,
  280. $fieldUnique,
  281. 'UNIQUE'
  282. );
  283. // Builds the FULLTEXT statements
  284. $definitions = $this->mergeIndexStatements(
  285. $definitions,
  286. $isCreateTable,
  287. $fieldFullText,
  288. 'FULLTEXT'
  289. );
  290. // Builds the SPATIAL statements
  291. $definitions = $this->mergeIndexStatements(
  292. $definitions,
  293. $isCreateTable,
  294. $fieldSpatial,
  295. 'SPATIAL'
  296. );
  297. if (count($definitions)) {
  298. $sqlStatement = implode(', ', $definitions);
  299. }
  300. return (string) preg_replace('@, $@', '', $sqlStatement);
  301. }
  302. /**
  303. * Returns the partitioning clause
  304. *
  305. * @return string partitioning clause
  306. */
  307. public function getPartitionsDefinition(): string
  308. {
  309. $sqlQuery = '';
  310. if (! empty($_POST['partition_by'])
  311. && ! empty($_POST['partition_expr'])
  312. && ! empty($_POST['partition_count'])
  313. && $_POST['partition_count'] > 1
  314. ) {
  315. $sqlQuery .= ' PARTITION BY ' . $_POST['partition_by']
  316. . ' (' . $_POST['partition_expr'] . ')'
  317. . ' PARTITIONS ' . $_POST['partition_count'];
  318. }
  319. if (! empty($_POST['subpartition_by'])
  320. && ! empty($_POST['subpartition_expr'])
  321. && ! empty($_POST['subpartition_count'])
  322. && $_POST['subpartition_count'] > 1
  323. ) {
  324. $sqlQuery .= ' SUBPARTITION BY ' . $_POST['subpartition_by']
  325. . ' (' . $_POST['subpartition_expr'] . ')'
  326. . ' SUBPARTITIONS ' . $_POST['subpartition_count'];
  327. }
  328. if (! empty($_POST['partitions'])) {
  329. $partitions = [];
  330. foreach ($_POST['partitions'] as $partition) {
  331. $partitions[] = $this->getPartitionDefinition($partition);
  332. }
  333. $sqlQuery .= ' (' . implode(', ', $partitions) . ')';
  334. }
  335. return $sqlQuery;
  336. }
  337. /**
  338. * Returns the definition of a partition/subpartition
  339. *
  340. * @param array $partition array of partition/subpartition details
  341. * @param bool $isSubPartition whether a subpartition
  342. *
  343. * @return string partition/subpartition definition
  344. */
  345. private function getPartitionDefinition(
  346. array $partition,
  347. bool $isSubPartition = false
  348. ): string {
  349. $sqlQuery = ' ' . ($isSubPartition ? 'SUB' : '') . 'PARTITION ';
  350. $sqlQuery .= $partition['name'];
  351. if (! empty($partition['value_type'])) {
  352. $sqlQuery .= ' VALUES ' . $partition['value_type'];
  353. if ($partition['value_type'] !== 'LESS THAN MAXVALUE') {
  354. $sqlQuery .= ' (' . $partition['value'] . ')';
  355. }
  356. }
  357. if (! empty($partition['engine'])) {
  358. $sqlQuery .= ' ENGINE = ' . $partition['engine'];
  359. }
  360. if (! empty($partition['comment'])) {
  361. $sqlQuery .= " COMMENT = '" . $partition['comment'] . "'";
  362. }
  363. if (! empty($partition['data_directory'])) {
  364. $sqlQuery .= " DATA DIRECTORY = '" . $partition['data_directory'] . "'";
  365. }
  366. if (! empty($partition['index_directory'])) {
  367. $sqlQuery .= " INDEX_DIRECTORY = '" . $partition['index_directory'] . "'";
  368. }
  369. if (! empty($partition['max_rows'])) {
  370. $sqlQuery .= ' MAX_ROWS = ' . $partition['max_rows'];
  371. }
  372. if (! empty($partition['min_rows'])) {
  373. $sqlQuery .= ' MIN_ROWS = ' . $partition['min_rows'];
  374. }
  375. if (! empty($partition['tablespace'])) {
  376. $sqlQuery .= ' TABLESPACE = ' . $partition['tablespace'];
  377. }
  378. if (! empty($partition['node_group'])) {
  379. $sqlQuery .= ' NODEGROUP = ' . $partition['node_group'];
  380. }
  381. if (! empty($partition['subpartitions'])) {
  382. $subpartitions = [];
  383. foreach ($partition['subpartitions'] as $subpartition) {
  384. $subpartitions[] = $this->getPartitionDefinition(
  385. $subpartition,
  386. true
  387. );
  388. }
  389. $sqlQuery .= ' (' . implode(', ', $subpartitions) . ')';
  390. }
  391. return $sqlQuery;
  392. }
  393. /**
  394. * Function to get table creation sql query
  395. *
  396. * @param string $db database name
  397. * @param string $table table name
  398. */
  399. public function getTableCreationQuery(string $db, string $table): string
  400. {
  401. // get column addition statements
  402. $sqlStatement = $this->getColumnCreationStatements(true);
  403. // Builds the 'create table' statement
  404. $sqlQuery = 'CREATE TABLE ' . Util::backquote($db) . '.'
  405. . Util::backquote(trim($table)) . ' (' . $sqlStatement . ')';
  406. // Adds table type, character set, comments and partition definition
  407. if (! empty($_POST['tbl_storage_engine'])
  408. && ($_POST['tbl_storage_engine'] !== 'Default')
  409. ) {
  410. $sqlQuery .= ' ENGINE = ' . $this->dbi->escapeString($_POST['tbl_storage_engine']);
  411. }
  412. if (! empty($_POST['tbl_collation'])) {
  413. $sqlQuery .= Util::getCharsetQueryPart($_POST['tbl_collation'] ?? '');
  414. }
  415. if (! empty($_POST['connection'])
  416. && ! empty($_POST['tbl_storage_engine'])
  417. && $_POST['tbl_storage_engine'] === 'FEDERATED'
  418. ) {
  419. $sqlQuery .= " CONNECTION = '"
  420. . $this->dbi->escapeString($_POST['connection']) . "'";
  421. }
  422. if (! empty($_POST['comment'])) {
  423. $sqlQuery .= ' COMMENT = \''
  424. . $this->dbi->escapeString($_POST['comment']) . '\'';
  425. }
  426. $sqlQuery .= $this->getPartitionsDefinition();
  427. $sqlQuery .= ';';
  428. return $sqlQuery;
  429. }
  430. /**
  431. * Function to get the number of fields for the table creation form
  432. */
  433. public function getNumberOfFieldsFromRequest(): int
  434. {
  435. // Limit to 4096 fields (MySQL maximal value)
  436. $mysqlLimit = 4096;
  437. if (isset($_POST['submit_num_fields'])) { // adding new fields
  438. $numberOfFields = intval($_POST['orig_num_fields']) + intval($_POST['added_fields']);
  439. } elseif (isset($_POST['orig_num_fields'])) { // retaining existing fields
  440. $numberOfFields = intval($_POST['orig_num_fields']);
  441. } elseif (isset($_POST['num_fields'])
  442. && intval($_POST['num_fields']) > 0
  443. ) { // new table with specified number of fields
  444. $numberOfFields = intval($_POST['num_fields']);
  445. } else { // new table with unspecified number of fields
  446. $numberOfFields = 4;
  447. }
  448. return min($numberOfFields, $mysqlLimit);
  449. }
  450. /**
  451. * Function to execute the column creation statement
  452. *
  453. * @param string $db current database
  454. * @param string $table current table
  455. * @param string $errorUrl error page url
  456. *
  457. * @return array
  458. */
  459. public function tryColumnCreationQuery(
  460. string $db,
  461. string $table,
  462. string $errorUrl
  463. ): array {
  464. // get column addition statements
  465. $sqlStatement = $this->getColumnCreationStatements(false);
  466. // To allow replication, we first select the db to use and then run queries
  467. // on this db.
  468. if (! $this->dbi->selectDb($db)) {
  469. Generator::mysqlDie(
  470. $this->dbi->getError(),
  471. 'USE ' . Util::backquote($db),
  472. false,
  473. $errorUrl
  474. );
  475. }
  476. $sqlQuery = 'ALTER TABLE ' .
  477. Util::backquote($table) . ' ' . $sqlStatement;
  478. if (isset($_POST['online_transaction'])) {
  479. $sqlQuery .= ', ALGORITHM=INPLACE, LOCK=NONE';
  480. }
  481. $sqlQuery .= ';';
  482. // If there is a request for SQL previewing.
  483. if (isset($_POST['preview_sql'])) {
  484. Core::previewSQL($sqlQuery);
  485. exit;
  486. }
  487. return [
  488. $this->dbi->tryQuery($sqlQuery),
  489. $sqlQuery,
  490. ];
  491. }
  492. }