Translated using Weblate (Russian)
[phpmyadmin.git] / src / Operations.php
blob6a63201a537512e9e386d6664adfc7884d812fba
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\Table;
15 use PhpMyAdmin\Triggers\Triggers;
17 use function __;
18 use function array_merge;
19 use function count;
20 use function explode;
21 use function is_scalar;
22 use function is_string;
23 use function mb_strtolower;
24 use function str_replace;
25 use function strtolower;
26 use function urldecode;
28 /**
29 * Set of functions with the operations section in phpMyAdmin
31 class Operations
33 public function __construct(private DatabaseInterface $dbi, private Relation $relation)
37 /**
38 * Run the Procedure definitions and function definitions
40 * to avoid selecting alternatively the current and new db
41 * we would need to modify the CREATE definitions to qualify
42 * the db name
44 * @param string $db database name
46 public function runProcedureAndFunctionDefinitions(string $db, DatabaseName $newDatabaseName): void
48 foreach (Routines::getProcedureNames($this->dbi, $db) as $procedureName) {
49 $this->dbi->selectDb($db);
50 $query = Routines::getProcedureDefinition($this->dbi, $db, $procedureName);
51 if ($query === null) {
52 continue;
55 // collect for later display
56 $GLOBALS['sql_query'] .= "\n" . $query;
57 $this->dbi->selectDb($newDatabaseName);
58 $this->dbi->query($query);
61 foreach (Routines::getFunctionNames($this->dbi, $db) as $functionName) {
62 $this->dbi->selectDb($db);
63 $query = Routines::getFunctionDefinition($this->dbi, $db, $functionName);
64 if ($query === null) {
65 continue;
68 // collect for later display
69 $GLOBALS['sql_query'] .= "\n" . $query;
70 $this->dbi->selectDb($newDatabaseName);
71 $this->dbi->query($query);
75 /**
76 * Create database before copy
78 public function createDbBeforeCopy(DatabaseName $newDatabaseName): void
80 $localQuery = 'CREATE DATABASE IF NOT EXISTS '
81 . Util::backquote($newDatabaseName);
82 if (isset($_POST['db_collation'])) {
83 $localQuery .= ' DEFAULT'
84 . Util::getCharsetQueryPart($_POST['db_collation']);
87 $localQuery .= ';';
88 $GLOBALS['sql_query'] .= $localQuery;
90 // save the original db name because Tracker.php which
91 // may be called under $this->dbi->query() changes $GLOBALS['db']
92 // for some statements, one of which being CREATE DATABASE
93 $originalDb = $GLOBALS['db'];
94 $this->dbi->query($localQuery);
95 $GLOBALS['db'] = $originalDb;
97 // Set the SQL mode to NO_AUTO_VALUE_ON_ZERO to prevent MySQL from creating
98 // export statements it cannot import
99 $sqlSetMode = "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO'";
100 $this->dbi->query($sqlSetMode);
102 // rebuild the database list because Table::moveCopy
103 // checks in this list if the target db exists
104 $this->dbi->getDatabaseList()->build();
108 * Get views as an array and create SQL view stand-in
110 * @param string[] $tables array of all tables in given db or dbs
111 * @param ExportSql $exportSqlPlugin export plugin instance
112 * @param string $db database name
114 * @return mixed[]
116 public function getViewsAndCreateSqlViewStandIn(
117 array $tables,
118 ExportSql $exportSqlPlugin,
119 string $db,
120 DatabaseName $newDatabaseName,
121 ): array {
122 $views = [];
123 foreach ($tables as $table) {
124 // to be able to rename a db containing views,
125 // first all the views are collected and a stand-in is created
126 // the real views are created after the tables
127 if (! $this->dbi->getTable($db, $table)->isView()) {
128 continue;
131 // If view exists, and 'add drop view' is selected: Drop it!
132 if ($_POST['what'] !== 'nocopy' && isset($_POST['drop_if_exists']) && $_POST['drop_if_exists'] === 'true') {
133 $dropQuery = 'DROP VIEW IF EXISTS '
134 . Util::backquote($newDatabaseName) . '.'
135 . Util::backquote($table);
136 $this->dbi->query($dropQuery);
138 $GLOBALS['sql_query'] .= "\n" . $dropQuery . ';';
141 $views[] = $table;
142 // Create stand-in definition to resolve view dependencies
143 $sqlViewStandin = $exportSqlPlugin->getTableDefStandIn($db, $table);
144 $this->dbi->selectDb($newDatabaseName);
145 $this->dbi->query($sqlViewStandin);
146 $GLOBALS['sql_query'] .= "\n" . $sqlViewStandin;
149 return $views;
153 * Get sql query for copy/rename table and boolean for whether copy/rename or not
155 * @param string[] $tables array of all tables in given db or dbs
156 * @param bool $move whether database name is empty or not
157 * @param string $db database name
159 * @return mixed[] SQL queries for the constraints
161 public function copyTables(array $tables, bool $move, string $db, DatabaseName $newDatabaseName): array
163 $sqlContraints = [];
164 foreach ($tables as $table) {
165 // skip the views; we have created stand-in definitions
166 if ($this->dbi->getTable($db, $table)->isView()) {
167 continue;
170 // value of $what for this table only
171 $copyMode = $_POST['what'];
173 // do not copy the data from a Merge table
174 // note: on the calling FORM, 'data' means 'structure and data'
175 if ($this->dbi->getTable($db, $table)->isMerge()) {
176 if ($copyMode === 'data') {
177 $copyMode = 'structure';
180 if ($copyMode === 'dataonly') {
181 $copyMode = 'nocopy';
185 if ($copyMode === 'nocopy') {
186 continue;
189 // keep the triggers from the original db+table
190 // (third param is empty because delimiters are only intended
191 // for importing via the mysql client or our Import feature)
192 $triggers = Triggers::getDetails($this->dbi, $db, $table);
194 if (
195 ! Table::moveCopy(
196 $db,
197 $table,
198 $newDatabaseName->getName(),
199 $table,
200 ($copyMode ?? 'data'),
201 $move,
202 'db_copy',
203 isset($_POST['drop_if_exists']) && $_POST['drop_if_exists'] === 'true',
206 $GLOBALS['_error'] = true;
207 break;
210 // apply the triggers to the destination db+table
211 if ($triggers !== []) {
212 $this->dbi->selectDb($newDatabaseName);
213 foreach ($triggers as $trigger) {
214 $createSqlQuery = $trigger->getCreateSql('');
215 $this->dbi->query($createSqlQuery);
216 $GLOBALS['sql_query'] .= "\n" . $createSqlQuery . ';';
220 // this does not apply to a rename operation
221 if (! isset($_POST['add_constraints']) || empty($GLOBALS['sql_constraints_query'])) {
222 continue;
225 $sqlContraints[] = $GLOBALS['sql_constraints_query'];
226 unset($GLOBALS['sql_constraints_query']);
229 return $sqlContraints;
233 * Run the EVENT definition for selected database
235 * to avoid selecting alternatively the current and new db
236 * we would need to modify the CREATE definitions to qualify
237 * the db name
239 * @param string $db database name
241 public function runEventDefinitionsForDb(string $db, DatabaseName $newDatabaseName): void
243 /** @var string[] $eventNames */
244 $eventNames = $this->dbi->fetchResult(
245 'SELECT EVENT_NAME FROM information_schema.EVENTS WHERE EVENT_SCHEMA= '
246 . $this->dbi->quoteString($db) . ';',
249 foreach ($eventNames as $eventName) {
250 $this->dbi->selectDb($db);
251 $query = Events::getDefinition($this->dbi, $db, $eventName);
252 // collect for later display
253 $GLOBALS['sql_query'] .= "\n" . $query;
254 $this->dbi->selectDb($newDatabaseName);
255 $this->dbi->query($query);
260 * Handle the views, return the boolean value whether table rename/copy or not
262 * @param mixed[] $views views as an array
263 * @param bool $move whether database name is empty or not
264 * @param string $db database name
266 public function handleTheViews(array $views, bool $move, string $db, DatabaseName $newDatabaseName): void
268 // Add DROP IF EXIST to CREATE VIEW query, to remove stand-in VIEW that was created earlier.
269 foreach ($views as $view) {
270 $copyingSucceeded = Table::moveCopy(
271 $db,
272 $view,
273 $newDatabaseName->getName(),
274 $view,
275 'structure',
276 $move,
277 'db_copy',
278 true,
280 if (! $copyingSucceeded) {
281 $GLOBALS['_error'] = true;
282 break;
288 * Adjust the privileges after Renaming the db
290 * @param string $oldDb Database name before renaming
292 public function adjustPrivilegesMoveDb(string $oldDb, DatabaseName $newDatabaseName): void
294 if (
295 ! $GLOBALS['db_priv'] || ! $GLOBALS['table_priv']
296 || ! $GLOBALS['col_priv'] || ! $GLOBALS['proc_priv']
297 || ! $GLOBALS['is_reload_priv']
299 return;
302 $this->dbi->selectDb('mysql');
303 $newName = str_replace('_', '\_', $newDatabaseName->getName());
304 $oldDb = str_replace('_', '\_', $oldDb);
306 // For Db specific privileges
307 $this->dbi->query('UPDATE ' . Util::backquote('db')
308 . 'SET Db = ' . $this->dbi->quoteString($newName)
309 . ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
311 // For table specific privileges
312 $this->dbi->query('UPDATE ' . Util::backquote('tables_priv')
313 . 'SET Db = ' . $this->dbi->quoteString($newName)
314 . ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
316 // For column specific privileges
317 $this->dbi->query('UPDATE ' . Util::backquote('columns_priv')
318 . 'SET Db = ' . $this->dbi->quoteString($newName)
319 . ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
321 // For procedures specific privileges
322 $this->dbi->query('UPDATE ' . Util::backquote('procs_priv')
323 . 'SET Db = ' . $this->dbi->quoteString($newName)
324 . ' where Db = ' . $this->dbi->quoteString($oldDb) . ';');
326 // Finally FLUSH the new privileges
327 $this->dbi->query('FLUSH PRIVILEGES;');
331 * Adjust the privileges after Copying the db
333 * @param string $oldDb Database name before copying
335 public function adjustPrivilegesCopyDb(string $oldDb, DatabaseName $newDatabaseName): void
337 if (
338 ! $GLOBALS['db_priv'] || ! $GLOBALS['table_priv']
339 || ! $GLOBALS['col_priv'] || ! $GLOBALS['proc_priv']
340 || ! $GLOBALS['is_reload_priv']
342 return;
345 $this->dbi->selectDb('mysql');
346 $newName = str_replace('_', '\_', $newDatabaseName->getName());
347 $oldDb = str_replace('_', '\_', $oldDb);
349 $queryDbSpecificOld = 'SELECT * FROM '
350 . Util::backquote('db') . ' WHERE '
351 . 'Db = "' . $oldDb . '";';
353 $oldPrivsDb = $this->dbi->fetchResult($queryDbSpecificOld, 0);
355 foreach ($oldPrivsDb as $oldPriv) {
356 $newDbDbPrivsQuery = 'INSERT INTO ' . Util::backquote('db')
357 . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '"';
358 $privCount = count($oldPriv);
359 for ($i = 2; $i < $privCount; $i++) {
360 $newDbDbPrivsQuery .= ', "' . $oldPriv[$i] . '"';
363 $newDbDbPrivsQuery .= ')';
365 $this->dbi->query($newDbDbPrivsQuery);
368 // For Table Specific privileges
369 $queryTableSpecificOld = 'SELECT * FROM '
370 . Util::backquote('tables_priv') . ' WHERE '
371 . 'Db = "' . $oldDb . '";';
373 $oldPrivsTable = $this->dbi->fetchResult($queryTableSpecificOld, 0);
375 foreach ($oldPrivsTable as $oldPriv) {
376 $newDbTablePrivsQuery = 'INSERT INTO ' . Util::backquote(
377 'tables_priv',
378 ) . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '", "'
379 . $oldPriv[2] . '", "' . $oldPriv[3] . '", "' . $oldPriv[4]
380 . '", "' . $oldPriv[5] . '", "' . $oldPriv[6] . '", "'
381 . $oldPriv[7] . '");';
383 $this->dbi->query($newDbTablePrivsQuery);
386 // For Column Specific privileges
387 $queryColSpecificOld = 'SELECT * FROM '
388 . Util::backquote('columns_priv') . ' WHERE '
389 . 'Db = "' . $oldDb . '";';
391 $oldPrivsCol = $this->dbi->fetchResult($queryColSpecificOld, 0);
393 foreach ($oldPrivsCol as $oldPriv) {
394 $newDbColPrivsQuery = 'INSERT INTO ' . Util::backquote(
395 'columns_priv',
396 ) . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '", "'
397 . $oldPriv[2] . '", "' . $oldPriv[3] . '", "' . $oldPriv[4]
398 . '", "' . $oldPriv[5] . '", "' . $oldPriv[6] . '");';
400 $this->dbi->query($newDbColPrivsQuery);
403 // For Procedure Specific privileges
404 $queryProcSpecificOld = 'SELECT * FROM '
405 . Util::backquote('procs_priv') . ' WHERE '
406 . 'Db = "' . $oldDb . '";';
408 $oldPrivsProc = $this->dbi->fetchResult($queryProcSpecificOld, 0);
410 foreach ($oldPrivsProc as $oldPriv) {
411 $newDbProcPrivsQuery = 'INSERT INTO ' . Util::backquote(
412 'procs_priv',
413 ) . ' VALUES("' . $oldPriv[0] . '", "' . $newName . '", "'
414 . $oldPriv[2] . '", "' . $oldPriv[3] . '", "' . $oldPriv[4]
415 . '", "' . $oldPriv[5] . '", "' . $oldPriv[6] . '", "'
416 . $oldPriv[7] . '");';
418 $this->dbi->query($newDbProcPrivsQuery);
421 // Finally FLUSH the new privileges
422 $this->dbi->query('FLUSH PRIVILEGES;');
426 * Create all accumulated constraints
428 * @param mixed[] $sqlConstraints array of sql constraints for the database
430 public function createAllAccumulatedConstraints(array $sqlConstraints, DatabaseName $newDatabaseName): void
432 $this->dbi->selectDb($newDatabaseName);
433 foreach ($sqlConstraints as $query) {
434 $this->dbi->query($query);
435 // and prepare to display them
436 $GLOBALS['sql_query'] .= "\n" . $query;
441 * Duplicate the bookmarks for the db (done once for each db)
443 * @param bool $error whether table rename/copy or not
444 * @param string $db database name
446 public function duplicateBookmarks(bool $error, string $db, DatabaseName $newDatabaseName): void
448 if ($error || $db === $newDatabaseName->getName()) {
449 return;
452 $getFields = ['user', 'label', 'query'];
453 $whereFields = ['dbase' => $db];
454 $newFields = ['dbase' => $newDatabaseName->getName()];
455 Table::duplicateInfo('bookmarkwork', 'bookmark', $getFields, $whereFields, $newFields);
459 * Get array of possible row formats
461 * @return mixed[]
463 public function getPossibleRowFormat(): array
465 // the outer array is for engines, the inner array contains the dropdown
466 // option values as keys then the dropdown option labels
468 $possibleRowFormats = [
469 'ARCHIVE' => ['COMPRESSED' => 'COMPRESSED'],
470 'ARIA' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC', 'PAGE' => 'PAGE'],
471 'MARIA' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC', 'PAGE' => 'PAGE'],
472 'MYISAM' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC'],
473 'PBXT' => ['FIXED' => 'FIXED', 'DYNAMIC' => 'DYNAMIC'],
474 'INNODB' => ['COMPACT' => 'COMPACT', 'REDUNDANT' => 'REDUNDANT'],
477 /** @var Innodb $innodbEnginePlugin */
478 $innodbEnginePlugin = StorageEngine::getEngine('Innodb');
479 $innodbPluginVersion = $innodbEnginePlugin->getInnodbPluginVersion();
480 $innodbFileFormat = '';
481 if ($innodbPluginVersion !== '') {
482 $innodbFileFormat = $innodbEnginePlugin->getInnodbFileFormat() ?? '';
486 * Newer MySQL/MariaDB always return empty a.k.a '' on $innodbFileFormat otherwise
487 * old versions of MySQL/MariaDB must be returning something or not empty.
488 * This patch is to support newer MySQL/MariaDB while also for backward compatibilities.
490 if (
491 (strtolower($innodbFileFormat) === 'barracuda') || ($innodbFileFormat == '')
492 && $innodbEnginePlugin->supportsFilePerTable()
494 $possibleRowFormats['INNODB']['DYNAMIC'] = 'DYNAMIC';
495 $possibleRowFormats['INNODB']['COMPRESSED'] = 'COMPRESSED';
498 return $possibleRowFormats;
501 /** @return array<string, string> */
502 public function getPartitionMaintenanceChoices(): array
504 $choices = [
505 'ANALYZE' => __('Analyze'),
506 'CHECK' => __('Check'),
507 'OPTIMIZE' => __('Optimize'),
508 'REBUILD' => __('Rebuild'),
509 'REPAIR' => __('Repair'),
510 'TRUNCATE' => __('Truncate'),
513 $partitionMethod = Partition::getPartitionMethod($GLOBALS['db'], $GLOBALS['table']);
515 // add COALESCE or DROP option to choices array depending on Partition method
516 if (
517 $partitionMethod === 'RANGE'
518 || $partitionMethod === 'RANGE COLUMNS'
519 || $partitionMethod === 'LIST'
520 || $partitionMethod === 'LIST COLUMNS'
522 $choices['DROP'] = __('Drop');
523 } else {
524 $choices['COALESCE'] = __('Coalesce');
527 return $choices;
531 * @param mixed[] $urlParams Array of url parameters.
532 * @param bool $hasRelationFeature If relation feature is enabled.
534 * @return mixed[]
536 public function getForeignersForReferentialIntegrityCheck(
537 array $urlParams,
538 bool $hasRelationFeature,
539 ): array {
540 if (! $hasRelationFeature) {
541 return [];
544 $foreigners = [];
545 $this->dbi->selectDb($GLOBALS['db']);
546 $foreign = $this->relation->getForeigners($GLOBALS['db'], $GLOBALS['table'], '', 'internal');
548 foreach ($foreign as $master => $arr) {
549 $joinQuery = 'SELECT '
550 . Util::backquote($GLOBALS['table']) . '.*'
551 . ' FROM ' . Util::backquote($GLOBALS['table'])
552 . ' LEFT JOIN '
553 . Util::backquote($arr['foreign_db'])
554 . '.'
555 . Util::backquote($arr['foreign_table']);
557 if ($arr['foreign_table'] == $GLOBALS['table']) {
558 $foreignTable = $GLOBALS['table'] . '1';
559 $joinQuery .= ' AS ' . Util::backquote($foreignTable);
560 } else {
561 $foreignTable = $arr['foreign_table'];
564 $joinQuery .= ' ON '
565 . Util::backquote($GLOBALS['table']) . '.'
566 . Util::backquote($master)
567 . ' = '
568 . Util::backquote($arr['foreign_db'])
569 . '.'
570 . Util::backquote($foreignTable) . '.'
571 . Util::backquote($arr['foreign_field'])
572 . ' WHERE '
573 . Util::backquote($arr['foreign_db'])
574 . '.'
575 . Util::backquote($foreignTable) . '.'
576 . Util::backquote($arr['foreign_field'])
577 . ' IS NULL AND '
578 . Util::backquote($GLOBALS['table']) . '.'
579 . Util::backquote($master)
580 . ' IS NOT NULL';
581 $thisUrlParams = array_merge(
582 $urlParams,
583 ['sql_query' => $joinQuery, 'sql_signature' => Core::signSqlQuery($joinQuery)],
586 $foreigners[] = [
587 'params' => $thisUrlParams,
588 'master' => $master,
589 'db' => $arr['foreign_db'],
590 'table' => $arr['foreign_table'],
591 'field' => $arr['foreign_field'],
595 return $foreigners;
599 * Get table alters array
601 * @param Table $pmaTable The Table object
602 * @param string $packKeys pack keys
603 * @param string $checksum value of checksum
604 * @param string $pageChecksum value of page checksum
605 * @param string $delayKeyWrite delay key write
606 * @param string $rowFormat row format
607 * @param string $newTblStorageEngine table storage engine
608 * @param string $transactional value of transactional
609 * @param string $tableCollation collation of the table
611 * @return mixed[]
613 public function getTableAltersArray(
614 Table $pmaTable,
615 string $packKeys,
616 string $checksum,
617 string $pageChecksum,
618 string $delayKeyWrite,
619 string $rowFormat,
620 string $newTblStorageEngine,
621 string $transactional,
622 string $tableCollation,
623 string $tableStorageEngine,
624 ): array {
625 $GLOBALS['auto_increment'] ??= null;
627 $tableAlters = [];
629 if (isset($_POST['comment']) && urldecode($_POST['prev_comment']) !== $_POST['comment']) {
630 $tableAlters[] = 'COMMENT = ' . $this->dbi->quoteString($_POST['comment']);
633 if (
634 $newTblStorageEngine !== ''
635 && mb_strtolower($newTblStorageEngine) !== mb_strtolower($tableStorageEngine)
637 $tableAlters[] = 'ENGINE = ' . $newTblStorageEngine;
640 if (! empty($_POST['tbl_collation']) && $_POST['tbl_collation'] !== $tableCollation) {
641 $tableAlters[] = 'DEFAULT '
642 . Util::getCharsetQueryPart($_POST['tbl_collation']);
645 if (
646 $pmaTable->isEngine(['MYISAM', 'ARIA', 'ISAM'])
647 && isset($_POST['new_pack_keys'])
648 && $_POST['new_pack_keys'] != $packKeys
650 $tableAlters[] = 'pack_keys = ' . $_POST['new_pack_keys'];
653 $newChecksum = empty($_POST['new_checksum']) ? '0' : '1';
654 if ($pmaTable->isEngine(['MYISAM', 'ARIA']) && $newChecksum !== $checksum) {
655 $tableAlters[] = 'checksum = ' . $newChecksum;
658 $newTransactional = empty($_POST['new_transactional']) ? '0' : '1';
659 if ($pmaTable->isEngine('ARIA') && $newTransactional !== $transactional) {
660 $tableAlters[] = 'TRANSACTIONAL = ' . $newTransactional;
663 $newPageChecksum = empty($_POST['new_page_checksum']) ? '0' : '1';
664 if ($pmaTable->isEngine('ARIA') && $newPageChecksum !== $pageChecksum) {
665 $tableAlters[] = 'PAGE_CHECKSUM = ' . $newPageChecksum;
668 $newDelayKeyWrite = empty($_POST['new_delay_key_write']) ? '0' : '1';
669 if ($pmaTable->isEngine(['MYISAM', 'ARIA']) && $newDelayKeyWrite !== $delayKeyWrite) {
670 $tableAlters[] = 'delay_key_write = ' . $newDelayKeyWrite;
673 if (
674 $pmaTable->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT', 'ROCKSDB'])
675 && ! empty($_POST['new_auto_increment'])
676 && (! isset($GLOBALS['auto_increment'])
677 || $_POST['new_auto_increment'] !== $GLOBALS['auto_increment'])
678 && $_POST['new_auto_increment'] !== $_POST['hidden_auto_increment']
680 $tableAlters[] = 'auto_increment = '
681 . $this->dbi->escapeString($_POST['new_auto_increment']);
684 if (! empty($_POST['new_row_format'])) {
685 $newRowFormat = $_POST['new_row_format'];
686 $newRowFormatLower = mb_strtolower($newRowFormat);
687 if (
688 $pmaTable->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT'])
689 && ($rowFormat === ''
690 || $newRowFormatLower !== mb_strtolower($rowFormat))
692 $tableAlters[] = 'ROW_FORMAT = '
693 . $this->dbi->escapeString($newRowFormat);
697 return $tableAlters;
701 * Get warning messages array
703 * @return string[]
705 public function getWarningMessagesArray(): array
707 $warningMessages = [];
708 foreach ($this->dbi->getWarnings() as $warning) {
709 // In MariaDB 5.1.44, when altering a table from Maria to MyISAM
710 // and if TRANSACTIONAL was set, the system reports an error;
711 // I discussed with a Maria developer and he agrees that this
712 // should not be reported with a Level of Error, so here
713 // I just ignore it. But there are other 1478 messages
714 // that it's better to show.
715 if (
716 isset($_POST['new_tbl_storage_engine'])
717 && $_POST['new_tbl_storage_engine'] === 'MyISAM'
718 && $warning->code === 1478
719 && $warning->level === 'Error'
721 continue;
724 $warningMessages[] = (string) $warning;
727 return $warningMessages;
731 * Adjust the privileges after renaming/moving a table
733 * @param string $oldDb Database name before table renaming/moving table
734 * @param string $oldTable Table name before table renaming/moving table
735 * @param string $newDb Database name after table renaming/ moving table
736 * @param string $newTable Table name after table renaming/moving table
738 public function adjustPrivilegesRenameOrMoveTable(
739 string $oldDb,
740 string $oldTable,
741 string $newDb,
742 string $newTable,
743 ): void {
744 if (! $GLOBALS['table_priv'] || ! $GLOBALS['col_priv'] || ! $GLOBALS['is_reload_priv']) {
745 return;
748 $this->dbi->selectDb('mysql');
750 // For table specific privileges
751 $this->dbi->query('UPDATE ' . Util::backquote('tables_priv')
752 . 'SET Db = ' . $this->dbi->quoteString($newDb)
753 . ', Table_name = ' . $this->dbi->quoteString($newTable)
754 . ' where Db = ' . $this->dbi->quoteString($oldDb)
755 . ' AND Table_name = ' . $this->dbi->quoteString($oldTable)
756 . ';');
758 // For column specific privileges
759 $this->dbi->query('UPDATE ' . Util::backquote('columns_priv')
760 . 'SET Db = ' . $this->dbi->quoteString($newDb)
761 . ', Table_name = ' . $this->dbi->quoteString($newTable)
762 . ' where Db = ' . $this->dbi->quoteString($oldDb)
763 . ' AND Table_name = ' . $this->dbi->quoteString($oldTable)
764 . ';');
766 // Finally FLUSH the new privileges
767 $this->dbi->query('FLUSH PRIVILEGES;');
771 * Adjust the privileges after copying a table
773 * @param string $oldDb Database name before table copying
774 * @param string $oldTable Table name before table copying
775 * @param string $newDb Database name after table copying
776 * @param string $newTable Table name after table copying
778 public function adjustPrivilegesCopyTable(string $oldDb, string $oldTable, string $newDb, string $newTable): void
780 if (! $GLOBALS['table_priv'] || ! $GLOBALS['col_priv'] || ! $GLOBALS['is_reload_priv']) {
781 return;
784 $this->dbi->selectDb('mysql');
786 // For Table Specific privileges
787 $queryTableSpecificOld = 'SELECT * FROM '
788 . Util::backquote('tables_priv') . ' where '
789 . 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";';
791 $oldPrivsTable = $this->dbi->fetchResult($queryTableSpecificOld, 0);
793 foreach ($oldPrivsTable as $oldPriv) {
794 $newDbTablePrivsQuery = 'INSERT INTO '
795 . Util::backquote('tables_priv') . ' VALUES("'
796 . $oldPriv[0] . '", "' . $newDb . '", "' . $oldPriv[2] . '", "'
797 . $newTable . '", "' . $oldPriv[4] . '", "' . $oldPriv[5]
798 . '", "' . $oldPriv[6] . '", "' . $oldPriv[7] . '");';
800 $this->dbi->query($newDbTablePrivsQuery);
803 // For Column Specific privileges
804 $queryColSpecificOld = 'SELECT * FROM '
805 . Util::backquote('columns_priv') . ' WHERE '
806 . 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";';
808 $oldPrivsCol = $this->dbi->fetchResult($queryColSpecificOld, 0);
810 foreach ($oldPrivsCol as $oldPriv) {
811 $newDbColPrivsQuery = 'INSERT INTO '
812 . Util::backquote('columns_priv') . ' VALUES("'
813 . $oldPriv[0] . '", "' . $newDb . '", "' . $oldPriv[2] . '", "'
814 . $newTable . '", "' . $oldPriv[4] . '", "' . $oldPriv[5]
815 . '", "' . $oldPriv[6] . '");';
817 $this->dbi->query($newDbColPrivsQuery);
820 // Finally FLUSH the new privileges
821 $this->dbi->query('FLUSH PRIVILEGES;');
825 * Change all collations and character sets of all columns in table
827 * @param string $db Database name
828 * @param string $table Table name
829 * @param string $tableCollation Collation Name
831 public function changeAllColumnsCollation(string $db, string $table, string $tableCollation): void
833 $this->dbi->selectDb($db);
835 $changeAllCollationsQuery = 'ALTER TABLE '
836 . Util::backquote($table)
837 . ' CONVERT TO';
839 [$charset] = explode('_', $tableCollation);
841 $changeAllCollationsQuery .= ' CHARACTER SET ' . $charset
842 . ($charset === $tableCollation ? '' : ' COLLATE ' . $tableCollation);
844 $this->dbi->query($changeAllCollationsQuery);
848 * Move or copy a table
850 * @param string $db current database name
851 * @param string $table current table name
853 public function moveOrCopyTable(string $db, string $table): Message
856 * Selects the database to work with
858 $this->dbi->selectDb($db);
861 * $_POST['target_db'] could be empty in case we came from an input field
862 * (when there are many databases, no drop-down)
864 $targetDb = $db;
865 if (isset($_POST['target_db']) && is_string($_POST['target_db']) && $_POST['target_db'] !== '') {
866 $targetDb = $_POST['target_db'];
870 * A target table name has been sent to this script -> do the work
872 if (isset($_POST['new_name']) && is_scalar($_POST['new_name']) && (string) $_POST['new_name'] !== '') {
873 if ($db === $targetDb && $table == $_POST['new_name']) {
874 if (isset($_POST['submit_move'])) {
875 $message = Message::error(__('Can\'t move table to same one!'));
876 } else {
877 $message = Message::error(__('Can\'t copy table to same one!'));
879 } else {
880 Table::moveCopy(
881 $db,
882 $table,
883 $targetDb,
884 (string) $_POST['new_name'],
885 $_POST['what'],
886 isset($_POST['submit_move']),
887 'one_table',
888 isset($_POST['drop_if_exists']) && $_POST['drop_if_exists'] === 'true',
891 if (! empty($_POST['adjust_privileges'])) {
892 if (isset($_POST['submit_move'])) {
893 $this->adjustPrivilegesRenameOrMoveTable($db, $table, $targetDb, (string) $_POST['new_name']);
894 } else {
895 $this->adjustPrivilegesCopyTable($db, $table, $targetDb, (string) $_POST['new_name']);
898 if (isset($_POST['submit_move'])) {
899 $message = Message::success(
901 'Table %s has been moved to %s. Privileges have been adjusted.',
904 } else {
905 $message = Message::success(
907 'Table %s has been copied to %s. Privileges have been adjusted.',
911 } elseif (isset($_POST['submit_move'])) {
912 $message = Message::success(
913 __('Table %s has been moved to %s.'),
915 } else {
916 $message = Message::success(
917 __('Table %s has been copied to %s.'),
921 $old = Util::backquote($db) . '.'
922 . Util::backquote($table);
923 $message->addParam($old);
925 $newName = (string) $_POST['new_name'];
926 if ($this->dbi->getLowerCaseNames() === 1) {
927 $newName = strtolower($newName);
930 $GLOBALS['table'] = $newName;
932 $new = Util::backquote($targetDb) . '.'
933 . Util::backquote($newName);
934 $message->addParam($new);
936 } else {
938 * No new name for the table!
940 $message = Message::error(__('The table name is empty!'));
943 return $message;