Merge branch 'QA_5_2'
[phpmyadmin.git] / src / Table / Table.php
blob600d06949a52e0947cbada1071952088216d9db8
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin\Table;
7 use PhpMyAdmin\Config;
8 use PhpMyAdmin\ConfigStorage\Features\DisplayFeature;
9 use PhpMyAdmin\ConfigStorage\Features\RelationFeature;
10 use PhpMyAdmin\ConfigStorage\Features\UiPreferencesFeature;
11 use PhpMyAdmin\ConfigStorage\Relation;
12 use PhpMyAdmin\Current;
13 use PhpMyAdmin\DatabaseInterface;
14 use PhpMyAdmin\Dbal\ConnectionType;
15 use PhpMyAdmin\FieldMetadata;
16 use PhpMyAdmin\Html\Generator;
17 use PhpMyAdmin\Html\MySQLDocumentation;
18 use PhpMyAdmin\Index;
19 use PhpMyAdmin\Message;
20 use PhpMyAdmin\Query\Compatibility;
21 use PhpMyAdmin\Query\Generator as QueryGenerator;
22 use PhpMyAdmin\SqlParser\Context;
23 use PhpMyAdmin\SqlParser\Parser;
24 use PhpMyAdmin\SqlParser\Statements\CreateStatement;
25 use PhpMyAdmin\SqlParser\Utils\Table as TableUtils;
26 use PhpMyAdmin\Triggers\Triggers;
27 use PhpMyAdmin\Util;
28 use Stringable;
30 use function __;
31 use function array_keys;
32 use function array_map;
33 use function array_merge;
34 use function count;
35 use function end;
36 use function explode;
37 use function htmlspecialchars;
38 use function implode;
39 use function in_array;
40 use function intval;
41 use function is_array;
42 use function is_numeric;
43 use function is_string;
44 use function json_decode;
45 use function json_encode;
46 use function mb_stripos;
47 use function mb_strlen;
48 use function mb_substr;
49 use function preg_match;
50 use function preg_replace;
51 use function rtrim;
52 use function sprintf;
53 use function str_contains;
54 use function str_replace;
55 use function stripos;
56 use function strtolower;
57 use function strtoupper;
58 use function substr;
59 use function substr_compare;
60 use function trim;
62 /**
63 * Handles everything related to tables
65 * @todo make use of Message and Error
67 class Table implements Stringable
69 /** @var mixed[] UI preferences */
70 public array $uiprefs = [];
72 /** @var mixed[] errors occurred */
73 public array $errors = [];
75 /** @var mixed[] messages */
76 public array $messages = [];
78 private Relation $relation;
80 /**
81 * @param string $name table name
82 * @param string $dbName database name
83 * @param DatabaseInterface $dbi database interface for the table
85 public function __construct(protected string $name, protected string $dbName, protected DatabaseInterface $dbi)
87 $this->relation = new Relation($this->dbi);
90 /**
91 * returns table name
93 * @see Table::getName()
95 public function __toString(): string
97 return $this->getName();
101 * Table getter
103 * @param string $tableName table name
104 * @param string $dbName database name
105 * @param DatabaseInterface $dbi database interface for the table
107 public static function get(string $tableName, string $dbName, DatabaseInterface $dbi): Table
109 return new Table($tableName, $dbName, $dbi);
113 * return the last error
115 * @return string the last error
117 public function getLastError(): string
119 if ($this->errors === []) {
120 return '';
123 return end($this->errors);
127 * return the last message
129 * @return string the last message
131 public function getLastMessage(): string
133 if ($this->messages === []) {
134 return '';
137 return end($this->messages);
141 * returns table name
143 * @param bool $backquoted whether to quote name with backticks ``
145 * @return string table name
147 public function getName(bool $backquoted = false): string
149 if ($backquoted) {
150 return Util::backquote($this->name);
153 return $this->name;
157 * returns database name for this table
159 * @param bool $backquoted whether to quote name with backticks ``
161 * @return string database name for this table
163 public function getDbName(bool $backquoted = false): string
165 if ($backquoted) {
166 return Util::backquote($this->dbName);
169 return $this->dbName;
173 * returns full name for table, including database name
175 * @param bool $backquoted whether to quote name with backticks ``
177 public function getFullName(bool $backquoted = false): string
179 return $this->getDbName($backquoted) . '.'
180 . $this->getName($backquoted);
184 * Checks the storage engine used to create table
186 * @param string[]|string $engine Checks the table engine against an
187 * array of engine strings or a single string, should be uppercase
189 public function isEngine(array|string $engine): bool
191 $engine = (array) $engine;
192 $tableStorageEngine = $this->getStorageEngine();
194 return in_array($tableStorageEngine, $engine, true);
198 * returns whether the table is actually a view
200 public function isView(): bool
202 if ($this->dbName === '' || $this->name === '') {
203 return false;
206 // use cached data or load information with SHOW command
207 $type = $this->dbi->getCache()->getCachedTableContent($this->dbName, $this->name, 'TABLE_TYPE');
208 if ($type === null && Config::getInstance()->selectedServer['DisableIS']) {
209 $type = $this->getStatusInfo('TABLE_TYPE');
212 if ($type !== null) {
213 return $type === 'VIEW' || $type === 'SYSTEM VIEW';
216 // information_schema tables are 'SYSTEM VIEW's
217 if ($this->dbName === 'information_schema') {
218 return true;
221 // query information_schema
222 return (bool) $this->dbi->fetchValue(
223 'SELECT 1'
224 . ' FROM information_schema.VIEWS'
225 . ' WHERE TABLE_SCHEMA = ' . $this->dbi->quoteString($this->dbName)
226 . ' AND TABLE_NAME = ' . $this->dbi->quoteString($this->name),
231 * Returns whether the table is actually an updatable view
233 public function isUpdatableView(): bool
235 if ($this->dbName === '' || $this->name === '') {
236 return false;
239 return (bool) $this->dbi->fetchValue(
240 'SELECT 1'
241 . ' FROM information_schema.VIEWS'
242 . ' WHERE TABLE_SCHEMA = ' . $this->dbi->quoteString($this->dbName)
243 . ' AND TABLE_NAME = ' . $this->dbi->quoteString($this->name)
244 . ' AND IS_UPDATABLE = \'YES\'',
249 * Checks if this is a merge table
251 * If the ENGINE of the table is MERGE or MRG_MYISAM (alias),
252 * this is a merge table.
254 public function isMerge(): bool
256 return $this->isEngine(['MERGE', 'MRG_MYISAM']);
260 * Returns full table status info, or specific if $info provided
261 * this info is collected from information_schema
263 * @param T $info specific information to be fetched
265 * @return (T is null ? (string|int|null)[]|null : (string|int|null))
267 * @template T of string|null
269 public function getStatusInfo(string|null $info = null): array|string|int|null
271 $cachedResult = $this->dbi->getCache()->getCachedTableContent($this->dbName, $this->name);
273 // sometimes there is only one entry (ExactRows) so
274 // we have to get the table's details
275 if ($cachedResult === null || count($cachedResult) === 1) {
276 $this->dbi->getTablesFull($this->dbName, $this->name);
277 $cachedResult = $this->dbi->getCache()->getCachedTableContent($this->dbName, $this->name);
280 if ($cachedResult === null) {
281 // happens when we enter the table creation dialog
282 // or when we really did not get any status info, for example
283 // when $table === 'TABLE_NAMES' after the user tried SHOW TABLES
284 return null;
287 if ($info === null) {
288 return $cachedResult;
291 return $cachedResult[$info];
295 * Returns the Table storage Engine for current table.
297 * @return string Return storage engine info if it is set for
298 * the selected table else return blank.
300 public function getStorageEngine(): string
302 $tableStorageEngine = $this->getStatusInfo('ENGINE');
304 return strtoupper((string) $tableStorageEngine);
308 * Returns the comments for current table.
310 * @return string Return comment info if it is set for the selected table or return blank.
312 public function getComment(): string
314 return $this->getStatusInfo('TABLE_COMMENT') ?? '';
318 * Returns the collation for current table.
320 * @return string Return blank if collation is empty else return the collation info from table info.
322 public function getCollation(): string
324 return $this->getStatusInfo('TABLE_COLLATION') ?? '';
328 * Returns the info about no of rows for current table.
330 * @return int Return no of rows info if it is not null for the selected table or return 0.
332 public function getNumRows(): int
334 return (int) $this->getStatusInfo('TABLE_ROWS');
338 * Returns the Row format for current table.
340 * @return string Return table row format info if it is set for the selected table or return blank.
342 public function getRowFormat(): string
344 $tableRowFormat = $this->getStatusInfo('ROW_FORMAT');
346 return is_string($tableRowFormat) ? $tableRowFormat : '';
350 * Returns the auto increment option for current table.
352 * @return string Return auto increment info if it is set for the selected table or return blank.
354 public function getAutoIncrement(): string
356 $tableAutoIncrement = $this->getStatusInfo('AUTO_INCREMENT');
358 return $tableAutoIncrement ?? '';
362 * Returns the array for CREATE statement for current table.
364 * @return array<string, string> Return options array info if it is set for the selected table or return blank.
366 public function getCreateOptions(): array
368 $tableOptions = $this->getStatusInfo('CREATE_OPTIONS');
369 $createOptionsTmp = is_string($tableOptions) && $tableOptions !== '' ? explode(' ', $tableOptions) : [];
370 $createOptions = [];
371 // export create options by its name as variables into global namespace
372 // f.e. pack_keys=1 becomes available as $pack_keys with value of '1'
373 // unset($pack_keys);
374 foreach ($createOptionsTmp as $eachCreateOption) {
375 $eachCreateOption = explode('=', $eachCreateOption);
376 if (! isset($eachCreateOption[1])) {
377 continue;
380 // ensure there is no ambiguity for PHP 5 and 7
381 $createOptions[$eachCreateOption[0]] = $eachCreateOption[1];
384 // we need explicit DEFAULT value here (different from '0')
385 $hasPackKeys = isset($createOptions['pack_keys']) && $createOptions['pack_keys'] !== '';
386 $createOptions['pack_keys'] = $hasPackKeys ? $createOptions['pack_keys'] : 'DEFAULT';
388 return $createOptions;
392 * generates column specification for ALTER or CREATE TABLE syntax
394 * @param string $name name
395 * @param string $type type ('INT', 'VARCHAR', 'BIT', ...)
396 * @param string $length length ('2', '5,2', '', ...)
397 * @param string $attribute attribute
398 * @param string $collation collation
399 * @param bool|string $null with 'NULL' or 'NOT NULL'
400 * @param string $defaultType whether default is CURRENT_TIMESTAMP,
401 * NULL, NONE, USER_DEFINED, UUID
402 * @param string $defaultValue default value for USER_DEFINED
403 * default type
404 * @param string $extra 'AUTO_INCREMENT'
405 * @param string $comment field comment
406 * @param string $virtuality virtuality of the column
407 * @param string $expression expression for the virtual column
408 * @param string $moveTo new position for column
409 * @param (string|int)[]|null $columnsWithIndex Fields having PRIMARY or UNIQUE KEY indexes
410 * @param string|null $oldColumnName Old column name
412 * @return string field specification
414 * @todo move into class PMA_Column
415 * @todo on the interface, some js to clear the default value when the
416 * default current_timestamp is checked
418 public static function generateFieldSpec(
419 string $name,
420 string $type,
421 string $length = '',
422 string $attribute = '',
423 string $collation = '',
424 bool|string $null = false,
425 string $defaultType = 'USER_DEFINED',
426 string $defaultValue = '',
427 string $extra = '',
428 string $comment = '',
429 string $virtuality = '',
430 string $expression = '',
431 string $moveTo = '',
432 array|null $columnsWithIndex = null,
433 string|null $oldColumnName = null,
434 ): string {
435 $isTimestamp = mb_stripos($type, 'TIMESTAMP') !== false;
437 $query = Util::backquote($name) . ' ' . $type;
439 // allow the possibility of a length for TIME, DATETIME and TIMESTAMP
440 // (will work on MySQL >= 5.6.4)
442 // MySQL permits a non-standard syntax for FLOAT and DOUBLE,
443 // see https://dev.mysql.com/doc/refman/5.5/en/floating-point-types.html
444 $pattern = '@^(DATE|TINYBLOB|TINYTEXT|BLOB|TEXT|'
445 . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN|UUID|JSON)$@i';
446 $dbi = DatabaseInterface::getInstance();
447 if (
448 $length !== ''
449 && preg_match($pattern, $type) !== 1
450 && Compatibility::isIntegersSupportLength($type, $length, $dbi)
452 // Note: The variable $length here can contain several other things
453 // besides length - ENUM/SET value or length of DECIMAL (eg. 12,3)
454 // so we can't just convert it to integer
455 $query .= '(' . $length . ')';
458 if ($attribute !== '') {
459 $query .= ' ' . $attribute;
461 if ($isTimestamp && stripos($attribute, 'TIMESTAMP') !== false && $length !== '') {
462 $query .= '(' . $length . ')';
466 // if column is virtual, check if server type is Mysql as only Mysql server
467 // supports extra column properties
468 $isVirtualColMysql = $virtuality && Compatibility::isMySqlOrPerconaDb();
469 // if column is virtual, check if server type is MariaDB as MariaDB server
470 // supports no extra virtual column properties except CHARACTER SET for text column types
471 $isVirtualColMariaDB = $virtuality && Compatibility::isMariaDb();
473 $matches = preg_match('@^(TINYTEXT|TEXT|MEDIUMTEXT|LONGTEXT|VARCHAR|CHAR|ENUM|SET)$@i', $type) === 1;
474 if ($collation !== '' && $collation !== 'NULL' && $matches) {
475 $query .= Util::getCharsetQueryPart(
476 $isVirtualColMariaDB ? (string) preg_replace('~_.+~s', '', $collation) : $collation,
477 true,
481 if ($virtuality !== '') {
482 $query .= ' AS (' . $expression . ') ' . $virtuality;
485 if ($virtuality === '' || $isVirtualColMysql) {
486 if ($null !== false) {
487 if ($null === 'YES') {
488 $query .= ' NULL';
489 } else {
490 $query .= ' NOT NULL';
494 if ($virtuality === '') {
495 switch ($defaultType) {
496 case 'USER_DEFINED':
497 if ($isTimestamp && $defaultValue === '0') {
498 // a TIMESTAMP does not accept DEFAULT '0'
499 // but DEFAULT 0 works
500 $query .= ' DEFAULT 0';
501 } elseif (
502 $isTimestamp
503 && preg_match('/^\'\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d(\.\d{1,6})?\'$/', $defaultValue) === 1
505 $query .= ' DEFAULT ' . $defaultValue;
506 } elseif ($type === 'BIT') {
507 $query .= ' DEFAULT b\''
508 . preg_replace('/[^01]/', '0', $defaultValue)
509 . '\'';
510 } elseif ($type === 'BOOLEAN') {
511 if (preg_match('/^1|T|TRUE|YES$/i', $defaultValue) === 1) {
512 $query .= ' DEFAULT TRUE';
513 } elseif (preg_match('/^0|F|FALSE|NO$/i', $defaultValue) === 1) {
514 $query .= ' DEFAULT FALSE';
515 } else {
516 // Invalid BOOLEAN value
517 $query .= ' DEFAULT ' . $dbi->quoteString($defaultValue);
519 } elseif ($type === 'BINARY' || $type === 'VARBINARY') {
520 $query .= ' DEFAULT 0x' . $defaultValue;
521 } else {
522 $query .= ' DEFAULT ' . $dbi->quoteString($defaultValue);
525 break;
526 /** @noinspection PhpMissingBreakStatementInspection */
527 case 'NULL':
528 // If user uncheck null checkbox and not change default value null,
529 // default value will be ignored.
530 if ($null !== false && $null !== 'YES') {
531 break;
533 // else fall-through intended, no break here
534 case 'CURRENT_TIMESTAMP':
535 case 'current_timestamp()':
536 $query .= ' DEFAULT ' . $defaultType;
538 if (
539 $length !== ''
540 && $isTimestamp
541 && $defaultType !== 'NULL' // Not to be added in case of NULL
543 $query .= '(' . $length . ')';
546 break;
547 case 'UUID':
548 case 'uuid()':
549 $query .= ' DEFAULT uuid()';
551 break;
552 case 'NONE':
553 default:
554 break;
558 if ($extra !== '') {
559 if ($virtuality !== '') {
560 $extra = trim((string) preg_replace('~^\s*AUTO_INCREMENT\s*~is', ' ', $extra));
563 $query .= ' ' . $extra;
567 if ($comment !== '') {
568 $query .= ' COMMENT ' . $dbi->quoteString($comment);
571 // move column
572 if ($moveTo === '-first') { // dash can't appear as part of column name
573 $query .= ' FIRST';
574 } elseif ($moveTo !== '') {
575 $query .= ' AFTER ' . Util::backquote($moveTo);
578 if ($virtuality === '' && $extra !== '') {
579 if ($oldColumnName === null) {
580 if (is_array($columnsWithIndex) && ! in_array($name, $columnsWithIndex)) {
581 $query .= ', add PRIMARY KEY (' . Util::backquote($name) . ')';
583 } elseif (is_array($columnsWithIndex) && ! in_array($oldColumnName, $columnsWithIndex)) {
584 $query .= ', add PRIMARY KEY (' . Util::backquote($name) . ')';
588 return $query;
592 * Checks if the number of records in a table is at least equal to
593 * $min_records
595 * @param int $minRecords Number of records to check for in a table
597 public function checkIfMinRecordsExist(int $minRecords = 0): bool
599 $checkQuery = 'SELECT ';
601 $uniqueFields = $this->getUniqueColumns(true, false);
602 if ($uniqueFields !== []) {
603 $fieldsToSelect = implode(', ', $uniqueFields);
604 } else {
605 $indexedCols = $this->getIndexedColumns(true, false);
606 if ($indexedCols !== []) {
607 $fieldsToSelect = implode(', ', $indexedCols);
608 } else {
609 $fieldsToSelect = '*';
613 $checkQuery .= $fieldsToSelect
614 . ' FROM ' . $this->getFullName(true)
615 . ' LIMIT ' . $minRecords;
617 $res = $this->dbi->tryQuery($checkQuery);
619 if ($res !== false) {
620 $numRecords = $res->numRows();
621 if ($numRecords >= $minRecords) {
622 return true;
626 return false;
630 * Counts the number of records in a table
632 * @param bool $forceExact whether to force an exact count
634 public function countRecords(bool $forceExact = false): int
636 $isView = $this->isView();
637 $cache = $this->dbi->getCache();
639 $exactRowsCached = $cache->getCachedTableContent($this->dbName, $this->name, 'ExactRows');
640 if ($exactRowsCached !== null) {
641 return (int) $exactRowsCached;
644 $rowCount = null;
646 if (! $forceExact) {
647 if ($cache->getCachedTableContent($this->dbName, $this->name, 'Rows') === null && ! $isView) {
648 $this->dbi->getTablesFull($this->dbName, $this->name);
651 $rowCount = $cache->getCachedTableContent($this->dbName, $this->name, 'Rows');
654 // for a VIEW, $row_count is always false at this point
655 $config = Config::getInstance();
656 if ($rowCount !== null && $rowCount >= $config->settings['MaxExactCount']) {
657 return (int) $rowCount;
660 if (! $isView) {
661 $rowCount = $this->dbi->fetchValue(
662 'SELECT COUNT(*) FROM ' . Util::backquote($this->dbName) . '.' . Util::backquote($this->name),
664 } elseif ($config->settings['MaxExactCountViews'] == 0) {
665 // For complex views, even trying to get a partial record
666 // count could bring down a server, so we offer an
667 // alternative: setting MaxExactCountViews to 0 will bypass
668 // completely the record counting for views
669 $rowCount = false;
670 } else {
671 // Counting all rows of a VIEW could be too long,
672 // so use a LIMIT clause.
673 // Use try_query because it can fail (when a VIEW is
674 // based on a table that no longer exists)
675 $result = $this->dbi->tryQuery(
676 'SELECT 1 FROM ' . Util::backquote($this->dbName) . '.'
677 . Util::backquote($this->name) . ' LIMIT '
678 . $config->settings['MaxExactCountViews'],
680 if ($result) {
681 $rowCount = $result->numRows();
685 if (is_numeric($rowCount)) {
686 $cache->cacheTableValue($this->dbName, $this->name, 'ExactRows', (int) $rowCount);
688 return (int) $rowCount;
691 return 0;
695 * Generates column specification for ALTER syntax
697 * @see Table::generateFieldSpec()
699 * @param string $oldcol old column name
700 * @param string $newcol new column name
701 * @param string $type type ('INT', 'VARCHAR', 'BIT', ...)
702 * @param string $length length ('2', '5,2', '', ...)
703 * @param string $attribute attribute
704 * @param string $collation collation
705 * @param bool|string $null with 'NULL' or 'NOT NULL'
706 * @param string $defaultType whether default is CURRENT_TIMESTAMP,
707 * NULL, NONE, USER_DEFINED
708 * @param string $defaultValue default value for USER_DEFINED default
709 * type
710 * @param string $extra 'AUTO_INCREMENT'
711 * @param string $comment field comment
712 * @param string $virtuality virtuality of the column
713 * @param string $expression expression for the virtual column
714 * @param string $moveTo new position for column
715 * @param (string|int)[]|null $columnsWithIndex Fields having PRIMARY or UNIQUE KEY indexes
717 * @return string field specification
719 public static function generateAlter(
720 string $oldcol,
721 string $newcol,
722 string $type,
723 string $length,
724 string $attribute,
725 string $collation,
726 bool|string $null,
727 string $defaultType,
728 string $defaultValue,
729 string $extra,
730 string $comment,
731 string $virtuality,
732 string $expression,
733 string $moveTo,
734 array|null $columnsWithIndex = null,
735 ): string {
736 return Util::backquote($oldcol) . ' '
737 . self::generateFieldSpec(
738 $newcol,
739 $type,
740 $length,
741 $attribute,
742 $collation,
743 $null,
744 $defaultType,
745 $defaultValue,
746 $extra,
747 $comment,
748 $virtuality,
749 $expression,
750 $moveTo,
751 $columnsWithIndex,
752 $oldcol,
757 * checks if given name is a valid table name,
758 * currently if not empty, trailing spaces, '.', '/' and '\'
760 * @see https://dev.mysql.com/doc/refman/5.0/en/legal-names.html
762 * @param string $tableName name to check
763 * @param bool $isBackquoted whether this name is used inside backquotes or not
765 * @todo add check for valid chars in filename on current system/os
767 public static function isValidName(string $tableName, bool $isBackquoted = false): bool
769 if ($tableName !== rtrim($tableName)) {
770 // trailing spaces not allowed even in backquotes
771 return false;
774 if ($tableName === '') {
775 // zero length
776 return false;
779 if (! $isBackquoted && $tableName !== trim($tableName)) {
780 // spaces at the start or in between only allowed inside backquotes
781 return false;
784 if (! $isBackquoted && preg_match('/^[a-zA-Z0-9_$]+$/', $tableName) === 1) {
785 // only allow the above regex in unquoted identifiers
786 // see : https://dev.mysql.com/doc/refman/5.7/en/identifiers.html
787 return true;
790 // If backquoted, all characters should be allowed (except w/ trailing spaces).
791 return $isBackquoted;
795 * renames table
797 * @param string $newName new table name
798 * @param string|null $newDb new database name
800 public function rename(string $newName, string|null $newDb = null): bool
802 if ($this->dbi->getLowerCaseNames() === 1) {
803 $newName = strtolower($newName);
806 if ($newDb !== null && $newDb !== $this->getDbName()) {
807 // Ensure the target is valid
808 if (! $this->dbi->getDatabaseList()->exists($newDb)) {
809 $this->errors[] = __('Invalid database:') . ' ' . $newDb;
811 return false;
813 } else {
814 $newDb = $this->getDbName();
817 $newTable = new Table($newName, $newDb, $this->dbi);
819 if ($this->getFullName() === $newTable->getFullName()) {
820 return true;
823 // Allow whitespaces (not trailing) in $new_name,
824 // since we are using $backquoted in getting the fullName of table
825 // below to be used in the query
826 if (! self::isValidName($newName, true)) {
827 $this->errors[] = __('Invalid table name:') . ' '
828 . $newTable->getFullName();
830 return false;
833 // If the table is moved to a different database drop its triggers first
834 $triggers = Triggers::getDetails($this->dbi, $this->getDbName(), $this->getName());
835 $handleTriggers = $this->getDbName() !== $newDb && $triggers !== [];
836 if ($handleTriggers) {
837 foreach ($triggers as $trigger) {
838 $sql = 'DROP TRIGGER IF EXISTS '
839 . Util::backquote($this->getDbName())
840 . '.' . Util::backquote($trigger->name->getName()) . ';';
841 $this->dbi->query($sql);
845 // tested also for a view, in MySQL 5.0.92, 5.1.55 and 5.5.13
846 $GLOBALS['sql_query'] = 'RENAME TABLE ' . $this->getFullName(true) . ' TO '
847 . $newTable->getFullName(true) . ';';
848 // I don't think a specific error message for views is necessary
849 if ($this->dbi->tryQuery($GLOBALS['sql_query']) === false) {
850 $this->errors[] = $this->dbi->getError();
852 // Restore triggers in the old database
853 if ($handleTriggers) {
854 $this->dbi->selectDb($this->getDbName());
855 foreach ($triggers as $trigger) {
856 $this->dbi->query($trigger->getCreateSql(''));
860 return false;
863 $oldName = $this->getName();
864 $oldDb = $this->getDbName();
865 $this->name = $newName;
866 $this->dbName = $newDb;
868 // Rename table in configuration storage
869 $this->relation->renameTable($oldDb, $newDb, $oldName, $newName);
871 $this->messages[] = sprintf(
872 __('Table %1$s has been renamed to %2$s.'),
873 htmlspecialchars($oldName),
874 htmlspecialchars($newName),
877 return true;
881 * Get all unique columns
883 * returns an array with all columns with unique content, in fact these are
884 * all columns being single indexed in PRIMARY or UNIQUE
886 * e.g.
887 * - PRIMARY(id) // id
888 * - UNIQUE(name) // name
889 * - PRIMARY(fk_id1, fk_id2) // NONE
890 * - UNIQUE(x,y) // NONE
892 * @param bool $backquoted whether to quote name with backticks ``
893 * @param bool $fullName whether to include full name of the table as a prefix
895 * @return string[]
897 public function getUniqueColumns(bool $backquoted = true, bool $fullName = true): array
899 $sql = QueryGenerator::getTableIndexesSql(
900 $this->getDbName(),
901 $this->getName(),
902 'Non_unique = 0',
904 $uniques = $this->dbi->fetchResult(
905 $sql,
906 ['Key_name', null],
907 'Column_name',
910 $return = [];
911 foreach ($uniques as $index) {
912 if (count($index) > 1) {
913 continue;
916 $possibleColumn = $fullName ? $this->getFullName($backquoted) . '.' : '';
918 if ($backquoted) {
919 $possibleColumn .= Util::backquote($index[0]);
920 } else {
921 $possibleColumn .= $index[0];
924 // a column might have a primary and an unique index on it
925 if (in_array($possibleColumn, $return, true)) {
926 continue;
929 $return[] = $possibleColumn;
932 return $return;
936 * Formats lists of columns
938 * returns an array with all columns that make use of an index
940 * e.g. index(col1, col2) would return col1, col2
942 * @param mixed[] $indexed column data
943 * @param bool $backquoted whether to quote name with backticks ``
944 * @param bool $fullName whether to include full name of the table as a prefix
946 * @return string[]
948 private function formatColumns(array $indexed, bool $backquoted, bool $fullName): array
950 $return = [];
951 foreach ($indexed as $column) {
952 $return[] = ($fullName ? $this->getFullName($backquoted) . '.' : '')
953 . ($backquoted ? Util::backquote($column) : $column);
956 return $return;
960 * Get all indexed columns
962 * returns an array with all columns that make use of an index
964 * e.g. index(col1, col2) would return col1, col2
966 * @param bool $backquoted whether to quote name with backticks ``
967 * @param bool $fullName whether to include full name of the table as a prefix
969 * @return string[]
971 public function getIndexedColumns(bool $backquoted = true, bool $fullName = true): array
973 $sql = QueryGenerator::getTableIndexesSql(
974 $this->getDbName(),
975 $this->getName(),
977 $indexed = $this->dbi->fetchResult($sql, 'Column_name', 'Column_name');
979 return $this->formatColumns($indexed, $backquoted, $fullName);
983 * Get all columns
985 * returns an array with all columns
987 * @param bool $backquoted whether to quote name with backticks ``
988 * @param bool $fullName whether to include full name of the table as a prefix
990 * @return string[]
992 public function getColumns(bool $backquoted = true, bool $fullName = true): array
994 $sql = 'SHOW COLUMNS FROM ' . $this->getFullName(true);
995 $indexed = $this->dbi->fetchResult($sql, 'Field', 'Field');
997 return $this->formatColumns($indexed, $backquoted, $fullName);
1001 * Get meta info for fields in table
1003 * @return FieldMetadata[]
1005 public function getColumnsMeta(): array
1007 $moveColumnsSqlQuery = sprintf(
1008 'SELECT * FROM %s.%s LIMIT 1',
1009 Util::backquote($this->dbName),
1010 Util::backquote($this->name),
1012 $moveColumnsSqlResult = $this->dbi->tryQuery($moveColumnsSqlQuery);
1013 if ($moveColumnsSqlResult !== false) {
1014 return $this->dbi->getFieldsMeta($moveColumnsSqlResult);
1017 // unsure how to reproduce but it was seen on the reporting server
1018 return [];
1022 * Get non-generated columns in table
1024 * @param bool $backquoted whether to quote name with backticks ``
1026 * @return string[]
1028 public function getNonGeneratedColumns(bool $backquoted = true): array
1030 $columnsMetaQuery = 'SHOW COLUMNS FROM ' . $this->getFullName(true);
1031 $ret = [];
1033 $columnsMetaQueryResult = $this->dbi->fetchResult($columnsMetaQuery);
1035 foreach ($columnsMetaQueryResult as $column) {
1036 $value = $column['Field'];
1037 if ($backquoted) {
1038 $value = Util::backquote($value);
1041 // If contains GENERATED or VIRTUAL and does not contain DEFAULT_GENERATED
1042 if (
1044 str_contains($column['Extra'], 'GENERATED')
1045 || str_contains($column['Extra'], 'VIRTUAL')
1046 ) && ! str_contains($column['Extra'], 'DEFAULT_GENERATED')
1048 continue;
1051 $ret[] = $value;
1054 return $ret;
1058 * Return UI preferences for this table from phpMyAdmin database.
1060 * @return mixed[]
1062 protected function getUiPrefsFromDb(UiPreferencesFeature|null $uiPreferencesFeature): array
1064 if ($uiPreferencesFeature === null) {
1065 return [];
1068 // Read from phpMyAdmin database
1069 $sqlQuery = sprintf(
1070 'SELECT `prefs` FROM %s.%s WHERE `username` = %s AND `db_name` = %s AND `table_name` = %s',
1071 Util::backquote($uiPreferencesFeature->database),
1072 Util::backquote($uiPreferencesFeature->tableUiPrefs),
1073 $this->dbi->quoteString(Config::getInstance()->selectedServer['user'], ConnectionType::ControlUser),
1074 $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser),
1075 $this->dbi->quoteString($this->name, ConnectionType::ControlUser),
1078 $value = $this->dbi->queryAsControlUser($sqlQuery)->fetchValue();
1079 if (is_string($value)) {
1080 return json_decode($value, true);
1083 return [];
1087 * Save this table's UI preferences into phpMyAdmin database.
1089 * @return true|Message
1091 protected function saveUiPrefsToDb(UiPreferencesFeature $uiPreferencesFeature): bool|Message
1093 $table = Util::backquote($uiPreferencesFeature->database) . '.'
1094 . Util::backquote($uiPreferencesFeature->tableUiPrefs);
1096 $config = Config::getInstance();
1097 $username = $config->selectedServer['user'];
1098 $sqlQuery = ' REPLACE INTO ' . $table
1099 . ' (username, db_name, table_name, prefs) VALUES ('
1100 . $this->dbi->quoteString($username, ConnectionType::ControlUser) . ', '
1101 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser) . ', '
1102 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser) . ', '
1103 . $this->dbi->quoteString((string) json_encode($this->uiprefs), ConnectionType::ControlUser) . ')';
1105 $success = $this->dbi->tryQuery($sqlQuery, ConnectionType::ControlUser);
1107 if (! $success) {
1108 $message = Message::error(
1109 __('Could not save table UI preferences!'),
1111 $message->addMessage(
1112 Message::rawError($this->dbi->getError(ConnectionType::ControlUser)),
1113 '<br><br>',
1116 return $message;
1119 // Remove some old rows in table_uiprefs if it exceeds the configured
1120 // maximum rows
1121 $sqlQuery = 'SELECT COUNT(*) FROM ' . $table;
1122 $rowsCount = (int) $this->dbi->fetchValue($sqlQuery);
1123 $maxRows = $config->selectedServer['MaxTableUiprefs'];
1124 if ($rowsCount > $maxRows) {
1125 $numRowsToDelete = $rowsCount - $maxRows;
1126 $sqlQuery = ' DELETE FROM ' . $table . ' ORDER BY last_update ASC LIMIT ' . $numRowsToDelete;
1127 $success = $this->dbi->tryQuery($sqlQuery, ConnectionType::ControlUser);
1129 if (! $success) {
1130 $message = Message::error(sprintf(
1131 __('Failed to cleanup table UI preferences (see $cfg[\'Servers\'][$i][\'MaxTableUiprefs\'] %s)'),
1132 MySQLDocumentation::showDocumentation('config', 'cfg_Servers_MaxTableUiprefs'),
1134 $message->addMessage(
1135 Message::rawError($this->dbi->getError(ConnectionType::ControlUser)),
1136 '<br><br>',
1139 return $message;
1143 return true;
1147 * Loads the UI preferences for this table.
1148 * If pmadb and table_uiprefs is set, it will load the UI preferences from
1149 * phpMyAdmin database.
1151 protected function loadUiPrefs(): void
1153 $serverId = Current::$server;
1155 // set session variable if it's still undefined
1156 if (! isset($_SESSION['tmpval']['table_uiprefs'][$serverId][$this->dbName][$this->name])) {
1157 // check whether we can get from pmadb
1158 $uiPrefs = $this->getUiPrefsFromDb($this->relation->getRelationParameters()->uiPreferencesFeature);
1159 $_SESSION['tmpval']['table_uiprefs'][$serverId][$this->dbName][$this->name] = $uiPrefs;
1162 $this->uiprefs =& $_SESSION['tmpval']['table_uiprefs'][$serverId][$this->dbName][$this->name];
1166 * Get a property from UI preferences.
1167 * Return false if the property is not found.
1169 public function getUiProp(UiProperty $property): mixed
1171 if ($this->uiprefs === []) {
1172 $this->loadUiPrefs();
1175 // do checking based on property
1176 if ($property === UiProperty::SortedColumn) {
1177 if (! isset($this->uiprefs[$property->value])) {
1178 return false;
1181 if (! isset($_POST['discard_remembered_sort'])) {
1182 // check if the column name exists in this table
1183 $tmp = explode(' ', $this->uiprefs[$property->value]);
1184 $colname = $tmp[0];
1185 //remove backquoting from colname
1186 $colname = str_replace('`', '', $colname);
1187 //get the available column name without backquoting
1188 $availColumns = $this->getColumns(false);
1190 foreach ($availColumns as $eachCol) {
1191 // check if $each_col ends with $colname
1192 if (substr_compare($eachCol, $colname, mb_strlen($eachCol) - mb_strlen($colname)) === 0) {
1193 return $this->uiprefs[$property->value];
1198 // remove the property, since it no longer exists in database
1199 $this->removeUiProp($property);
1201 return false;
1204 if ($this->isView() || ! isset($this->uiprefs[$property->value])) {
1205 return false;
1208 // check if the table has not been modified
1209 if ($this->getStatusInfo('Create_time') == $this->uiprefs['CREATE_TIME']) {
1210 return array_map(intval(...), $this->uiprefs[$property->value]);
1213 // remove the property, since the table has been modified
1214 $this->removeUiProp($property);
1216 return false;
1220 * Set a property from UI preferences.
1221 * If pmadb and table_uiprefs is set, it will save the UI preferences to
1222 * phpMyAdmin database.
1224 * @param int[]|string $value Value for the property
1225 * @param string|null $tableCreateTime Needed for PROP_COLUMN_ORDER and PROP_COLUMN_VISIB
1227 public function setUiProp(
1228 UiProperty $property,
1229 array|string $value,
1230 string|null $tableCreateTime = null,
1231 ): bool|Message {
1232 if ($this->uiprefs === []) {
1233 $this->loadUiPrefs();
1236 // we want to save the create time if the property is PROP_COLUMN_ORDER
1237 if (
1238 ! $this->isView()
1239 && ($property === UiProperty::ColumnOrder || $property === UiProperty::ColumnVisibility)
1241 $currCreateTime = $this->getStatusInfo('CREATE_TIME');
1242 if ($tableCreateTime === null || $tableCreateTime != $currCreateTime) {
1243 // there is no $table_create_time, or
1244 // supplied $table_create_time is older than current create time,
1245 // so don't save
1246 return Message::error(
1247 sprintf(
1249 'Cannot save UI property "%s". The changes made will ' .
1250 'not be persistent after you refresh this page. ' .
1251 'Please check if the table structure has been changed.',
1253 $property->value,
1258 $this->uiprefs['CREATE_TIME'] = $currCreateTime;
1261 // save the value
1262 $this->uiprefs[$property->value] = $value;
1264 // check if pmadb is set
1265 $uiPreferencesFeature = $this->relation->getRelationParameters()->uiPreferencesFeature;
1266 if ($uiPreferencesFeature !== null) {
1267 return $this->saveUiPrefsToDb($uiPreferencesFeature);
1270 return true;
1274 * Remove a property from UI preferences.
1276 * @return true|Message
1278 public function removeUiProp(UiProperty $property): bool|Message
1280 if ($this->uiprefs === []) {
1281 $this->loadUiPrefs();
1284 if (isset($this->uiprefs[$property->value])) {
1285 unset($this->uiprefs[$property->value]);
1287 // check if pmadb is set
1288 $uiPreferencesFeature = $this->relation->getRelationParameters()->uiPreferencesFeature;
1289 if ($uiPreferencesFeature !== null) {
1290 return $this->saveUiPrefsToDb($uiPreferencesFeature);
1294 return true;
1298 * Get all column names which are MySQL reserved words
1300 * @return string[]
1302 public function getReservedColumnNames(): array
1304 $columns = $this->getColumns(false);
1305 $return = [];
1306 foreach ($columns as $column) {
1307 $temp = explode('.', $column);
1308 $columnName = $temp[2];
1309 if (! Context::isKeyword($columnName, true)) {
1310 continue;
1313 $return[] = $columnName;
1316 return $return;
1320 * Function to get the name and type of the columns of a table
1322 * @return array<string, string>
1324 public function getNameAndTypeOfTheColumns(): array
1326 $columns = [];
1327 foreach (
1328 $this->dbi->getColumnsFull($this->dbName, $this->name) as $row
1330 if (preg_match('@^(set|enum)\((.+)\)$@i', $row['Type'], $tmp) === 1) {
1331 $tmp[2] = mb_substr(
1332 (string) preg_replace('@([^,])\'\'@', '\\1\\\'', ',' . $tmp[2]),
1335 $columns[$row['Field']] = $tmp[1] . '('
1336 . str_replace(',', ', ', $tmp[2]) . ')';
1337 } else {
1338 $columns[$row['Field']] = $row['Type'];
1342 return $columns;
1346 * Get index with index name
1348 * @param string $index Index name
1350 public function getIndex(string $index): Index
1352 return Index::singleton($this->dbi, $this->dbName, $this->name, $index);
1356 * Function to handle update for display field
1358 * @param string $displayField display field
1360 public function updateDisplayField(string $displayField, DisplayFeature $displayFeature): void
1362 if ($displayField === '') {
1363 $updQuery = 'DELETE FROM '
1364 . Util::backquote($displayFeature->database)
1365 . '.' . Util::backquote($displayFeature->tableInfo)
1366 . ' WHERE db_name = ' . $this->dbi->quoteString($this->dbName)
1367 . ' AND table_name = ' . $this->dbi->quoteString($this->name);
1368 } else {
1369 $updQuery = 'REPLACE INTO '
1370 . Util::backquote($displayFeature->database)
1371 . '.' . Util::backquote($displayFeature->tableInfo)
1372 . '(db_name, table_name, display_field) VALUES('
1373 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser) . ','
1374 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser) . ','
1375 . $this->dbi->quoteString($displayField, ConnectionType::ControlUser) . ')';
1378 $this->dbi->queryAsControlUser($updQuery);
1382 * Function to get update query for updating internal relations
1384 * @param mixed[] $multiEditColumnsName multi edit column names
1385 * @param mixed[] $destinationDb destination tables
1386 * @param mixed[] $destinationTable destination tables
1387 * @param mixed[] $destinationColumn destination columns
1388 * @param mixed[]|null $existrel db, table, column
1390 public function updateInternalRelations(
1391 array $multiEditColumnsName,
1392 array $destinationDb,
1393 array $destinationTable,
1394 array $destinationColumn,
1395 RelationFeature $relationFeature,
1396 array|null $existrel,
1397 ): bool {
1398 $updated = false;
1399 foreach ($destinationDb as $masterFieldMd5 => $foreignDb) {
1400 $updQuery = null;
1401 // Map the fieldname's md5 back to its real name
1402 $masterField = $multiEditColumnsName[$masterFieldMd5];
1403 $foreignTable = $destinationTable[$masterFieldMd5];
1404 $foreignField = $destinationColumn[$masterFieldMd5];
1405 if (! empty($foreignDb) && ! empty($foreignTable) && ! empty($foreignField)) {
1406 if (! isset($existrel[$masterField])) {
1407 $updQuery = 'INSERT INTO '
1408 . Util::backquote($relationFeature->database)
1409 . '.' . Util::backquote($relationFeature->relation)
1410 . '(master_db, master_table, master_field, foreign_db,'
1411 . ' foreign_table, foreign_field)'
1412 . ' values('
1413 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser) . ', '
1414 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser) . ', '
1415 . $this->dbi->quoteString($masterField, ConnectionType::ControlUser) . ', '
1416 . $this->dbi->quoteString($foreignDb, ConnectionType::ControlUser) . ', '
1417 . $this->dbi->quoteString($foreignTable, ConnectionType::ControlUser) . ','
1418 . $this->dbi->quoteString($foreignField, ConnectionType::ControlUser) . ')';
1419 } elseif (
1420 $existrel[$masterField]['foreign_db'] != $foreignDb
1421 || $existrel[$masterField]['foreign_table'] != $foreignTable
1422 || $existrel[$masterField]['foreign_field'] != $foreignField
1424 $updQuery = 'UPDATE '
1425 . Util::backquote($relationFeature->database)
1426 . '.' . Util::backquote($relationFeature->relation)
1427 . ' SET foreign_db = '
1428 . $this->dbi->quoteString($foreignDb, ConnectionType::ControlUser) . ', '
1429 . ' foreign_table = '
1430 . $this->dbi->quoteString($foreignTable, ConnectionType::ControlUser) . ', '
1431 . ' foreign_field = '
1432 . $this->dbi->quoteString($foreignField, ConnectionType::ControlUser) . ' '
1433 . ' WHERE master_db = '
1434 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser)
1435 . ' AND master_table = '
1436 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser)
1437 . ' AND master_field = '
1438 . $this->dbi->quoteString($masterField, ConnectionType::ControlUser);
1440 } elseif (isset($existrel[$masterField])) {
1441 $updQuery = 'DELETE FROM '
1442 . Util::backquote($relationFeature->database)
1443 . '.' . Util::backquote($relationFeature->relation)
1444 . ' WHERE master_db = '
1445 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser)
1446 . ' AND master_table = '
1447 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser)
1448 . ' AND master_field = '
1449 . $this->dbi->quoteString($masterField, ConnectionType::ControlUser);
1452 if (! isset($updQuery)) {
1453 continue;
1456 $this->dbi->queryAsControlUser($updQuery);
1457 $updated = true;
1460 return $updated;
1464 * Function to handle foreign key updates
1466 * @param mixed[] $destinationForeignDb destination foreign database
1467 * @param mixed[] $multiEditColumnsName multi edit column names
1468 * @param mixed[] $destinationForeignTable destination foreign table
1469 * @param mixed[] $destinationForeignColumn destination foreign column
1470 * @param string[] $optionsArray options array
1471 * @param string $table current table
1472 * @param mixed[] $existrelForeign db, table, column
1474 * @return array{string, string, string, bool}
1476 public function updateForeignKeys(
1477 array $destinationForeignDb,
1478 array $multiEditColumnsName,
1479 array $destinationForeignTable,
1480 array $destinationForeignColumn,
1481 array $optionsArray,
1482 string $table,
1483 array $existrelForeign,
1484 ): array {
1485 $htmlOutput = '';
1486 $previewSqlData = '';
1487 $displayQuery = '';
1488 $seenError = false;
1490 foreach ($destinationForeignDb as $masterFieldMd5 => $foreignDb) {
1491 $create = false;
1492 $drop = false;
1494 // Map the fieldname's md5 back to its real name
1495 $masterField = $multiEditColumnsName[$masterFieldMd5];
1497 $foreignTable = $destinationForeignTable[$masterFieldMd5];
1498 $foreignField = $destinationForeignColumn[$masterFieldMd5];
1500 $refDbName = $existrelForeign[$masterFieldMd5]['ref_db_name'] ?? Current::$database;
1502 $emptyFields = false;
1503 foreach ($masterField as $key => $oneField) {
1504 if (
1505 (! empty($oneField) && empty($foreignField[$key]))
1506 || (empty($oneField) && ! empty($foreignField[$key]))
1508 $emptyFields = true;
1511 if (! empty($oneField) || ! empty($foreignField[$key])) {
1512 continue;
1515 unset($masterField[$key], $foreignField[$key]);
1518 if (! empty($foreignDb) && ! empty($foreignTable) && ! $emptyFields) {
1519 if (isset($existrelForeign[$masterFieldMd5])) {
1520 $constraintName = $existrelForeign[$masterFieldMd5]['constraint'];
1521 $onDelete = ! empty(
1522 $existrelForeign[$masterFieldMd5]['on_delete']
1524 ? $existrelForeign[$masterFieldMd5]['on_delete']
1525 : 'RESTRICT';
1526 $onUpdate = ! empty(
1527 $existrelForeign[$masterFieldMd5]['on_update']
1529 ? $existrelForeign[$masterFieldMd5]['on_update']
1530 : 'RESTRICT';
1532 if (
1533 $refDbName != $foreignDb
1534 || $existrelForeign[$masterFieldMd5]['ref_table_name'] != $foreignTable
1535 || $existrelForeign[$masterFieldMd5]['ref_index_list'] != $foreignField
1536 || $existrelForeign[$masterFieldMd5]['index_list'] != $masterField
1537 || $_POST['constraint_name'][$masterFieldMd5] != $constraintName
1538 || ($_POST['on_delete'][$masterFieldMd5] != $onDelete)
1539 || ($_POST['on_update'][$masterFieldMd5] != $onUpdate)
1541 // another foreign key is already defined for this field
1542 // or an option has been changed for ON DELETE or ON UPDATE
1543 $drop = true;
1544 $create = true;
1546 } else {
1547 // no key defined for this field(s)
1548 $create = true;
1550 } elseif (isset($existrelForeign[$masterFieldMd5])) {
1551 $drop = true;
1554 if ($drop) {
1555 $dropQuery = 'ALTER TABLE ' . Util::backquote($table)
1556 . ' DROP FOREIGN KEY '
1557 . Util::backquote($existrelForeign[$masterFieldMd5]['constraint'])
1558 . ';';
1560 if (! isset($_POST['preview_sql'])) {
1561 $displayQuery .= $dropQuery . "\n";
1562 $this->dbi->tryQuery($dropQuery);
1563 $tmpErrorDrop = $this->dbi->getError();
1565 if ($tmpErrorDrop !== '') {
1566 $seenError = true;
1567 $htmlOutput .= Generator::mysqlDie($tmpErrorDrop, $dropQuery, false, '', false);
1568 continue;
1570 } else {
1571 $previewSqlData .= $dropQuery . "\n";
1575 $tmpErrorCreate = false;
1576 if (! $create) {
1577 continue;
1580 $createQuery = $this->getSQLToCreateForeignKey(
1581 $table,
1582 $masterField,
1583 $foreignDb,
1584 $foreignTable,
1585 $foreignField,
1586 $_POST['constraint_name'][$masterFieldMd5],
1587 $optionsArray[$_POST['on_delete'][$masterFieldMd5]],
1588 $optionsArray[$_POST['on_update'][$masterFieldMd5]],
1591 if (! isset($_POST['preview_sql'])) {
1592 $displayQuery .= $createQuery . "\n";
1593 $this->dbi->tryQuery($createQuery);
1594 $tmpErrorCreate = $this->dbi->getError();
1595 if ($tmpErrorCreate !== '') {
1596 $seenError = true;
1598 if (substr($tmpErrorCreate, 1, 4) == '1005') {
1599 $message = Message::error(
1601 'Error creating foreign key on %1$s (check data types)',
1604 $message->addParam(implode(', ', $masterField));
1605 $htmlOutput .= $message->getDisplay();
1606 } else {
1607 $htmlOutput .= Generator::mysqlDie($tmpErrorCreate, $createQuery, false, '', false);
1610 $htmlOutput .= MySQLDocumentation::show('create-table-foreign-keys') . "\n";
1612 } else {
1613 $previewSqlData .= $createQuery . "\n";
1616 // this is an alteration and the old constraint has been dropped
1617 // without creation of a new one
1618 if (! $drop || $tmpErrorCreate === '' || $tmpErrorCreate === false) {
1619 continue;
1622 // a rollback may be better here
1623 $sqlQueryRecreate = '# Restoring the dropped constraint...' . "\n";
1624 $sqlQueryRecreate .= $this->getSQLToCreateForeignKey(
1625 $table,
1626 $masterField,
1627 $existrelForeign[$masterFieldMd5]['ref_db_name'],
1628 $existrelForeign[$masterFieldMd5]['ref_table_name'],
1629 $existrelForeign[$masterFieldMd5]['ref_index_list'],
1630 $existrelForeign[$masterFieldMd5]['constraint'],
1631 $optionsArray[$existrelForeign[$masterFieldMd5]['on_delete'] ?? ''] ?? null,
1632 $optionsArray[$existrelForeign[$masterFieldMd5]['on_update'] ?? ''] ?? null,
1634 if (! isset($_POST['preview_sql'])) {
1635 $displayQuery .= $sqlQueryRecreate . "\n";
1636 $this->dbi->tryQuery($sqlQueryRecreate);
1637 } else {
1638 $previewSqlData .= $sqlQueryRecreate;
1642 return [$htmlOutput, $previewSqlData, $displayQuery, $seenError];
1646 * Returns the SQL query for foreign key constraint creation
1648 * @param string $table table name
1649 * @param mixed[] $field field names
1650 * @param string $foreignDb foreign database name
1651 * @param string $foreignTable foreign table name
1652 * @param mixed[] $foreignField foreign field names
1653 * @param string|null $name name of the constraint
1654 * @param string|null $onDelete on delete action
1655 * @param string|null $onUpdate on update action
1657 * @return string SQL query for foreign key constraint creation
1659 private function getSQLToCreateForeignKey(
1660 string $table,
1661 array $field,
1662 string $foreignDb,
1663 string $foreignTable,
1664 array $foreignField,
1665 string|null $name = null,
1666 string|null $onDelete = null,
1667 string|null $onUpdate = null,
1668 ): string {
1669 $sqlQuery = 'ALTER TABLE ' . Util::backquote($table) . ' ADD ';
1670 // if user entered a constraint name
1671 if ($name !== null && $name !== '') {
1672 $sqlQuery .= ' CONSTRAINT ' . Util::backquote($name);
1675 foreach ($field as $key => $oneField) {
1676 $field[$key] = Util::backquote($oneField);
1679 foreach ($foreignField as $key => $oneField) {
1680 $foreignField[$key] = Util::backquote($oneField);
1683 $sqlQuery .= ' FOREIGN KEY (' . implode(', ', $field) . ') REFERENCES '
1684 . ($this->dbName !== $foreignDb
1685 ? Util::backquote($foreignDb) . '.' : '')
1686 . Util::backquote($foreignTable)
1687 . '(' . implode(', ', $foreignField) . ')';
1689 if ($onDelete !== null && $onDelete !== '') {
1690 $sqlQuery .= ' ON DELETE ' . $onDelete;
1693 if ($onUpdate !== null && $onUpdate !== '') {
1694 $sqlQuery .= ' ON UPDATE ' . $onUpdate;
1697 $sqlQuery .= ';';
1699 return $sqlQuery;
1703 * Returns the generation expression for virtual columns
1705 * @param string|null $column name of the column
1707 * @return mixed[]|bool associative array of column name and their expressions
1708 * or false on failure
1710 public function getColumnGenerationExpression(string|null $column = null): array|bool
1712 if (
1713 Compatibility::isMySqlOrPerconaDb()
1714 && $this->dbi->getVersion() > 50705
1715 && ! Config::getInstance()->selectedServer['DisableIS']
1717 $sql = 'SELECT
1718 `COLUMN_NAME` AS `Field`,
1719 `GENERATION_EXPRESSION` AS `Expression`
1720 FROM
1721 `information_schema`.`COLUMNS`
1722 WHERE
1723 `TABLE_SCHEMA` = ' . $this->dbi->quoteString($this->dbName) . '
1724 AND `TABLE_NAME` = ' . $this->dbi->quoteString($this->name);
1725 if ($column !== null) {
1726 $sql .= ' AND `COLUMN_NAME` = ' . $this->dbi->quoteString($column);
1729 return $this->dbi->fetchResult($sql, 'Field', 'Expression');
1732 $createTable = $this->showCreate();
1733 if ($createTable === '') {
1734 return false;
1737 $parser = new Parser($createTable);
1738 $stmt = $parser->statements[0];
1739 $fields = [];
1740 if ($stmt instanceof CreateStatement) {
1741 $fields = TableUtils::getFields($stmt);
1744 if ($column != null) {
1745 $expression = isset($fields[$column]['expr']) ? substr($fields[$column]['expr'], 1, -1) : '';
1747 return [$column => $expression];
1750 $ret = [];
1751 foreach ($fields as $field => $options) {
1752 if (! isset($options['expr'])) {
1753 continue;
1756 $ret[$field] = substr($options['expr'], 1, -1);
1759 return $ret;
1763 * Returns the CREATE statement for this table
1765 public function showCreate(): string
1767 return (string) $this->dbi->fetchValue(
1768 'SHOW CREATE TABLE ' . Util::backquote($this->dbName) . '.'
1769 . Util::backquote($this->name),
1775 * Returns the real row count for a table
1777 public function getRealRowCountTable(): int|null
1779 // SQL query to get row count for a table.
1780 $result = $this->dbi->fetchSingleRow(
1781 sprintf(
1782 'SELECT COUNT(*) AS %s FROM %s.%s',
1783 Util::backquote('row_count'),
1784 Util::backquote($this->dbName),
1785 Util::backquote($this->name),
1789 if (! is_array($result)) {
1790 return null;
1793 return (int) $result['row_count'];
1797 * Get columns with indexes
1799 * @param int $types types bitmask
1801 * @return (string|int)[] an array of columns
1803 public function getColumnsWithIndex(int $types): array
1805 $columnsWithIndex = [];
1806 foreach (
1807 Index::getFromTableByChoice($this->name, $this->dbName, $types) as $index
1809 $columns = $index->getColumns();
1810 $columnsWithIndex = array_merge($columnsWithIndex, array_keys($columns));
1813 return $columnsWithIndex;