PageRenderTime 28ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/CreateAddField.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 538 lines | 339 code | 76 blank | 123 comment | 52 complexity | 29a04ec5426382c73187c65e8151cda3 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  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($previousField, $isCreateTable);
  89. $previousField = $i;
  90. $definitions[] = $definition;
  91. }
  92. return $definitions;
  93. }
  94. /**
  95. * Set column creation suffix according to requested position of the new column
  96. *
  97. * @param int $previousField previous field for ALTER statement
  98. * @param bool $isCreateTable true if requirement is to get the statement
  99. * for table creation
  100. *
  101. * @return string suffix
  102. */
  103. private function setColumnCreationStatementSuffix(
  104. int $previousField,
  105. bool $isCreateTable = true
  106. ): string {
  107. // no suffix is needed if request is a table creation
  108. $sqlSuffix = ' ';
  109. if ($isCreateTable) {
  110. return $sqlSuffix;
  111. }
  112. if ((string) $_POST['field_where'] === 'last') {
  113. return $sqlSuffix;
  114. }
  115. // Only the first field can be added somewhere other than at the end
  116. if ($previousField == -1) {
  117. if ((string) $_POST['field_where'] === 'first') {
  118. $sqlSuffix .= ' FIRST';
  119. } elseif (! empty($_POST['after_field'])) {
  120. $sqlSuffix .= ' AFTER '
  121. . Util::backquote($_POST['after_field']);
  122. }
  123. return $sqlSuffix;
  124. }
  125. return $sqlSuffix
  126. . ' AFTER '
  127. . Util::backquote($_POST['field_name'][$previousField]);
  128. }
  129. /**
  130. * Create relevant index statements
  131. *
  132. * @param array $index an array of index columns
  133. * @param string $indexChoice index choice that which represents
  134. * the index type of $indexed_fields
  135. * @param bool $isCreateTable true if requirement is to get the statement
  136. * for table creation
  137. *
  138. * @return array an array of sql statements for indexes
  139. */
  140. private function buildIndexStatements(
  141. array $index,
  142. string $indexChoice,
  143. bool $isCreateTable = true
  144. ): array {
  145. $statement = [];
  146. if (! count($index)) {
  147. return $statement;
  148. }
  149. $sqlQuery = $this->getStatementPrefix($isCreateTable)
  150. . ' ' . $indexChoice;
  151. if (! empty($index['Key_name']) && $index['Key_name'] !== 'PRIMARY') {
  152. $sqlQuery .= ' ' . Util::backquote($index['Key_name']);
  153. }
  154. $indexFields = [];
  155. foreach ($index['columns'] as $key => $column) {
  156. $indexFields[$key] = Util::backquote($_POST['field_name'][$column['col_index']]);
  157. if (! $column['size']) {
  158. continue;
  159. }
  160. $indexFields[$key] .= '(' . $column['size'] . ')';
  161. }
  162. $sqlQuery .= ' (' . implode(', ', $indexFields) . ')';
  163. $keyBlockSizes = $index['Key_block_size'];
  164. if (! empty($keyBlockSizes)) {
  165. $sqlQuery .= ' KEY_BLOCK_SIZE = '
  166. . $this->dbi->escapeString($keyBlockSizes);
  167. }
  168. // specifying index type is allowed only for primary, unique and index only
  169. $type = $index['Index_type'];
  170. if (
  171. $index['Index_choice'] !== 'SPATIAL'
  172. && $index['Index_choice'] !== 'FULLTEXT'
  173. && in_array($type, Index::getIndexTypes())
  174. ) {
  175. $sqlQuery .= ' USING ' . $type;
  176. }
  177. $parser = $index['Parser'];
  178. if ($index['Index_choice'] === 'FULLTEXT' && ! empty($parser)) {
  179. $sqlQuery .= ' WITH PARSER ' . $this->dbi->escapeString($parser);
  180. }
  181. $comment = $index['Index_comment'];
  182. if (! empty($comment)) {
  183. $sqlQuery .= " COMMENT '" . $this->dbi->escapeString($comment)
  184. . "'";
  185. }
  186. $statement[] = $sqlQuery;
  187. return $statement;
  188. }
  189. /**
  190. * Statement prefix for the buildColumnCreationStatement()
  191. *
  192. * @param bool $isCreateTable true if requirement is to get the statement
  193. * for table creation
  194. *
  195. * @return string prefix
  196. */
  197. private function getStatementPrefix(bool $isCreateTable = true): string
  198. {
  199. $sqlPrefix = ' ';
  200. if (! $isCreateTable) {
  201. $sqlPrefix = ' ADD ';
  202. }
  203. return $sqlPrefix;
  204. }
  205. /**
  206. * Merge index definitions for one type of index
  207. *
  208. * @param array $definitions the index definitions to merge to
  209. * @param bool $isCreateTable true if requirement is to get the statement
  210. * for table creation
  211. * @param array $indexedColumns the columns for one type of index
  212. * @param string $indexKeyword the index keyword to use in the definition
  213. *
  214. * @return array
  215. */
  216. private function mergeIndexStatements(
  217. array $definitions,
  218. bool $isCreateTable,
  219. array $indexedColumns,
  220. string $indexKeyword
  221. ): array {
  222. foreach ($indexedColumns as $index) {
  223. $statements = $this->buildIndexStatements($index, ' ' . $indexKeyword . ' ', $isCreateTable);
  224. $definitions = array_merge($definitions, $statements);
  225. }
  226. return $definitions;
  227. }
  228. /**
  229. * Returns sql statement according to the column and index specifications as
  230. * requested
  231. *
  232. * @param bool $isCreateTable true if requirement is to get the statement
  233. * for table creation
  234. *
  235. * @return string sql statement
  236. */
  237. private function getColumnCreationStatements(bool $isCreateTable = true): string
  238. {
  239. $sqlStatement = '';
  240. [
  241. $fieldCount,
  242. $fieldPrimary,
  243. $fieldIndex,
  244. $fieldUnique,
  245. $fieldFullText,
  246. $fieldSpatial,
  247. ] = $this->getIndexedColumns();
  248. $definitions = $this->buildColumnCreationStatement($fieldCount, $isCreateTable);
  249. // Builds the PRIMARY KEY statements
  250. $primaryKeyStatements = $this->buildIndexStatements(
  251. $fieldPrimary[0] ?? [],
  252. ' PRIMARY KEY ',
  253. $isCreateTable
  254. );
  255. $definitions = array_merge($definitions, $primaryKeyStatements);
  256. // Builds the INDEX statements
  257. $definitions = $this->mergeIndexStatements($definitions, $isCreateTable, $fieldIndex, 'INDEX');
  258. // Builds the UNIQUE statements
  259. $definitions = $this->mergeIndexStatements($definitions, $isCreateTable, $fieldUnique, 'UNIQUE');
  260. // Builds the FULLTEXT statements
  261. $definitions = $this->mergeIndexStatements($definitions, $isCreateTable, $fieldFullText, 'FULLTEXT');
  262. // Builds the SPATIAL statements
  263. $definitions = $this->mergeIndexStatements($definitions, $isCreateTable, $fieldSpatial, 'SPATIAL');
  264. if (count($definitions)) {
  265. $sqlStatement = implode(', ', $definitions);
  266. }
  267. return (string) preg_replace('@, $@', '', $sqlStatement);
  268. }
  269. /**
  270. * Returns the partitioning clause
  271. *
  272. * @return string partitioning clause
  273. */
  274. public function getPartitionsDefinition(): string
  275. {
  276. $sqlQuery = '';
  277. if (
  278. ! empty($_POST['partition_by'])
  279. && ! empty($_POST['partition_expr'])
  280. && ! empty($_POST['partition_count'])
  281. && $_POST['partition_count'] > 1
  282. ) {
  283. $sqlQuery .= ' PARTITION BY ' . $_POST['partition_by']
  284. . ' (' . $_POST['partition_expr'] . ')'
  285. . ' PARTITIONS ' . $_POST['partition_count'];
  286. }
  287. if (
  288. ! empty($_POST['subpartition_by'])
  289. && ! empty($_POST['subpartition_expr'])
  290. && ! empty($_POST['subpartition_count'])
  291. && $_POST['subpartition_count'] > 1
  292. ) {
  293. $sqlQuery .= ' SUBPARTITION BY ' . $_POST['subpartition_by']
  294. . ' (' . $_POST['subpartition_expr'] . ')'
  295. . ' SUBPARTITIONS ' . $_POST['subpartition_count'];
  296. }
  297. if (! empty($_POST['partitions'])) {
  298. $partitions = [];
  299. foreach ($_POST['partitions'] as $partition) {
  300. $partitions[] = $this->getPartitionDefinition($partition);
  301. }
  302. $sqlQuery .= ' (' . implode(', ', $partitions) . ')';
  303. }
  304. return $sqlQuery;
  305. }
  306. /**
  307. * Returns the definition of a partition/subpartition
  308. *
  309. * @param array $partition array of partition/subpartition details
  310. * @param bool $isSubPartition whether a subpartition
  311. *
  312. * @return string partition/subpartition definition
  313. */
  314. private function getPartitionDefinition(
  315. array $partition,
  316. bool $isSubPartition = false
  317. ): string {
  318. $sqlQuery = ' ' . ($isSubPartition ? 'SUB' : '') . 'PARTITION ';
  319. $sqlQuery .= $partition['name'];
  320. if (! empty($partition['value_type'])) {
  321. $sqlQuery .= ' VALUES ' . $partition['value_type'];
  322. if ($partition['value_type'] !== 'LESS THAN MAXVALUE') {
  323. $sqlQuery .= ' (' . $partition['value'] . ')';
  324. }
  325. }
  326. if (! empty($partition['engine'])) {
  327. $sqlQuery .= ' ENGINE = ' . $partition['engine'];
  328. }
  329. if (! empty($partition['comment'])) {
  330. $sqlQuery .= " COMMENT = '" . $partition['comment'] . "'";
  331. }
  332. if (! empty($partition['data_directory'])) {
  333. $sqlQuery .= " DATA DIRECTORY = '" . $partition['data_directory'] . "'";
  334. }
  335. if (! empty($partition['index_directory'])) {
  336. $sqlQuery .= " INDEX_DIRECTORY = '" . $partition['index_directory'] . "'";
  337. }
  338. if (! empty($partition['max_rows'])) {
  339. $sqlQuery .= ' MAX_ROWS = ' . $partition['max_rows'];
  340. }
  341. if (! empty($partition['min_rows'])) {
  342. $sqlQuery .= ' MIN_ROWS = ' . $partition['min_rows'];
  343. }
  344. if (! empty($partition['tablespace'])) {
  345. $sqlQuery .= ' TABLESPACE = ' . $partition['tablespace'];
  346. }
  347. if (! empty($partition['node_group'])) {
  348. $sqlQuery .= ' NODEGROUP = ' . $partition['node_group'];
  349. }
  350. if (! empty($partition['subpartitions'])) {
  351. $subpartitions = [];
  352. foreach ($partition['subpartitions'] as $subpartition) {
  353. $subpartitions[] = $this->getPartitionDefinition($subpartition, true);
  354. }
  355. $sqlQuery .= ' (' . implode(', ', $subpartitions) . ')';
  356. }
  357. return $sqlQuery;
  358. }
  359. /**
  360. * Function to get table creation sql query
  361. *
  362. * @param string $db database name
  363. * @param string $table table name
  364. */
  365. public function getTableCreationQuery(string $db, string $table): string
  366. {
  367. // get column addition statements
  368. $sqlStatement = $this->getColumnCreationStatements(true);
  369. // Builds the 'create table' statement
  370. $sqlQuery = 'CREATE TABLE ' . Util::backquote($db) . '.'
  371. . Util::backquote(trim($table)) . ' (' . $sqlStatement . ')';
  372. // Adds table type, character set, comments and partition definition
  373. if (! empty($_POST['tbl_storage_engine']) && ($_POST['tbl_storage_engine'] !== 'Default')) {
  374. $sqlQuery .= ' ENGINE = ' . $this->dbi->escapeString($_POST['tbl_storage_engine']);
  375. }
  376. if (! empty($_POST['tbl_collation'])) {
  377. $sqlQuery .= Util::getCharsetQueryPart($_POST['tbl_collation'] ?? '');
  378. }
  379. if (
  380. ! empty($_POST['connection'])
  381. && ! empty($_POST['tbl_storage_engine'])
  382. && $_POST['tbl_storage_engine'] === 'FEDERATED'
  383. ) {
  384. $sqlQuery .= " CONNECTION = '"
  385. . $this->dbi->escapeString($_POST['connection']) . "'";
  386. }
  387. if (! empty($_POST['comment'])) {
  388. $sqlQuery .= ' COMMENT = \''
  389. . $this->dbi->escapeString($_POST['comment']) . '\'';
  390. }
  391. $sqlQuery .= $this->getPartitionsDefinition();
  392. $sqlQuery .= ';';
  393. return $sqlQuery;
  394. }
  395. /**
  396. * Function to get the number of fields for the table creation form
  397. */
  398. public function getNumberOfFieldsFromRequest(): int
  399. {
  400. // Limit to 4096 fields (MySQL maximal value)
  401. $mysqlLimit = 4096;
  402. if (isset($_POST['submit_num_fields'])) { // adding new fields
  403. $numberOfFields = intval($_POST['orig_num_fields']) + intval($_POST['added_fields']);
  404. } elseif (isset($_POST['orig_num_fields'])) { // retaining existing fields
  405. $numberOfFields = intval($_POST['orig_num_fields']);
  406. } elseif (
  407. isset($_POST['num_fields'])
  408. && intval($_POST['num_fields']) > 0
  409. ) { // new table with specified number of fields
  410. $numberOfFields = intval($_POST['num_fields']);
  411. } else { // new table with unspecified number of fields
  412. $numberOfFields = 4;
  413. }
  414. return min($numberOfFields, $mysqlLimit);
  415. }
  416. /**
  417. * Function to get the column creation statement
  418. *
  419. * @param string $table current table
  420. */
  421. public function getColumnCreationQuery(
  422. string $table
  423. ): string {
  424. // get column addition statements
  425. $sqlStatement = $this->getColumnCreationStatements(false);
  426. $sqlQuery = 'ALTER TABLE ' .
  427. Util::backquote($table) . ' ' . $sqlStatement;
  428. if (isset($_POST['online_transaction'])) {
  429. $sqlQuery .= ', ALGORITHM=INPLACE, LOCK=NONE';
  430. }
  431. return $sqlQuery . ';';
  432. }
  433. /**
  434. * Function to execute the column creation statement
  435. *
  436. * @param string $db current database
  437. * @param string $sqlQuery the query to run
  438. * @param string $errorUrl error page url
  439. *
  440. * @return array
  441. */
  442. public function tryColumnCreationQuery(
  443. string $db,
  444. string $sqlQuery,
  445. string $errorUrl
  446. ): array {
  447. // To allow replication, we first select the db to use and then run queries
  448. // on this db.
  449. if (! $this->dbi->selectDb($db)) {
  450. Generator::mysqlDie(
  451. $this->dbi->getError(),
  452. 'USE ' . Util::backquote($db),
  453. false,
  454. $errorUrl
  455. );
  456. }
  457. return [
  458. $this->dbi->tryQuery($sqlQuery),
  459. $sqlQuery,
  460. ];
  461. }
  462. }