Translated using Weblate (Vietnamese)
[phpmyadmin.git] / src / Operations.php
blob8d940be77b66db3d95bc02f1d69ec1113bc36bba
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use PhpMyAdmin\ConfigStorage\Relation;
8 use PhpMyAdmin\Database\Events;
9 use PhpMyAdmin\Database\Routines;
10 use PhpMyAdmin\Engines\Innodb;
11 use PhpMyAdmin\Identifiers\DatabaseName;
12 use PhpMyAdmin\Partitioning\Partition;
13 use PhpMyAdmin\Plugins\Export\ExportSql;
14 use PhpMyAdmin\Table\MoveMode;
15 use PhpMyAdmin\Table\MoveScope;
16 use PhpMyAdmin\Table\Table;
17 use PhpMyAdmin\Table\TableMover;
18 use PhpMyAdmin\Triggers\Triggers;
20 use function __;
21 use function array_merge;
22 use function count;
23 use function explode;
24 use function in_array;
25 use function is_scalar;
26 use function is_string;
27 use function mb_strtolower;
28 use function str_replace;
29 use function strtolower;
30 use function urldecode;
32 /**
33 * Set of functions with the operations section in phpMyAdmin
35 class Operations
37 public function __construct(
38 private readonly DatabaseInterface $dbi,
39 private readonly Relation $relation,
40 private readonly TableMover $tableMover,
41 ) {
44 /**
45 * Run the Procedure definitions and function definitions
47 * to avoid selecting alternatively the current and new db
48 * we would need to modify the CREATE definitions to qualify
49 * the db name
51 * @param string $db database name
53 public function runProcedureAndFunctionDefinitions(string $db, DatabaseName $newDatabaseName): void
55 foreach (Routines::getProcedureNames($this->dbi, $db) as $procedureName) {
56 $this->dbi->selectDb($db);
57 $query = Routines::getProcedureDefinition($this->dbi, $db, $procedureName);
58 if ($query === null) {
59 continue;
62 // collect for later display
63 $GLOBALS['sql_query'] .= "\n" . $query;
64 $this->dbi->selectDb($newDatabaseName);
65 $this->dbi->query($query);
68 foreach (Routines::getFunctionNames($this->dbi, $db) as $functionName) {
69 $this->dbi->selectDb($db);
70 $query = Routines::getFunctionDefinition($this->dbi, $db, $functionName);
71 if ($query === null) {
72 continue;
75 // collect for later display
76 $GLOBALS['sql_query'] .= "\n" . $query;
77 $this->dbi->selectDb($newDatabaseName);
78 $this->dbi->query($query);
82 /**
83 * Create database before copy
85 public function createDbBeforeCopy(UserPrivileges $userPrivileges, DatabaseName $newDatabaseName): void
87 $localQuery = 'CREATE DATABASE IF NOT EXISTS '
88 . Util::backquote($newDatabaseName);
89 if (isset($_POST['db_collation'])) {
90 $localQuery .= ' DEFAULT'
91 . Util::getCharsetQueryPart($_POST['db_collation']);
94 $localQuery .= ';';
95 $GLOBALS['sql_query'] .= $localQuery;
97 // save the original db name because Tracker.php which
98 // may be called under $this->dbi->query() changes \PhpMyAdmin\Current::$database
99 // for some statements, one of which being CREATE DATABASE
100 $originalDb = Current::$database;
101 $this->dbi->query($localQuery);
102 Current::$database = $originalDb;
104 // Set the SQL mode to NO_AUTO_VALUE_ON_ZERO to prevent MySQL from creating
105 // export statements it cannot import
106 $sqlSetMode = "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO'";
107 $this->dbi->query($sqlSetMode);
109 // rebuild the database list because Table::moveCopy
110 // checks in this list if the target db exists
111 $this->dbi->getDatabaseList()->build($userPrivileges);
115 * Get views as an array and create SQL view stand-in
117 * @param string[] $tables array of all tables in given db or dbs
118 * @param ExportSql $exportSqlPlugin export plugin instance
119 * @param string $db database name
121 * @return string[]
123 public function getViewsAndCreateSqlViewStandIn(
124 array $tables,
125 ExportSql $exportSqlPlugin,
126 string $db,
127 DatabaseName $newDatabaseName,
128 ): array {
129 $views = [];
130 foreach ($tables as $table) {
131 // to be able to rename a db containing views,
132 // first all the views are collected and a stand-in is created
133 // the real views are created after the tables
134 if (! $this->dbi->getTable($db, $table)->isView()) {
135 continue;
138 // If view exists, and 'add drop view' is selected: Drop it!
139 if ($_POST['what'] !== 'nocopy' && isset($_POST['drop_if_exists']) && $_POST['drop_if_exists'] === 'true') {
140 $dropQuery = 'DROP VIEW IF EXISTS '
141 . Util::backquote($newDatabaseName) . '.'
142 . Util::backquote($table);
143 $this->dbi->query($dropQuery);
145 $GLOBALS['sql_query'] .= "\n" . $dropQuery . ';';
148 $views[] = $table;
149 // Create stand-in definition to resolve view dependencies
150 $sqlViewStandin = $exportSqlPlugin->getTableDefStandIn($db, $table);
151 $this->dbi->selectDb($newDatabaseName);
152 $this->dbi->query($sqlViewStandin);
153 $GLOBALS['sql_query'] .= "\n" . $sqlViewStandin;
156 return $views;
160 * Get sql query for copy/rename table and boolean for whether copy/rename or not
162 * @param string[] $tables array of all tables in given db or dbs
163 * @param bool $move whether database name is empty or not
164 * @param string $db database name
166 * @return mixed[] SQL queries for the constraints
168 public function copyTables(array $tables, bool $move, string $db, DatabaseName $newDatabaseName): array
170 $sqlContraints = [];
171 foreach ($tables as $table) {
172 // skip the views; we have created stand-in definitions
173 if ($this->dbi->getTable($db, $table)->isView()) {
174 continue;
177 // value of $what for this table only
178 $copyMode = $_POST['what'];
180 // do not copy the data from a Merge table
181 // note: on the calling FORM, 'data' means 'structure and data'
182 if ($this->dbi->getTable($db, $table)->isMerge()) {
183 if ($copyMode === 'data') {
184 $copyMode = 'structure';
187 if ($copyMode === 'dataonly') {
188 $copyMode = 'nocopy';
192 if ($copyMode === 'nocopy') {
193 continue;
196 // keep the triggers from the original db+table
197 // (third param is empty because delimiters are only intended
198 // for importing via the mysql client or our Import feature)
199 $triggers = Triggers::getDetails($this->dbi, $db, $table);
201 $moveScope = MoveScope::tryFrom($copyMode) ?? MoveScope::StructureAndData;
202 if (
203 ! $this->tableMover->moveCopy(
204 $db,
205 $table,
206 $newDatabaseName->getName(),
207 $table,
208 $move ? MoveScope::Move : $moveScope,
209 MoveMode::WholeDatabase,
210 isset($_POST['drop_if_exists']) && $_POST['drop_if_exists'] === 'true',
213 $GLOBALS['_error'] = true;
214 break;
217 // apply the triggers to the destination db+table
218 if ($triggers !== []) {
219 $this->dbi->selectDb($newDatabaseName);
220 foreach ($triggers as $trigger) {
221 $createSqlQuery = $trigger->getCreateSql('');
222 $this->dbi->query($createSqlQuery);
223 $GLOBALS['sql_query'] .= "\n" . $createSqlQuery . ';';
227 // this does not apply to a rename operation
228 if (! isset($_POST['add_constraints']) || $this->tableMover->sqlConstraintsQuery === '') {
229 continue;
232 $sqlContraints[] = $this->tableMover->sqlConstraintsQuery;
233 $this->tableMover->sqlConstraintsQuery = '';
236 return $sqlContraints;
240 * Run the EVENT definition for selected database
242 * to avoid selecting alternatively the current and new db
243 * we would need to modify the CREATE definitions to qualify
244 * the db name
246 * @param string $db database name
248 public function runEventDefinitionsForDb(string $db, DatabaseName $newDatabaseName): void
250 /** @var string[] $eventNames */
251 $eventNames = $this->dbi->fetchResult(
252 'SELECT EVENT_NAME FROM information_schema.EVENTS WHERE EVENT_SCHEMA= '
253 . $this->dbi->quoteString($db) . ';',
256 foreach ($eventNames as $eventName) {
257 $this->dbi->selectDb($db);
258 $query = Events::getDefinition($this->dbi, $db, $eventName);
259 // collect for later display
260 $GLOBALS['sql_query'] .= "\n" . $query;
261 $this->dbi->selectDb($newDatabaseName);
262 $this->dbi->query($query);
267 * Handle the views, return the boolean value whether table rename/copy or not
269 * @param string[] $views views as an array
270 * @param bool $move whether database name is empty or not
271 * @param string $db database name
273 public function handleTheViews(array $views, bool $move, string $db, DatabaseName $newDatabaseName): void
275 // Add DROP IF EXIST to CREATE VIEW query, to remove stand-in VIEW that was created earlier.
276 foreach ($views as $view) {
277 $copyingSucceeded = $this->tableMover->moveCopy(
278 $db,
279 $view,
280 $newDatabaseName->getName(),
281 $view,
282 $move ? MoveScope::Move : MoveScope::StructureOnly,
283 MoveMode::WholeDatabase,
284 true,
286 if (! $copyingSucceeded) {
287 $GLOBALS['_error'] = true;
288 break;
294 * Adjust the privileges after Renaming the db
296 * @param string $oldDb Database name before renaming
298 public function adjustPrivilegesMoveDb(
299 UserPrivileges $userPrivileges,
300 string $oldDb,
301 DatabaseName $newDatabaseName,
302 ): void {
303 if (
304 ! $userPrivileges->database || ! $userPrivileges->table
305 || ! $userPrivileges->column || ! $userPrivileges->routines
306 || ! $userPrivileges->isReload
308 return;
311 $this->dbi->selectDb('mysql');
312 $newName = str_replace('_', '\_', $newDatabaseName->getName());
313 $oldDb = str_replace('_', '\_', $oldDb);
315 // For Db specific privileges
316 $this->dbi->query('UPDATE ' . Util::backquote('db')
317 . 'SET Db = ' . $this->dbi->quoteString($newName)
318 . ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
320 // For table specific privileges
321 $this->dbi->query('UPDATE ' . Util::backquote('tables_priv')
322 . 'SET Db = ' . $this->dbi->quoteString($newName)
323 . ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
325 // For column specific privileges
326 $this->dbi->query('UPDATE ' . Util::backquote('columns_priv')
327 . 'SET Db = ' . $this->dbi->quoteString($newName)
328 . ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
330 // For procedures specific privileges
331 $this->dbi->query('UPDATE ' . Util::backquote('procs_priv')
332 . 'SET Db = ' . $this->dbi->quoteString($newName)
333 . ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
335 // Finally FLUSH the new privileges
336 $this->dbi->tryQuery('FLUSH PRIVILEGES;');
340 * Adjust the privileges after Copying the db
342 * @param string $oldDb Database name before copying
344 public function adjustPrivilegesCopyDb(
345 UserPrivileges $userPrivileges,
346 string $oldDb,
347 DatabaseName $newDatabaseName,
348 ): void {
349 if (
350 ! $userPrivileges->database || ! $userPrivileges->table
351 || ! $userPrivileges->column || ! $userPrivileges->routines
352 || ! $userPrivileges->isReload
354 return;
357 $this->dbi->selectDb('mysql');
358 $newName = str_replace('_', '\_', $newDatabaseName->getName());
359 $oldDb = str_replace('_', '\_', $oldDb);
361 $queryDbSpecificOld = 'SELECT * FROM '
362 . Util::backquote('db') . ' WHERE '
363 . 'Db = "' . $oldDb . '";';
365 $oldPrivsDb = $this->dbi->fetchResult($queryDbSpecificOld, 0);
367 foreach ($oldPrivsDb as $oldPriv) {
368 $newDbDbPrivsQuery = 'INSERT INTO ' . Util::backquote('db')
369 . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '"';
370 $privCount = count($oldPriv);
371 for ($i = 2; $i < $privCount; $i++) {
372 $newDbDbPrivsQuery .= ', "' . $oldPriv[$i] . '"';
375 $newDbDbPrivsQuery .= ')';
377 $this->dbi->query($newDbDbPrivsQuery);
380 // For Table Specific privileges
381 $queryTableSpecificOld = 'SELECT * FROM '
382 . Util::backquote('tables_priv') . ' WHERE '
383 . 'Db = "' . $oldDb . '";';
385 $oldPrivsTable = $this->dbi->fetchResult($queryTableSpecificOld, 0);
387 foreach ($oldPrivsTable as $oldPriv) {
388 $newDbTablePrivsQuery = 'INSERT INTO ' . Util::backquote(
389 'tables_priv',
390 ) . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '", "'
391 . $oldPriv[2] . '", "' . $oldPriv[3] . '", "' . $oldPriv[4]
392 . '", "' . $oldPriv[5] . '", "' . $oldPriv[6] . '", "'
393 . $oldPriv[7] . '");';
395 $this->dbi->query($newDbTablePrivsQuery);
398 // For Column Specific privileges
399 $queryColSpecificOld = 'SELECT * FROM '
400 . Util::backquote('columns_priv') . ' WHERE '
401 . 'Db = "' . $oldDb . '";';
403 $oldPrivsCol = $this->dbi->fetchResult($queryColSpecificOld, 0);
405 foreach ($oldPrivsCol as $oldPriv) {
406 $newDbColPrivsQuery = 'INSERT INTO ' . Util::backquote(
407 'columns_priv',
408 ) . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '", "'
409 . $oldPriv[2] . '", "' . $oldPriv[3] . '", "' . $oldPriv[4]
410 . '", "' . $oldPriv[5] . '", "' . $oldPriv[6] . '");';
412 $this->dbi->query($newDbColPrivsQuery);
415 // For Procedure Specific privileges
416 $queryProcSpecificOld = 'SELECT * FROM '
417 . Util::backquote('procs_priv') . ' WHERE '
418 . 'Db = "' . $oldDb . '";';
420 $oldPrivsProc = $this->dbi->fetchResult($queryProcSpecificOld, 0);
422 foreach ($oldPrivsProc as $oldPriv) {
423 $newDbProcPrivsQuery = 'INSERT INTO ' . Util::backquote(
424 'procs_priv',
425 ) . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '", "'
426 . $oldPriv[2] . '", "' . $oldPriv[3] . '", "' . $oldPriv[4]
427 . '", "' . $oldPriv[5] . '", "' . $oldPriv[6] . '", "'
428 . $oldPriv[7] . '");';
430 $this->dbi->query($newDbProcPrivsQuery);
433 // Finally FLUSH the new privileges
434 $this->dbi->tryQuery('FLUSH PRIVILEGES;');
438 * Create all accumulated constraints
440 * @param mixed[] $sqlConstraints array of sql constraints for the database
442 public function createAllAccumulatedConstraints(array $sqlConstraints, DatabaseName $newDatabaseName): void
444 $this->dbi->selectDb($newDatabaseName);
445 foreach ($sqlConstraints as $query) {
446 $this->dbi->query($query);
447 // and prepare to display them
448 $GLOBALS['sql_query'] .= "\n" . $query;
453 * Duplicate the bookmarks for the db (done once for each db)
455 * @param bool $error whether table rename/copy or not
456 * @param string $db database name
458 public function duplicateBookmarks(bool $error, string $db, DatabaseName $newDatabaseName): void
460 if ($error || $db === $newDatabaseName->getName()) {
461 return;
464 $getFields = ['user', 'label', 'query'];
465 $whereFields = ['dbase' => $db];
466 $newFields = ['dbase' => $newDatabaseName->getName()];
467 $this->tableMover->duplicateInfo('bookmarkwork', 'bookmark', $getFields, $whereFields, $newFields);
471 * Get array of possible row formats
473 * @return string[][]
475 public function getPossibleRowFormat(): array
477 // the outer array is for engines, the inner array contains the dropdown
478 // option values as keys then the dropdown option labels
480 $possibleRowFormats = [
481 'ARCHIVE' => ['COMPRESSED' => 'COMPRESSED'],
482 'ARIA' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC', 'PAGE' => 'PAGE'],
483 'MARIA' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC', 'PAGE' => 'PAGE'],
484 'MYISAM' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC'],
485 'PBXT' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC'],
486 'INNODB' => ['COMPACT' => 'COMPACT', 'REDUNDANT' => 'REDUNDANT'],
489 /** @var Innodb $innodbEnginePlugin */
490 $innodbEnginePlugin = StorageEngine::getEngine('Innodb');
491 $innodbPluginVersion = $innodbEnginePlugin->getInnodbPluginVersion();
492 $innodbFileFormat = '';
493 if ($innodbPluginVersion !== '') {
494 $innodbFileFormat = $innodbEnginePlugin->getInnodbFileFormat() ?? '';
498 * Newer MySQL/MariaDB always return empty a.k.a '' on $innodbFileFormat otherwise
499 * old versions of MySQL/MariaDB must be returning something or not empty.
500 * This patch is to support newer MySQL/MariaDB while also for backward compatibilities.
502 if (
503 (strtolower($innodbFileFormat) === 'barracuda') || ($innodbFileFormat == '')
504 && $innodbEnginePlugin->supportsFilePerTable()
506 $possibleRowFormats['INNODB']['DYNAMIC'] = 'DYNAMIC';
507 $possibleRowFormats['INNODB']['COMPRESSED'] = 'COMPRESSED';
510 return $possibleRowFormats;
513 /** @return array<string, string> */
514 public function getPartitionMaintenanceChoices(): array
516 $choices = [
517 'ANALYZE' => __('Analyze'),
518 'CHECK' => __('Check'),
519 'OPTIMIZE' => __('Optimize'),
520 'REBUILD' => __('Rebuild'),
521 'REPAIR' => __('Repair'),
522 'TRUNCATE' => __('Truncate'),
525 $partitionMethod = Partition::getPartitionMethod(Current::$database, Current::$table);
527 // add COALESCE or DROP option to choices array depending on Partition method
528 if (
529 $partitionMethod === 'RANGE'
530 || $partitionMethod === 'RANGE COLUMNS'
531 || $partitionMethod === 'LIST'
532 || $partitionMethod === 'LIST COLUMNS'
534 $choices['DROP'] = __('Drop');
535 } else {
536 $choices['COALESCE'] = __('Coalesce');
539 return $choices;
543 * @param mixed[] $urlParams Array of url parameters.
544 * @param bool $hasRelationFeature If relation feature is enabled.
546 * @return mixed[]
548 public function getForeignersForReferentialIntegrityCheck(
549 array $urlParams,
550 bool $hasRelationFeature,
551 ): array {
552 if (! $hasRelationFeature) {
553 return [];
556 $foreigners = [];
557 $this->dbi->selectDb(Current::$database);
558 $foreign = $this->relation->getForeignersInternal(Current::$database, Current::$table);
560 foreach ($foreign as $master => $arr) {
561 $joinQuery = 'SELECT '
562 . Util::backquote(Current::$table) . '.*'
563 . ' FROM ' . Util::backquote(Current::$table)
564 . ' LEFT JOIN '
565 . Util::backquote($arr['foreign_db'])
566 . '.'
567 . Util::backquote($arr['foreign_table']);
569 if ($arr['foreign_table'] == Current::$table) {
570 $foreignTable = Current::$table . '1';
571 $joinQuery .= ' AS ' . Util::backquote($foreignTable);
572 } else {
573 $foreignTable = $arr['foreign_table'];
576 $joinQuery .= ' ON '
577 . Util::backquote(Current::$table) . '.'
578 . Util::backquote($master)
579 . ' = '
580 . Util::backquote($arr['foreign_db'])
581 . '.'
582 . Util::backquote($foreignTable) . '.'
583 . Util::backquote($arr['foreign_field'])
584 . ' WHERE '
585 . Util::backquote($arr['foreign_db'])
586 . '.'
587 . Util::backquote($foreignTable) . '.'
588 . Util::backquote($arr['foreign_field'])
589 . ' IS NULL AND '
590 . Util::backquote(Current::$table) . '.'
591 . Util::backquote($master)
592 . ' IS NOT NULL';
593 $thisUrlParams = array_merge(
594 $urlParams,
595 ['sql_query' => $joinQuery, 'sql_signature' => Core::signSqlQuery($joinQuery)],
598 $foreigners[] = [
599 'params' => $thisUrlParams,
600 'master' => $master,
601 'db' => $arr['foreign_db'],
602 'table' => $arr['foreign_table'],
603 'field' => $arr['foreign_field'],
607 return $foreigners;
611 * Get table alters array
613 * @param Table $pmaTable The Table object
614 * @param string $packKeys pack keys
615 * @param string $checksum value of checksum
616 * @param string $pageChecksum value of page checksum
617 * @param string $delayKeyWrite delay key write
618 * @param string $rowFormat row format
619 * @param string $newTblStorageEngine table storage engine
620 * @param string $transactional value of transactional
621 * @param string $tableCollation collation of the table
623 * @return string[]
625 public function getTableAltersArray(
626 Table $pmaTable,
627 string $packKeys,
628 string $checksum,
629 string $pageChecksum,
630 string $delayKeyWrite,
631 string $rowFormat,
632 string $newTblStorageEngine,
633 string $transactional,
634 string $tableCollation,
635 string $tableStorageEngine,
636 ): array {
637 $GLOBALS['auto_increment'] ??= null;
639 $tableAlters = [];
641 if (isset($_POST['comment']) && urldecode($_POST['prev_comment']) !== $_POST['comment']) {
642 $tableAlters[] = 'COMMENT = ' . $this->dbi->quoteString($_POST['comment']);
645 if (
646 $newTblStorageEngine !== ''
647 && mb_strtolower($newTblStorageEngine) !== mb_strtolower($tableStorageEngine)
649 $tableAlters[] = 'ENGINE = ' . $newTblStorageEngine;
652 if (! empty($_POST['tbl_collation']) && $_POST['tbl_collation'] !== $tableCollation) {
653 $tableAlters[] = 'DEFAULT '
654 . Util::getCharsetQueryPart($_POST['tbl_collation']);
657 if (
658 $pmaTable->isEngine(['MYISAM', 'ARIA', 'ISAM'])
659 && isset($_POST['new_pack_keys'])
660 && $_POST['new_pack_keys'] != $packKeys
662 $tableAlters[] = 'pack_keys = ' . $_POST['new_pack_keys'];
665 $newChecksum = empty($_POST['new_checksum']) ? '0' : '1';
666 if ($pmaTable->isEngine(['MYISAM', 'ARIA']) && $newChecksum !== $checksum) {
667 $tableAlters[] = 'checksum = ' . $newChecksum;
670 $newTransactional = empty($_POST['new_transactional']) ? '0' : '1';
671 if ($pmaTable->isEngine('ARIA') && $newTransactional !== $transactional) {
672 $tableAlters[] = 'TRANSACTIONAL = ' . $newTransactional;
675 $newPageChecksum = empty($_POST['new_page_checksum']) ? '0' : '1';
676 if ($pmaTable->isEngine('ARIA') && $newPageChecksum !== $pageChecksum) {
677 $tableAlters[] = 'PAGE_CHECKSUM = ' . $newPageChecksum;
680 $newDelayKeyWrite = empty($_POST['new_delay_key_write']) ? '0' : '1';
681 if ($pmaTable->isEngine(['MYISAM', 'ARIA']) && $newDelayKeyWrite !== $delayKeyWrite) {
682 $tableAlters[] = 'delay_key_write = ' . $newDelayKeyWrite;
685 if (
686 $pmaTable->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT', 'ROCKSDB'])
687 && ! empty($_POST['new_auto_increment'])
688 && (! isset($GLOBALS['auto_increment'])
689 || $_POST['new_auto_increment'] !== $GLOBALS['auto_increment'])
690 && $_POST['new_auto_increment'] !== $_POST['hidden_auto_increment']
692 $tableAlters[] = 'auto_increment = ' . (int) $_POST['new_auto_increment'];
695 if (! empty($_POST['new_row_format'])) {
696 $newRowFormat = $_POST['new_row_format'];
697 $newRowFormatLower = mb_strtolower($newRowFormat);
698 if (
699 $pmaTable->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT'])
700 && ($rowFormat === '' || $newRowFormatLower !== mb_strtolower($rowFormat))
701 && in_array($newRowFormat, ['DEFAULT', 'DYNAMIC', 'FIXED', 'COMPRESSED', 'REDUNDANT', 'COMPACT'], true)
703 $tableAlters[] = 'ROW_FORMAT = ' . $newRowFormat;
707 return $tableAlters;
711 * Get warning messages array
713 * @return string[]
715 public function getWarningMessagesArray(mixed $newTableStorageEngine): array
717 $warningMessages = [];
718 foreach ($this->dbi->getWarnings() as $warning) {
719 // In MariaDB 5.1.44, when altering a table from Maria to MyISAM
720 // and if TRANSACTIONAL was set, the system reports an error;
721 // I discussed with a Maria developer and he agrees that this
722 // should not be reported with a Level of Error, so here
723 // I just ignore it. But there are other 1478 messages
724 // that it's better to show.
725 if (
726 $newTableStorageEngine === 'MyISAM'
727 && $warning->code === 1478
728 && $warning->level === 'Error'
730 continue;
733 $warningMessages[] = (string) $warning;
736 return $warningMessages;
740 * Adjust the privileges after renaming/moving a table
742 * @param string $oldDb Database name before table renaming/moving table
743 * @param string $oldTable Table name before table renaming/moving table
744 * @param string $newDb Database name after table renaming/ moving table
745 * @param string $newTable Table name after table renaming/moving table
747 public function adjustPrivilegesRenameOrMoveTable(
748 UserPrivileges $userPrivileges,
749 string $oldDb,
750 string $oldTable,
751 string $newDb,
752 string $newTable,
753 ): void {
754 if (! $userPrivileges->table || ! $userPrivileges->column || ! $userPrivileges->isReload) {
755 return;
758 $this->dbi->selectDb('mysql');
760 // For table specific privileges
761 $this->dbi->query('UPDATE ' . Util::backquote('tables_priv')
762 . 'SET Db = ' . $this->dbi->quoteString($newDb)
763 . ', Table_name = ' . $this->dbi->quoteString($newTable)
764 . ' where Db = ' . $this->dbi->quoteString($oldDb)
765 . ' AND Table_name = ' . $this->dbi->quoteString($oldTable)
766 . ';');
768 // For column specific privileges
769 $this->dbi->query('UPDATE ' . Util::backquote('columns_priv')
770 . 'SET Db = ' . $this->dbi->quoteString($newDb)
771 . ', Table_name = ' . $this->dbi->quoteString($newTable)
772 . ' where Db = ' . $this->dbi->quoteString($oldDb)
773 . ' AND Table_name = ' . $this->dbi->quoteString($oldTable)
774 . ';');
776 // Finally FLUSH the new privileges
777 $this->dbi->tryQuery('FLUSH PRIVILEGES;');
781 * Adjust the privileges after copying a table
783 * @param string $oldDb Database name before table copying
784 * @param string $oldTable Table name before table copying
785 * @param string $newDb Database name after table copying
786 * @param string $newTable Table name after table copying
788 public function adjustPrivilegesCopyTable(
789 UserPrivileges $userPrivileges,
790 string $oldDb,
791 string $oldTable,
792 string $newDb,
793 string $newTable,
794 ): void {
795 if (! $userPrivileges->table || ! $userPrivileges->column || ! $userPrivileges->isReload) {
796 return;
799 $this->dbi->selectDb('mysql');
801 // For Table Specific privileges
802 $queryTableSpecificOld = 'SELECT * FROM '
803 . Util::backquote('tables_priv') . ' where '
804 . 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";';
806 $oldPrivsTable = $this->dbi->fetchResult($queryTableSpecificOld, 0);
808 foreach ($oldPrivsTable as $oldPriv) {
809 $newDbTablePrivsQuery = 'INSERT INTO '
810 . Util::backquote('tables_priv') . ' VALUES("'
811 . $oldPriv[0] . '", "' . $newDb . '", "' . $oldPriv[2] . '", "'
812 . $newTable . '", "' . $oldPriv[4] . '", "' . $oldPriv[5]
813 . '", "' . $oldPriv[6] . '", "' . $oldPriv[7] . '");';
815 $this->dbi->query($newDbTablePrivsQuery);
818 // For Column Specific privileges
819 $queryColSpecificOld = 'SELECT * FROM '
820 . Util::backquote('columns_priv') . ' WHERE '
821 . 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";';
823 $oldPrivsCol = $this->dbi->fetchResult($queryColSpecificOld, 0);
825 foreach ($oldPrivsCol as $oldPriv) {
826 $newDbColPrivsQuery = 'INSERT INTO '
827 . Util::backquote('columns_priv') . ' VALUES("'
828 . $oldPriv[0] . '", "' . $newDb . '", "' . $oldPriv[2] . '", "'
829 . $newTable . '", "' . $oldPriv[4] . '", "' . $oldPriv[5]
830 . '", "' . $oldPriv[6] . '");';
832 $this->dbi->query($newDbColPrivsQuery);
835 // Finally FLUSH the new privileges
836 $this->dbi->tryQuery('FLUSH PRIVILEGES;');
840 * Change all collations and character sets of all columns in table
842 * @param string $db Database name
843 * @param string $table Table name
844 * @param string $tableCollation Collation Name
846 public function changeAllColumnsCollation(string $db, string $table, string $tableCollation): void
848 $this->dbi->selectDb($db);
850 $changeAllCollationsQuery = 'ALTER TABLE '
851 . Util::backquote($table)
852 . ' CONVERT TO';
854 [$charset] = explode('_', $tableCollation);
856 $changeAllCollationsQuery .= ' CHARACTER SET ' . $charset
857 . ($charset === $tableCollation ? '' : ' COLLATE ' . $tableCollation);
859 $this->dbi->query($changeAllCollationsQuery);
863 * Move or copy a table
865 * @param string $db current database name
866 * @param string $table current table name
868 public function moveOrCopyTable(UserPrivileges $userPrivileges, string $db, string $table): Message
871 * Selects the database to work with
873 $this->dbi->selectDb($db);
876 * $_POST['target_db'] could be empty in case we came from an input field
877 * (when there are many databases, no drop-down)
879 $targetDb = $db;
880 if (isset($_POST['target_db']) && is_string($_POST['target_db']) && $_POST['target_db'] !== '') {
881 $targetDb = $_POST['target_db'];
885 * A target table name has been sent to this script -> do the work
887 if (isset($_POST['new_name']) && is_scalar($_POST['new_name']) && (string) $_POST['new_name'] !== '') {
888 if ($db === $targetDb && $table == $_POST['new_name']) {
889 if (isset($_POST['submit_move'])) {
890 $message = Message::error(__('Can\'t move table to same one!'));
891 } else {
892 $message = Message::error(__('Can\'t copy table to same one!'));
894 } else {
895 $move = isset($_POST['submit_move']);
896 $this->tableMover->moveCopy(
897 $db,
898 $table,
899 $targetDb,
900 (string) $_POST['new_name'],
901 $move ? MoveScope::Move : MoveScope::from($_POST['what']),
902 MoveMode::SingleTable,
903 isset($_POST['drop_if_exists']) && $_POST['drop_if_exists'] === 'true',
906 if (! empty($_POST['adjust_privileges'])) {
907 if (isset($_POST['submit_move'])) {
908 $this->adjustPrivilegesRenameOrMoveTable(
909 $userPrivileges,
910 $db,
911 $table,
912 $targetDb,
913 (string) $_POST['new_name'],
915 } else {
916 $this->adjustPrivilegesCopyTable(
917 $userPrivileges,
918 $db,
919 $table,
920 $targetDb,
921 (string) $_POST['new_name'],
925 if (isset($_POST['submit_move'])) {
926 $message = Message::success(
928 'Table %s has been moved to %s. Privileges have been adjusted.',
931 } else {
932 $message = Message::success(
934 'Table %s has been copied to %s. Privileges have been adjusted.',
938 } elseif (isset($_POST['submit_move'])) {
939 $message = Message::success(
940 __('Table %s has been moved to %s.'),
942 } else {
943 $message = Message::success(
944 __('Table %s has been copied to %s.'),
948 $old = Util::backquote($db) . '.'
949 . Util::backquote($table);
950 $message->addParam($old);
952 $newName = (string) $_POST['new_name'];
953 if ($this->dbi->getLowerCaseNames() === 1) {
954 $newName = strtolower($newName);
957 Current::$table = $newName;
959 $new = Util::backquote($targetDb) . '.'
960 . Util::backquote($newName);
961 $message->addParam($new);
963 } else {
965 * No new name for the table!
967 $message = Message::error(__('The table name is empty!'));
970 return $message;