Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / Table / Table.php
blob768636ec5e470066f84f6075fbcb3030d474c28c
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\Plugins;
21 use PhpMyAdmin\Plugins\Export\ExportSql;
22 use PhpMyAdmin\Query\Compatibility;
23 use PhpMyAdmin\Query\Generator as QueryGenerator;
24 use PhpMyAdmin\SqlParser\Components\Expression;
25 use PhpMyAdmin\SqlParser\Components\OptionsArray;
26 use PhpMyAdmin\SqlParser\Context;
27 use PhpMyAdmin\SqlParser\Parser;
28 use PhpMyAdmin\SqlParser\Statements\AlterStatement;
29 use PhpMyAdmin\SqlParser\Statements\CreateStatement;
30 use PhpMyAdmin\SqlParser\Statements\DropStatement;
31 use PhpMyAdmin\SqlParser\Utils\Table as TableUtils;
32 use PhpMyAdmin\Triggers\Triggers;
33 use PhpMyAdmin\Util;
34 use Stringable;
36 use function __;
37 use function array_keys;
38 use function array_map;
39 use function array_merge;
40 use function count;
41 use function end;
42 use function explode;
43 use function htmlspecialchars;
44 use function implode;
45 use function in_array;
46 use function intval;
47 use function is_array;
48 use function is_numeric;
49 use function is_string;
50 use function json_decode;
51 use function json_encode;
52 use function mb_stripos;
53 use function mb_strlen;
54 use function mb_substr;
55 use function preg_match;
56 use function preg_replace;
57 use function rtrim;
58 use function sprintf;
59 use function str_contains;
60 use function str_replace;
61 use function stripos;
62 use function strtolower;
63 use function strtoupper;
64 use function substr;
65 use function substr_compare;
66 use function trim;
68 /**
69 * Handles everything related to tables
71 * @todo make use of Message and Error
73 class Table implements Stringable
75 /**
76 * UI preferences properties
78 public const PROP_SORTED_COLUMN = 'sorted_col';
79 public const PROP_COLUMN_ORDER = 'col_order';
80 public const PROP_COLUMN_VISIB = 'col_visib';
82 /** @var mixed[] UI preferences */
83 public array $uiprefs = [];
85 /** @var mixed[] errors occurred */
86 public array $errors = [];
88 /** @var mixed[] messages */
89 public array $messages = [];
91 private Relation $relation;
93 /**
94 * @param string $name table name
95 * @param string $dbName database name
96 * @param DatabaseInterface $dbi database interface for the table
98 public function __construct(protected string $name, protected string $dbName, protected DatabaseInterface $dbi)
100 $this->relation = new Relation($this->dbi);
104 * returns table name
106 * @see Table::getName()
108 public function __toString(): string
110 return $this->getName();
114 * Table getter
116 * @param string $tableName table name
117 * @param string $dbName database name
118 * @param DatabaseInterface $dbi database interface for the table
120 public static function get(string $tableName, string $dbName, DatabaseInterface $dbi): Table
122 return new Table($tableName, $dbName, $dbi);
126 * return the last error
128 * @return string the last error
130 public function getLastError(): string
132 if ($this->errors === []) {
133 return '';
136 return end($this->errors);
140 * return the last message
142 * @return string the last message
144 public function getLastMessage(): string
146 if ($this->messages === []) {
147 return '';
150 return end($this->messages);
154 * returns table name
156 * @param bool $backquoted whether to quote name with backticks ``
158 * @return string table name
160 public function getName(bool $backquoted = false): string
162 if ($backquoted) {
163 return Util::backquote($this->name);
166 return $this->name;
170 * returns database name for this table
172 * @param bool $backquoted whether to quote name with backticks ``
174 * @return string database name for this table
176 public function getDbName(bool $backquoted = false): string
178 if ($backquoted) {
179 return Util::backquote($this->dbName);
182 return $this->dbName;
186 * returns full name for table, including database name
188 * @param bool $backquoted whether to quote name with backticks ``
190 public function getFullName(bool $backquoted = false): string
192 return $this->getDbName($backquoted) . '.'
193 . $this->getName($backquoted);
197 * Checks the storage engine used to create table
199 * @param string[]|string $engine Checks the table engine against an
200 * array of engine strings or a single string, should be uppercase
202 public function isEngine(array|string $engine): bool
204 $engine = (array) $engine;
205 $tableStorageEngine = $this->getStorageEngine();
207 return in_array($tableStorageEngine, $engine, true);
211 * returns whether the table is actually a view
213 public function isView(): bool
215 if ($this->dbName === '' || $this->name === '') {
216 return false;
219 // use cached data or load information with SHOW command
220 $type = $this->dbi->getCache()->getCachedTableContent($this->dbName, $this->name, 'TABLE_TYPE');
221 if ($type === null && Config::getInstance()->selectedServer['DisableIS']) {
222 $type = $this->getStatusInfo('TABLE_TYPE');
225 if ($type !== null) {
226 return $type === 'VIEW' || $type === 'SYSTEM VIEW';
229 // information_schema tables are 'SYSTEM VIEW's
230 if ($this->dbName === 'information_schema') {
231 return true;
234 // query information_schema
235 return (bool) $this->dbi->fetchValue(
236 'SELECT 1'
237 . ' FROM information_schema.VIEWS'
238 . ' WHERE TABLE_SCHEMA = ' . $this->dbi->quoteString($this->dbName)
239 . ' AND TABLE_NAME = ' . $this->dbi->quoteString($this->name),
244 * Returns whether the table is actually an updatable view
246 public function isUpdatableView(): bool
248 if ($this->dbName === '' || $this->name === '') {
249 return false;
252 return (bool) $this->dbi->fetchValue(
253 'SELECT 1'
254 . ' FROM information_schema.VIEWS'
255 . ' WHERE TABLE_SCHEMA = ' . $this->dbi->quoteString($this->dbName)
256 . ' AND TABLE_NAME = ' . $this->dbi->quoteString($this->name)
257 . ' AND IS_UPDATABLE = \'YES\'',
262 * Checks if this is a merge table
264 * If the ENGINE of the table is MERGE or MRG_MYISAM (alias),
265 * this is a merge table.
267 public function isMerge(): bool
269 return $this->isEngine(['MERGE', 'MRG_MYISAM']);
273 * Returns full table status info, or specific if $info provided
274 * this info is collected from information_schema
276 * @param T $info specific information to be fetched
278 * @return (T is null ? (string|int|null)[]|null : (string|int|null))
280 * @template T of string|null
282 public function getStatusInfo(string|null $info = null): array|string|int|null
284 $cachedResult = $this->dbi->getCache()->getCachedTableContent($this->dbName, $this->name);
286 // sometimes there is only one entry (ExactRows) so
287 // we have to get the table's details
288 if ($cachedResult === null || count($cachedResult) === 1) {
289 $this->dbi->getTablesFull($this->dbName, $this->name);
290 $cachedResult = $this->dbi->getCache()->getCachedTableContent($this->dbName, $this->name);
293 if ($cachedResult === null) {
294 // happens when we enter the table creation dialog
295 // or when we really did not get any status info, for example
296 // when $table === 'TABLE_NAMES' after the user tried SHOW TABLES
297 return null;
300 if ($info === null) {
301 return $cachedResult;
304 return $cachedResult[$info];
308 * Returns the Table storage Engine for current table.
310 * @return string Return storage engine info if it is set for
311 * the selected table else return blank.
313 public function getStorageEngine(): string
315 $tableStorageEngine = $this->getStatusInfo('ENGINE');
317 return strtoupper((string) $tableStorageEngine);
321 * Returns the comments for current table.
323 * @return string Return comment info if it is set for the selected table or return blank.
325 public function getComment(): string
327 return $this->getStatusInfo('TABLE_COMMENT') ?? '';
331 * Returns the collation for current table.
333 * @return string Return blank if collation is empty else return the collation info from table info.
335 public function getCollation(): string
337 return $this->getStatusInfo('TABLE_COLLATION') ?? '';
341 * Returns the info about no of rows for current table.
343 * @return int Return no of rows info if it is not null for the selected table or return 0.
345 public function getNumRows(): int
347 return (int) $this->getStatusInfo('TABLE_ROWS');
351 * Returns the Row format for current table.
353 * @return string Return table row format info if it is set for the selected table or return blank.
355 public function getRowFormat(): string
357 $tableRowFormat = $this->getStatusInfo('ROW_FORMAT');
359 return is_string($tableRowFormat) ? $tableRowFormat : '';
363 * Returns the auto increment option for current table.
365 * @return string Return auto increment info if it is set for the selected table or return blank.
367 public function getAutoIncrement(): string
369 $tableAutoIncrement = $this->getStatusInfo('AUTO_INCREMENT');
371 return $tableAutoIncrement ?? '';
375 * Returns the array for CREATE statement for current table.
377 * @return array<string, string> Return options array info if it is set for the selected table or return blank.
379 public function getCreateOptions(): array
381 $tableOptions = $this->getStatusInfo('CREATE_OPTIONS');
382 $createOptionsTmp = is_string($tableOptions) && $tableOptions !== '' ? explode(' ', $tableOptions) : [];
383 $createOptions = [];
384 // export create options by its name as variables into global namespace
385 // f.e. pack_keys=1 becomes available as $pack_keys with value of '1'
386 // unset($pack_keys);
387 foreach ($createOptionsTmp as $eachCreateOption) {
388 $eachCreateOption = explode('=', $eachCreateOption);
389 if (! isset($eachCreateOption[1])) {
390 continue;
393 // ensure there is no ambiguity for PHP 5 and 7
394 $createOptions[$eachCreateOption[0]] = $eachCreateOption[1];
397 // we need explicit DEFAULT value here (different from '0')
398 $hasPackKeys = isset($createOptions['pack_keys']) && $createOptions['pack_keys'] !== '';
399 $createOptions['pack_keys'] = $hasPackKeys ? $createOptions['pack_keys'] : 'DEFAULT';
401 return $createOptions;
405 * generates column specification for ALTER or CREATE TABLE syntax
407 * @param string $name name
408 * @param string $type type ('INT', 'VARCHAR', 'BIT', ...)
409 * @param string $length length ('2', '5,2', '', ...)
410 * @param string $attribute attribute
411 * @param string $collation collation
412 * @param bool|string $null with 'NULL' or 'NOT NULL'
413 * @param string $defaultType whether default is CURRENT_TIMESTAMP,
414 * NULL, NONE, USER_DEFINED, UUID
415 * @param string $defaultValue default value for USER_DEFINED
416 * default type
417 * @param string $extra 'AUTO_INCREMENT'
418 * @param string $comment field comment
419 * @param string $virtuality virtuality of the column
420 * @param string $expression expression for the virtual column
421 * @param string $moveTo new position for column
422 * @param (string|int)[]|null $columnsWithIndex Fields having PRIMARY or UNIQUE KEY indexes
423 * @param string|null $oldColumnName Old column name
425 * @return string field specification
427 * @todo move into class PMA_Column
428 * @todo on the interface, some js to clear the default value when the
429 * default current_timestamp is checked
431 public static function generateFieldSpec(
432 string $name,
433 string $type,
434 string $length = '',
435 string $attribute = '',
436 string $collation = '',
437 bool|string $null = false,
438 string $defaultType = 'USER_DEFINED',
439 string $defaultValue = '',
440 string $extra = '',
441 string $comment = '',
442 string $virtuality = '',
443 string $expression = '',
444 string $moveTo = '',
445 array|null $columnsWithIndex = null,
446 string|null $oldColumnName = null,
447 ): string {
448 $isTimestamp = mb_stripos($type, 'TIMESTAMP') !== false;
450 $query = Util::backquote($name) . ' ' . $type;
452 // allow the possibility of a length for TIME, DATETIME and TIMESTAMP
453 // (will work on MySQL >= 5.6.4)
455 // MySQL permits a non-standard syntax for FLOAT and DOUBLE,
456 // see https://dev.mysql.com/doc/refman/5.5/en/floating-point-types.html
457 $pattern = '@^(DATE|TINYBLOB|TINYTEXT|BLOB|TEXT|'
458 . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN|UUID|JSON)$@i';
459 $dbi = DatabaseInterface::getInstance();
460 if (
461 $length !== ''
462 && ! preg_match($pattern, $type)
463 && Compatibility::isIntegersSupportLength($type, $length, $dbi)
465 // Note: The variable $length here can contain several other things
466 // besides length - ENUM/SET value or length of DECIMAL (eg. 12,3)
467 // so we can't just convert it to integer
468 $query .= '(' . $length . ')';
471 if ($attribute !== '') {
472 $query .= ' ' . $attribute;
474 if ($isTimestamp && stripos($attribute, 'TIMESTAMP') !== false && $length !== '') {
475 $query .= '(' . $length . ')';
479 // if column is virtual, check if server type is Mysql as only Mysql server
480 // supports extra column properties
481 $isVirtualColMysql = $virtuality && Compatibility::isMySqlOrPerconaDb();
482 // if column is virtual, check if server type is MariaDB as MariaDB server
483 // supports no extra virtual column properties except CHARACTER SET for text column types
484 $isVirtualColMariaDB = $virtuality && Compatibility::isMariaDb();
486 $matches = preg_match('@^(TINYTEXT|TEXT|MEDIUMTEXT|LONGTEXT|VARCHAR|CHAR|ENUM|SET)$@i', $type);
487 if ($collation !== '' && $collation !== 'NULL' && $matches) {
488 $query .= Util::getCharsetQueryPart(
489 $isVirtualColMariaDB ? (string) preg_replace('~_.+~s', '', $collation) : $collation,
490 true,
494 if ($virtuality !== '') {
495 $query .= ' AS (' . $expression . ') ' . $virtuality;
498 if ($virtuality === '' || $isVirtualColMysql) {
499 if ($null !== false) {
500 if ($null === 'YES') {
501 $query .= ' NULL';
502 } else {
503 $query .= ' NOT NULL';
507 if ($virtuality === '') {
508 switch ($defaultType) {
509 case 'USER_DEFINED':
510 if ($isTimestamp && $defaultValue === '0') {
511 // a TIMESTAMP does not accept DEFAULT '0'
512 // but DEFAULT 0 works
513 $query .= ' DEFAULT 0';
514 } elseif (
515 $isTimestamp
516 && preg_match('/^\'\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d(\.\d{1,6})?\'$/', $defaultValue)
518 $query .= ' DEFAULT ' . $defaultValue;
519 } elseif ($type === 'BIT') {
520 $query .= ' DEFAULT b\''
521 . preg_replace('/[^01]/', '0', $defaultValue)
522 . '\'';
523 } elseif ($type === 'BOOLEAN') {
524 if (preg_match('/^1|T|TRUE|YES$/i', $defaultValue)) {
525 $query .= ' DEFAULT TRUE';
526 } elseif (preg_match('/^0|F|FALSE|NO$/i', $defaultValue)) {
527 $query .= ' DEFAULT FALSE';
528 } else {
529 // Invalid BOOLEAN value
530 $query .= ' DEFAULT ' . $dbi->quoteString($defaultValue);
532 } elseif ($type === 'BINARY' || $type === 'VARBINARY') {
533 $query .= ' DEFAULT 0x' . $defaultValue;
534 } else {
535 $query .= ' DEFAULT ' . $dbi->quoteString($defaultValue);
538 break;
539 /** @noinspection PhpMissingBreakStatementInspection */
540 case 'NULL':
541 // If user uncheck null checkbox and not change default value null,
542 // default value will be ignored.
543 if ($null !== false && $null !== 'YES') {
544 break;
546 // else fall-through intended, no break here
547 case 'CURRENT_TIMESTAMP':
548 case 'current_timestamp()':
549 $query .= ' DEFAULT ' . $defaultType;
551 if (
552 $length !== ''
553 && $isTimestamp
554 && $defaultType !== 'NULL' // Not to be added in case of NULL
556 $query .= '(' . $length . ')';
559 break;
560 case 'UUID':
561 case 'uuid()':
562 $query .= ' DEFAULT uuid()';
564 break;
565 case 'NONE':
566 default:
567 break;
571 if ($extra !== '') {
572 if ($virtuality !== '') {
573 $extra = trim((string) preg_replace('~^\s*AUTO_INCREMENT\s*~is', ' ', $extra));
576 $query .= ' ' . $extra;
580 if ($comment !== '') {
581 $query .= ' COMMENT ' . $dbi->quoteString($comment);
584 // move column
585 if ($moveTo === '-first') { // dash can't appear as part of column name
586 $query .= ' FIRST';
587 } elseif ($moveTo !== '') {
588 $query .= ' AFTER ' . Util::backquote($moveTo);
591 if ($virtuality === '' && $extra !== '') {
592 if ($oldColumnName === null) {
593 if (is_array($columnsWithIndex) && ! in_array($name, $columnsWithIndex)) {
594 $query .= ', add PRIMARY KEY (' . Util::backquote($name) . ')';
596 } elseif (is_array($columnsWithIndex) && ! in_array($oldColumnName, $columnsWithIndex)) {
597 $query .= ', add PRIMARY KEY (' . Util::backquote($name) . ')';
601 return $query;
605 * Checks if the number of records in a table is at least equal to
606 * $min_records
608 * @param int $minRecords Number of records to check for in a table
610 public function checkIfMinRecordsExist(int $minRecords = 0): bool
612 $checkQuery = 'SELECT ';
614 $uniqueFields = $this->getUniqueColumns(true, false);
615 if ($uniqueFields !== []) {
616 $fieldsToSelect = implode(', ', $uniqueFields);
617 } else {
618 $indexedCols = $this->getIndexedColumns(true, false);
619 if ($indexedCols !== []) {
620 $fieldsToSelect = implode(', ', $indexedCols);
621 } else {
622 $fieldsToSelect = '*';
626 $checkQuery .= $fieldsToSelect
627 . ' FROM ' . $this->getFullName(true)
628 . ' LIMIT ' . $minRecords;
630 $res = $this->dbi->tryQuery($checkQuery);
632 if ($res !== false) {
633 $numRecords = $res->numRows();
634 if ($numRecords >= $minRecords) {
635 return true;
639 return false;
643 * Counts the number of records in a table
645 * @param bool $forceExact whether to force an exact count
647 public function countRecords(bool $forceExact = false): int
649 $isView = $this->isView();
650 $cache = $this->dbi->getCache();
652 $exactRowsCached = $cache->getCachedTableContent($this->dbName, $this->name, 'ExactRows');
653 if ($exactRowsCached !== null) {
654 return (int) $exactRowsCached;
657 $rowCount = null;
659 if (! $forceExact) {
660 if ($cache->getCachedTableContent($this->dbName, $this->name, 'Rows') === null && ! $isView) {
661 $this->dbi->getTablesFull($this->dbName, $this->name);
664 $rowCount = $cache->getCachedTableContent($this->dbName, $this->name, 'Rows');
667 // for a VIEW, $row_count is always false at this point
668 $config = Config::getInstance();
669 if ($rowCount !== null && $rowCount >= $config->settings['MaxExactCount']) {
670 return (int) $rowCount;
673 if (! $isView) {
674 $rowCount = $this->dbi->fetchValue(
675 'SELECT COUNT(*) FROM ' . Util::backquote($this->dbName) . '.' . Util::backquote($this->name),
677 } elseif ($config->settings['MaxExactCountViews'] == 0) {
678 // For complex views, even trying to get a partial record
679 // count could bring down a server, so we offer an
680 // alternative: setting MaxExactCountViews to 0 will bypass
681 // completely the record counting for views
682 $rowCount = false;
683 } else {
684 // Counting all rows of a VIEW could be too long,
685 // so use a LIMIT clause.
686 // Use try_query because it can fail (when a VIEW is
687 // based on a table that no longer exists)
688 $result = $this->dbi->tryQuery(
689 'SELECT 1 FROM ' . Util::backquote($this->dbName) . '.'
690 . Util::backquote($this->name) . ' LIMIT '
691 . $config->settings['MaxExactCountViews'],
693 if ($result) {
694 $rowCount = $result->numRows();
698 if (is_numeric($rowCount)) {
699 $cache->cacheTableValue($this->dbName, $this->name, 'ExactRows', (int) $rowCount);
701 return (int) $rowCount;
704 return 0;
708 * Generates column specification for ALTER syntax
710 * @see Table::generateFieldSpec()
712 * @param string $oldcol old column name
713 * @param string $newcol new column name
714 * @param string $type type ('INT', 'VARCHAR', 'BIT', ...)
715 * @param string $length length ('2', '5,2', '', ...)
716 * @param string $attribute attribute
717 * @param string $collation collation
718 * @param bool|string $null with 'NULL' or 'NOT NULL'
719 * @param string $defaultType whether default is CURRENT_TIMESTAMP,
720 * NULL, NONE, USER_DEFINED
721 * @param string $defaultValue default value for USER_DEFINED default
722 * type
723 * @param string $extra 'AUTO_INCREMENT'
724 * @param string $comment field comment
725 * @param string $virtuality virtuality of the column
726 * @param string $expression expression for the virtual column
727 * @param string $moveTo new position for column
728 * @param (string|int)[]|null $columnsWithIndex Fields having PRIMARY or UNIQUE KEY indexes
730 * @return string field specification
732 public static function generateAlter(
733 string $oldcol,
734 string $newcol,
735 string $type,
736 string $length,
737 string $attribute,
738 string $collation,
739 bool|string $null,
740 string $defaultType,
741 string $defaultValue,
742 string $extra,
743 string $comment,
744 string $virtuality,
745 string $expression,
746 string $moveTo,
747 array|null $columnsWithIndex = null,
748 ): string {
749 return Util::backquote($oldcol) . ' '
750 . self::generateFieldSpec(
751 $newcol,
752 $type,
753 $length,
754 $attribute,
755 $collation,
756 $null,
757 $defaultType,
758 $defaultValue,
759 $extra,
760 $comment,
761 $virtuality,
762 $expression,
763 $moveTo,
764 $columnsWithIndex,
765 $oldcol,
770 * Inserts existing entries in a PMA_* table by reading a value from an old
771 * entry
773 * @param string $work The array index, which Relation feature to check ('relwork', 'commwork', ...)
774 * @param string $table The array index, which PMA-table to update ('bookmark', 'relation', ...)
775 * @param string[] $getFields Which fields will be SELECT'ed from the old entry
776 * @param mixed[] $whereFields Which fields will be used for the WHERE query (array('FIELDNAME' => 'FIELDVALUE'))
777 * @param mixed[] $newFields Which fields will be used as new VALUES. These are the important keys which differ
778 * from the old entry (array('FIELDNAME' => 'NEW FIELDVALUE'))
780 public static function duplicateInfo(
781 string $work,
782 string $table,
783 array $getFields,
784 array $whereFields,
785 array $newFields,
786 ): int|bool {
787 $dbi = DatabaseInterface::getInstance();
788 $relation = new Relation($dbi);
789 $relationParameters = $relation->getRelationParameters();
790 $relationParams = $relationParameters->toArray();
791 $lastId = -1;
793 if (! isset($relationParams[$work], $relationParams[$table]) || ! $relationParams[$work]) {
794 return true;
797 $selectParts = [];
798 $rowFields = [];
799 foreach ($getFields as $getField) {
800 $selectParts[] = Util::backquote($getField);
801 $rowFields[] = $getField;
804 $whereParts = [];
805 foreach ($whereFields as $where => $value) {
806 $whereParts[] = Util::backquote((string) $where) . ' = '
807 . $dbi->quoteString((string) $value, ConnectionType::ControlUser);
810 $newParts = [];
811 $newValueParts = [];
812 foreach ($newFields as $where => $value) {
813 $newParts[] = Util::backquote((string) $where);
814 $newValueParts[] = $dbi->quoteString((string) $value, ConnectionType::ControlUser);
817 $tableCopyQuery = '
818 SELECT ' . implode(', ', $selectParts) . '
819 FROM ' . Util::backquote($relationParameters->db) . '.'
820 . Util::backquote((string) $relationParams[$table]) . '
821 WHERE ' . implode(' AND ', $whereParts);
823 // must use DatabaseInterface::QUERY_BUFFERED here, since we execute
824 // another query inside the loop
825 $tableCopyRs = $dbi->queryAsControlUser($tableCopyQuery);
827 foreach ($tableCopyRs as $tableCopyRow) {
828 $valueParts = [];
829 foreach ($tableCopyRow as $key => $val) {
830 if (! in_array($key, $rowFields)) {
831 continue;
834 $valueParts[] = $dbi->quoteString($val, ConnectionType::ControlUser);
837 $newTableQuery = 'INSERT IGNORE INTO '
838 . Util::backquote($relationParameters->db)
839 . '.' . Util::backquote((string) $relationParams[$table])
840 . ' (' . implode(', ', $selectParts) . ', '
841 . implode(', ', $newParts) . ') VALUES ('
842 . implode(', ', $valueParts) . ', '
843 . implode(', ', $newValueParts) . ')';
845 $dbi->queryAsControlUser($newTableQuery);
846 $lastId = $dbi->insertId();
849 return $lastId;
853 * Copies or renames table
855 * @param string $sourceDb source database
856 * @param string $sourceTable source table
857 * @param string $targetDb target database
858 * @param string $targetTable target table
859 * @param string $what what to be moved or copied (data, dataonly)
860 * @param bool $move whether to move
861 * @param string $mode mode
863 public static function moveCopy(
864 string $sourceDb,
865 string $sourceTable,
866 string $targetDb,
867 string $targetTable,
868 string $what,
869 bool $move,
870 string $mode,
871 bool $addDropIfExists,
872 ): bool {
873 $GLOBALS['errorUrl'] ??= null;
874 $dbi = DatabaseInterface::getInstance();
875 $relation = new Relation($dbi);
877 // Try moving the tables directly, using native `RENAME` statement.
878 if ($move && $what === 'data') {
879 $tbl = new Table($sourceTable, $sourceDb, $dbi);
880 if ($tbl->rename($targetTable, $targetDb)) {
881 $GLOBALS['message'] = $tbl->getLastMessage();
883 return true;
887 // Setting required export settings.
888 $GLOBALS['asfile'] = 1;
890 // Ensuring the target database is valid.
891 $databaseList = $dbi->getDatabaseList();
892 if (! $databaseList->exists($sourceDb, $targetDb)) {
893 if (! $databaseList->exists($sourceDb)) {
894 $GLOBALS['message'] = Message::rawError(
895 sprintf(
896 __('Source database `%s` was not found!'),
897 htmlspecialchars($sourceDb),
902 if (! $databaseList->exists($targetDb)) {
903 $GLOBALS['message'] = Message::rawError(
904 sprintf(
905 __('Target database `%s` was not found!'),
906 htmlspecialchars($targetDb),
911 return false;
915 * The full name of source table, quoted.
917 $source = Util::backquote($sourceDb) . '.' . Util::backquote($sourceTable);
919 // If the target database is not specified, the operation is taking
920 // place in the same database.
921 if ($targetDb === '') {
922 $targetDb = $sourceDb;
925 // Selecting the database could avoid some problems with replicated
926 // databases, when moving table from replicated one to not replicated one.
927 $dbi->selectDb($targetDb);
930 * The full name of target table, quoted.
932 $target = Util::backquote($targetDb) . '.' . Util::backquote($targetTable);
934 // No table is created when this is a data-only operation.
935 if ($what !== 'dataonly') {
937 * Instance used for exporting the current structure of the table.
939 * @var ExportSql $exportSqlPlugin
941 $exportSqlPlugin = Plugins::getPlugin('export', 'sql', [
942 'export_type' => 'table',
943 'single_table' => false,
945 // It is better that all identifiers are quoted
946 $exportSqlPlugin->useSqlBackquotes(true);
948 $noConstraintsComments = true;
949 $GLOBALS['sql_constraints_query'] = '';
950 // set the value of global sql_auto_increment variable
951 if (isset($_POST['sql_auto_increment'])) {
952 $GLOBALS['sql_auto_increment'] = $_POST['sql_auto_increment'];
955 $isView = (new Table($sourceTable, $sourceDb, $dbi))->isView();
957 * The old structure of the table.
959 $sqlStructure = $exportSqlPlugin->getTableDef($sourceDb, $sourceTable, false, false, $isView);
961 unset($noConstraintsComments);
963 // -----------------------------------------------------------------
964 // Phase 0: Preparing structures used.
967 * The destination where the table is moved or copied to.
969 $destination = new Expression($targetDb, $targetTable, '');
971 // Find server's SQL mode so the builder can generate correct
972 // queries.
973 // One of the options that alters the behaviour is `ANSI_QUOTES`.
974 Context::setMode((string) $dbi->fetchValue('SELECT @@sql_mode'));
976 // -----------------------------------------------------------------
977 // Phase 1: Dropping existent element of the same name (if exists
978 // and required).
980 if ($addDropIfExists) {
982 * Drop statement used for building the query.
984 $statement = new DropStatement();
986 $tbl = new Table($targetTable, $targetDb, $dbi);
988 $statement->options = new OptionsArray(
989 [$tbl->isView() ? 'VIEW' : 'TABLE', 'IF EXISTS'],
992 $statement->fields = [$destination];
994 // Building the query.
995 $dropQuery = $statement->build() . ';';
997 // Executing it.
998 $dbi->query($dropQuery);
999 $GLOBALS['sql_query'] .= "\n" . $dropQuery;
1001 // If an existing table gets deleted, maintain any entries for
1002 // the PMA_* tables.
1003 $maintainRelations = true;
1006 // -----------------------------------------------------------------
1007 // Phase 2: Generating the new query of this structure.
1010 * The parser responsible for parsing the old queries.
1012 $parser = new Parser($sqlStructure);
1014 if (! empty($parser->statements[0])) {
1017 * The CREATE statement of this structure.
1019 * @var CreateStatement $statement
1021 $statement = $parser->statements[0];
1023 // Changing the destination.
1024 $statement->name = $destination;
1026 // Building back the query.
1027 $sqlStructure = $statement->build() . ';';
1029 // This is to avoid some issues when renaming databases with views
1030 // See: https://github.com/phpmyadmin/phpmyadmin/issues/16422
1031 if ($move) {
1032 $dbi->selectDb($targetDb);
1035 // Executing it
1036 $dbi->query($sqlStructure);
1037 $GLOBALS['sql_query'] .= "\n" . $sqlStructure;
1040 // -----------------------------------------------------------------
1041 // Phase 3: Adding constraints.
1042 // All constraint names are removed because they must be unique.
1044 if (($move || isset($GLOBALS['add_constraints'])) && ! empty($GLOBALS['sql_constraints_query'])) {
1045 $parser = new Parser($GLOBALS['sql_constraints_query']);
1048 * The ALTER statement that generates the constraints.
1050 * @var AlterStatement $statement
1052 $statement = $parser->statements[0];
1054 // Changing the altered table to the destination.
1055 $statement->table = $destination;
1057 // Removing the name of the constraints.
1058 foreach ($statement->altered as $altered) {
1059 // All constraint names are removed because they must be unique.
1060 if (! $altered->options->has('CONSTRAINT')) {
1061 continue;
1064 $altered->field = null;
1067 // Building back the query.
1068 $GLOBALS['sql_constraints_query'] = $statement->build() . ';';
1070 // Executing it.
1071 if ($mode === 'one_table') {
1072 $dbi->query($GLOBALS['sql_constraints_query']);
1075 $GLOBALS['sql_query'] .= "\n" . $GLOBALS['sql_constraints_query'];
1076 if ($mode === 'one_table') {
1077 unset($GLOBALS['sql_constraints_query']);
1081 // -----------------------------------------------------------------
1082 // Phase 4: Adding indexes.
1083 // View phase 3.
1085 if (! empty($GLOBALS['sql_indexes'])) {
1086 $parser = new Parser($GLOBALS['sql_indexes']);
1088 $GLOBALS['sql_indexes'] = '';
1090 * The ALTER statement that generates the indexes.
1092 * @var AlterStatement $statement
1094 foreach ($parser->statements as $statement) {
1095 // Changing the altered table to the destination.
1096 $statement->table = $destination;
1098 // Removing the name of the constraints.
1099 foreach ($statement->altered as $altered) {
1100 // All constraint names are removed because they must be unique.
1101 if (! $altered->options->has('CONSTRAINT')) {
1102 continue;
1105 $altered->field = null;
1108 // Building back the query.
1109 $sqlIndex = $statement->build() . ';';
1111 // Executing it.
1112 if ($mode === 'one_table' || $mode === 'db_copy') {
1113 $dbi->query($sqlIndex);
1116 $GLOBALS['sql_indexes'] .= $sqlIndex;
1119 $GLOBALS['sql_query'] .= "\n" . $GLOBALS['sql_indexes'];
1120 if ($mode === 'one_table' || $mode === 'db_copy') {
1121 unset($GLOBALS['sql_indexes']);
1125 // -----------------------------------------------------------------
1126 // Phase 5: Adding AUTO_INCREMENT.
1128 if (! empty($GLOBALS['sql_auto_increments']) && ($mode === 'one_table' || $mode === 'db_copy')) {
1129 $parser = new Parser($GLOBALS['sql_auto_increments']);
1132 * The ALTER statement that alters the AUTO_INCREMENT value.
1134 $statement = $parser->statements[0];
1135 if ($statement instanceof AlterStatement) {
1136 // Changing the altered table to the destination.
1137 $statement->table = $destination;
1139 // Building back the query.
1140 $GLOBALS['sql_auto_increments'] = $statement->build() . ';';
1142 // Executing it.
1143 $dbi->query($GLOBALS['sql_auto_increments']);
1144 $GLOBALS['sql_query'] .= "\n" . $GLOBALS['sql_auto_increments'];
1147 unset($GLOBALS['sql_auto_increments']);
1149 } else {
1150 $GLOBALS['sql_query'] = '';
1153 $table = new Table($targetTable, $targetDb, $dbi);
1154 // Copy the data unless this is a VIEW
1155 if (($what === 'data' || $what === 'dataonly') && ! $table->isView()) {
1156 $sqlSetMode = "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO'";
1157 $dbi->query($sqlSetMode);
1158 $GLOBALS['sql_query'] .= "\n\n" . $sqlSetMode . ';';
1160 $oldTable = new Table($sourceTable, $sourceDb, $dbi);
1161 $nonGeneratedCols = $oldTable->getNonGeneratedColumns();
1162 if ($nonGeneratedCols !== []) {
1163 $sqlInsertData = 'INSERT INTO ' . $target . '('
1164 . implode(', ', $nonGeneratedCols)
1165 . ') SELECT ' . implode(', ', $nonGeneratedCols)
1166 . ' FROM ' . $source;
1168 $dbi->query($sqlInsertData);
1169 $GLOBALS['sql_query'] .= "\n\n" . $sqlInsertData . ';';
1173 $relationParameters = $relation->getRelationParameters();
1175 // Drops old table if the user has requested to move it
1176 if ($move) {
1177 // This could avoid some problems with replicated databases, when
1178 // moving table from replicated one to not replicated one
1179 $dbi->selectDb($sourceDb);
1181 $sourceTableObj = new Table($sourceTable, $sourceDb, $dbi);
1182 $sqlDropQuery = $sourceTableObj->isView() ? 'DROP VIEW' : 'DROP TABLE';
1184 $sqlDropQuery .= ' ' . $source;
1185 $dbi->query($sqlDropQuery);
1187 // Rename table in configuration storage
1188 $relation->renameTable($sourceDb, $targetDb, $sourceTable, $targetTable);
1190 $GLOBALS['sql_query'] .= "\n\n" . $sqlDropQuery . ';';
1192 return true;
1195 // we are copying
1196 // Create new entries as duplicates from old PMA DBs
1197 if ($what === 'dataonly' || isset($maintainRelations)) {
1198 return true;
1201 if ($relationParameters->columnCommentsFeature !== null) {
1202 // Get all comments and MIME-Types for current table
1203 $commentsCopyRs = $dbi->queryAsControlUser(
1204 'SELECT column_name, comment'
1205 . ($relationParameters->browserTransformationFeature !== null
1206 ? ', mimetype, transformation, transformation_options'
1207 : '')
1208 . ' FROM '
1209 . Util::backquote($relationParameters->columnCommentsFeature->database)
1210 . '.'
1211 . Util::backquote($relationParameters->columnCommentsFeature->columnInfo)
1212 . ' WHERE '
1213 . ' db_name = ' . $dbi->quoteString($sourceDb, ConnectionType::ControlUser)
1214 . ' AND '
1215 . ' table_name = ' . $dbi->quoteString($sourceTable, ConnectionType::ControlUser),
1218 // Write every comment as new copied entry. [MIME]
1219 foreach ($commentsCopyRs as $commentsCopyRow) {
1220 $newCommentQuery = 'REPLACE INTO '
1221 . Util::backquote($relationParameters->columnCommentsFeature->database)
1222 . '.' . Util::backquote($relationParameters->columnCommentsFeature->columnInfo)
1223 . ' (db_name, table_name, column_name, comment'
1224 . ($relationParameters->browserTransformationFeature !== null
1225 ? ', mimetype, transformation, transformation_options'
1226 : '')
1227 . ') VALUES(' . $dbi->quoteString($targetDb, ConnectionType::ControlUser)
1228 . ',' . $dbi->quoteString($targetTable, ConnectionType::ControlUser) . ','
1229 . $dbi->quoteString($commentsCopyRow['column_name'], ConnectionType::ControlUser)
1230 . ','
1231 . $dbi->quoteString($commentsCopyRow['comment'], ConnectionType::ControlUser)
1232 . ($relationParameters->browserTransformationFeature !== null
1233 ? ',' . $dbi->quoteString($commentsCopyRow['mimetype'], ConnectionType::ControlUser)
1234 . ',' . $dbi->quoteString($commentsCopyRow['transformation'], ConnectionType::ControlUser)
1235 . ','
1236 . $dbi->quoteString($commentsCopyRow['transformation_options'], ConnectionType::ControlUser)
1237 : '')
1238 . ')';
1239 $dbi->queryAsControlUser($newCommentQuery);
1242 unset($commentsCopyRs);
1245 // duplicating the bookmarks must not be done here, but
1246 // just once per db
1248 $getFields = ['display_field'];
1249 $whereFields = ['db_name' => $sourceDb, 'table_name' => $sourceTable];
1250 $newFields = ['db_name' => $targetDb, 'table_name' => $targetTable];
1251 self::duplicateInfo('displaywork', 'table_info', $getFields, $whereFields, $newFields);
1253 /** @todo revise this code when we support cross-db relations */
1254 $getFields = ['master_field', 'foreign_table', 'foreign_field'];
1255 $whereFields = ['master_db' => $sourceDb, 'master_table' => $sourceTable];
1256 $newFields = ['master_db' => $targetDb, 'foreign_db' => $targetDb, 'master_table' => $targetTable];
1257 self::duplicateInfo('relwork', 'relation', $getFields, $whereFields, $newFields);
1259 $getFields = ['foreign_field', 'master_table', 'master_field'];
1260 $whereFields = ['foreign_db' => $sourceDb, 'foreign_table' => $sourceTable];
1261 $newFields = ['master_db' => $targetDb, 'foreign_db' => $targetDb, 'foreign_table' => $targetTable];
1262 self::duplicateInfo('relwork', 'relation', $getFields, $whereFields, $newFields);
1264 return true;
1268 * checks if given name is a valid table name,
1269 * currently if not empty, trailing spaces, '.', '/' and '\'
1271 * @see https://dev.mysql.com/doc/refman/5.0/en/legal-names.html
1273 * @param string $tableName name to check
1274 * @param bool $isBackquoted whether this name is used inside backquotes or not
1276 * @todo add check for valid chars in filename on current system/os
1278 public static function isValidName(string $tableName, bool $isBackquoted = false): bool
1280 if ($tableName !== rtrim($tableName)) {
1281 // trailing spaces not allowed even in backquotes
1282 return false;
1285 if ($tableName === '') {
1286 // zero length
1287 return false;
1290 if (! $isBackquoted && $tableName !== trim($tableName)) {
1291 // spaces at the start or in between only allowed inside backquotes
1292 return false;
1295 if (! $isBackquoted && preg_match('/^[a-zA-Z0-9_$]+$/', $tableName)) {
1296 // only allow the above regex in unquoted identifiers
1297 // see : https://dev.mysql.com/doc/refman/5.7/en/identifiers.html
1298 return true;
1301 // If backquoted, all characters should be allowed (except w/ trailing spaces).
1302 return $isBackquoted;
1306 * renames table
1308 * @param string $newName new table name
1309 * @param string|null $newDb new database name
1311 public function rename(string $newName, string|null $newDb = null): bool
1313 if ($this->dbi->getLowerCaseNames() === 1) {
1314 $newName = strtolower($newName);
1317 if ($newDb !== null && $newDb !== $this->getDbName()) {
1318 // Ensure the target is valid
1319 if (! $this->dbi->getDatabaseList()->exists($newDb)) {
1320 $this->errors[] = __('Invalid database:') . ' ' . $newDb;
1322 return false;
1324 } else {
1325 $newDb = $this->getDbName();
1328 $newTable = new Table($newName, $newDb, $this->dbi);
1330 if ($this->getFullName() === $newTable->getFullName()) {
1331 return true;
1334 // Allow whitespaces (not trailing) in $new_name,
1335 // since we are using $backquoted in getting the fullName of table
1336 // below to be used in the query
1337 if (! self::isValidName($newName, true)) {
1338 $this->errors[] = __('Invalid table name:') . ' '
1339 . $newTable->getFullName();
1341 return false;
1344 // If the table is moved to a different database drop its triggers first
1345 $triggers = Triggers::getDetails($this->dbi, $this->getDbName(), $this->getName());
1346 $handleTriggers = $this->getDbName() !== $newDb && $triggers !== [];
1347 if ($handleTriggers) {
1348 foreach ($triggers as $trigger) {
1349 $sql = 'DROP TRIGGER IF EXISTS '
1350 . Util::backquote($this->getDbName())
1351 . '.' . Util::backquote($trigger->name->getName()) . ';';
1352 $this->dbi->query($sql);
1356 // tested also for a view, in MySQL 5.0.92, 5.1.55 and 5.5.13
1357 $GLOBALS['sql_query'] = '
1358 RENAME TABLE ' . $this->getFullName(true) . '
1359 TO ' . $newTable->getFullName(true) . ';';
1360 // I don't think a specific error message for views is necessary
1361 if ($this->dbi->tryQuery($GLOBALS['sql_query']) === false) {
1362 $this->errors[] = $this->dbi->getError();
1364 // Restore triggers in the old database
1365 if ($handleTriggers) {
1366 $this->dbi->selectDb($this->getDbName());
1367 foreach ($triggers as $trigger) {
1368 $this->dbi->query($trigger->getCreateSql(''));
1372 return false;
1375 $oldName = $this->getName();
1376 $oldDb = $this->getDbName();
1377 $this->name = $newName;
1378 $this->dbName = $newDb;
1380 // Rename table in configuration storage
1381 $this->relation->renameTable($oldDb, $newDb, $oldName, $newName);
1383 $this->messages[] = sprintf(
1384 __('Table %1$s has been renamed to %2$s.'),
1385 htmlspecialchars($oldName),
1386 htmlspecialchars($newName),
1389 return true;
1393 * Get all unique columns
1395 * returns an array with all columns with unique content, in fact these are
1396 * all columns being single indexed in PRIMARY or UNIQUE
1398 * e.g.
1399 * - PRIMARY(id) // id
1400 * - UNIQUE(name) // name
1401 * - PRIMARY(fk_id1, fk_id2) // NONE
1402 * - UNIQUE(x,y) // NONE
1404 * @param bool $backquoted whether to quote name with backticks ``
1405 * @param bool $fullName whether to include full name of the table as a prefix
1407 * @return string[]
1409 public function getUniqueColumns(bool $backquoted = true, bool $fullName = true): array
1411 $sql = QueryGenerator::getTableIndexesSql(
1412 $this->getDbName(),
1413 $this->getName(),
1414 'Non_unique = 0',
1416 $uniques = $this->dbi->fetchResult(
1417 $sql,
1418 ['Key_name', null],
1419 'Column_name',
1422 $return = [];
1423 foreach ($uniques as $index) {
1424 if (count($index) > 1) {
1425 continue;
1428 $possibleColumn = $fullName ? $this->getFullName($backquoted) . '.' : '';
1430 if ($backquoted) {
1431 $possibleColumn .= Util::backquote($index[0]);
1432 } else {
1433 $possibleColumn .= $index[0];
1436 // a column might have a primary and an unique index on it
1437 if (in_array($possibleColumn, $return, true)) {
1438 continue;
1441 $return[] = $possibleColumn;
1444 return $return;
1448 * Formats lists of columns
1450 * returns an array with all columns that make use of an index
1452 * e.g. index(col1, col2) would return col1, col2
1454 * @param mixed[] $indexed column data
1455 * @param bool $backquoted whether to quote name with backticks ``
1456 * @param bool $fullName whether to include full name of the table as a prefix
1458 * @return string[]
1460 private function formatColumns(array $indexed, bool $backquoted, bool $fullName): array
1462 $return = [];
1463 foreach ($indexed as $column) {
1464 $return[] = ($fullName ? $this->getFullName($backquoted) . '.' : '')
1465 . ($backquoted ? Util::backquote($column) : $column);
1468 return $return;
1472 * Get all indexed columns
1474 * returns an array with all columns that make use of an index
1476 * e.g. index(col1, col2) would return col1, col2
1478 * @param bool $backquoted whether to quote name with backticks ``
1479 * @param bool $fullName whether to include full name of the table as a prefix
1481 * @return string[]
1483 public function getIndexedColumns(bool $backquoted = true, bool $fullName = true): array
1485 $sql = QueryGenerator::getTableIndexesSql(
1486 $this->getDbName(),
1487 $this->getName(),
1489 $indexed = $this->dbi->fetchResult($sql, 'Column_name', 'Column_name');
1491 return $this->formatColumns($indexed, $backquoted, $fullName);
1495 * Get all columns
1497 * returns an array with all columns
1499 * @param bool $backquoted whether to quote name with backticks ``
1500 * @param bool $fullName whether to include full name of the table as a prefix
1502 * @return string[]
1504 public function getColumns(bool $backquoted = true, bool $fullName = true): array
1506 $sql = 'SHOW COLUMNS FROM ' . $this->getFullName(true);
1507 $indexed = $this->dbi->fetchResult($sql, 'Field', 'Field');
1509 return $this->formatColumns($indexed, $backquoted, $fullName);
1513 * Get meta info for fields in table
1515 * @return FieldMetadata[]
1517 public function getColumnsMeta(): array
1519 $moveColumnsSqlQuery = sprintf(
1520 'SELECT * FROM %s.%s LIMIT 1',
1521 Util::backquote($this->dbName),
1522 Util::backquote($this->name),
1524 $moveColumnsSqlResult = $this->dbi->tryQuery($moveColumnsSqlQuery);
1525 if ($moveColumnsSqlResult !== false) {
1526 return $this->dbi->getFieldsMeta($moveColumnsSqlResult);
1529 // unsure how to reproduce but it was seen on the reporting server
1530 return [];
1534 * Get non-generated columns in table
1536 * @param bool $backquoted whether to quote name with backticks ``
1538 * @return string[]
1540 public function getNonGeneratedColumns(bool $backquoted = true): array
1542 $columnsMetaQuery = 'SHOW COLUMNS FROM ' . $this->getFullName(true);
1543 $ret = [];
1545 $columnsMetaQueryResult = $this->dbi->fetchResult($columnsMetaQuery);
1547 foreach ($columnsMetaQueryResult as $column) {
1548 $value = $column['Field'];
1549 if ($backquoted) {
1550 $value = Util::backquote($value);
1553 // If contains GENERATED or VIRTUAL and does not contain DEFAULT_GENERATED
1554 if (
1556 str_contains($column['Extra'], 'GENERATED')
1557 || str_contains($column['Extra'], 'VIRTUAL')
1558 ) && ! str_contains($column['Extra'], 'DEFAULT_GENERATED')
1560 continue;
1563 $ret[] = $value;
1566 return $ret;
1570 * Return UI preferences for this table from phpMyAdmin database.
1572 * @return mixed[]
1574 protected function getUiPrefsFromDb(UiPreferencesFeature|null $uiPreferencesFeature): array
1576 if ($uiPreferencesFeature === null) {
1577 return [];
1580 // Read from phpMyAdmin database
1581 $sqlQuery = sprintf(
1582 'SELECT `prefs` FROM %s.%s WHERE `username` = %s AND `db_name` = %s AND `table_name` = %s',
1583 Util::backquote($uiPreferencesFeature->database),
1584 Util::backquote($uiPreferencesFeature->tableUiPrefs),
1585 $this->dbi->quoteString(Config::getInstance()->selectedServer['user'], ConnectionType::ControlUser),
1586 $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser),
1587 $this->dbi->quoteString($this->name, ConnectionType::ControlUser),
1590 $value = $this->dbi->queryAsControlUser($sqlQuery)->fetchValue();
1591 if (is_string($value)) {
1592 return json_decode($value, true);
1595 return [];
1599 * Save this table's UI preferences into phpMyAdmin database.
1601 * @return true|Message
1603 protected function saveUiPrefsToDb(UiPreferencesFeature $uiPreferencesFeature): bool|Message
1605 $table = Util::backquote($uiPreferencesFeature->database) . '.'
1606 . Util::backquote($uiPreferencesFeature->tableUiPrefs);
1608 $config = Config::getInstance();
1609 $username = $config->selectedServer['user'];
1610 $sqlQuery = ' REPLACE INTO ' . $table
1611 . ' (username, db_name, table_name, prefs) VALUES ('
1612 . $this->dbi->quoteString($username, ConnectionType::ControlUser) . ', '
1613 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser) . ', '
1614 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser) . ', '
1615 . $this->dbi->quoteString((string) json_encode($this->uiprefs), ConnectionType::ControlUser) . ')';
1617 $success = $this->dbi->tryQuery($sqlQuery, ConnectionType::ControlUser);
1619 if (! $success) {
1620 $message = Message::error(
1621 __('Could not save table UI preferences!'),
1623 $message->addMessage(
1624 Message::rawError($this->dbi->getError(ConnectionType::ControlUser)),
1625 '<br><br>',
1628 return $message;
1631 // Remove some old rows in table_uiprefs if it exceeds the configured
1632 // maximum rows
1633 $sqlQuery = 'SELECT COUNT(*) FROM ' . $table;
1634 $rowsCount = (int) $this->dbi->fetchValue($sqlQuery);
1635 $maxRows = $config->selectedServer['MaxTableUiprefs'];
1636 if ($rowsCount > $maxRows) {
1637 $numRowsToDelete = $rowsCount - $maxRows;
1638 $sqlQuery = ' DELETE FROM ' . $table . ' ORDER BY last_update ASC LIMIT ' . $numRowsToDelete;
1639 $success = $this->dbi->tryQuery($sqlQuery, ConnectionType::ControlUser);
1641 if (! $success) {
1642 $message = Message::error(sprintf(
1643 __('Failed to cleanup table UI preferences (see $cfg[\'Servers\'][$i][\'MaxTableUiprefs\'] %s)'),
1644 MySQLDocumentation::showDocumentation('config', 'cfg_Servers_MaxTableUiprefs'),
1646 $message->addMessage(
1647 Message::rawError($this->dbi->getError(ConnectionType::ControlUser)),
1648 '<br><br>',
1651 return $message;
1655 return true;
1659 * Loads the UI preferences for this table.
1660 * If pmadb and table_uiprefs is set, it will load the UI preferences from
1661 * phpMyAdmin database.
1663 protected function loadUiPrefs(): void
1665 $serverId = Current::$server;
1667 // set session variable if it's still undefined
1668 if (! isset($_SESSION['tmpval']['table_uiprefs'][$serverId][$this->dbName][$this->name])) {
1669 // check whether we can get from pmadb
1670 $uiPrefs = $this->getUiPrefsFromDb($this->relation->getRelationParameters()->uiPreferencesFeature);
1671 $_SESSION['tmpval']['table_uiprefs'][$serverId][$this->dbName][$this->name] = $uiPrefs;
1674 $this->uiprefs =& $_SESSION['tmpval']['table_uiprefs'][$serverId][$this->dbName][$this->name];
1678 * Get a property from UI preferences.
1679 * Return false if the property is not found.
1680 * Available property:
1681 * - PROP_SORTED_COLUMN
1682 * - PROP_COLUMN_ORDER
1683 * - PROP_COLUMN_VISIB
1685 * @param string $property property
1687 public function getUiProp(string $property): mixed
1689 if ($this->uiprefs === []) {
1690 $this->loadUiPrefs();
1693 // do checking based on property
1694 if ($property === self::PROP_SORTED_COLUMN) {
1695 if (! isset($this->uiprefs[$property])) {
1696 return false;
1699 if (! isset($_POST['discard_remembered_sort'])) {
1700 // check if the column name exists in this table
1701 $tmp = explode(' ', $this->uiprefs[$property]);
1702 $colname = $tmp[0];
1703 //remove backquoting from colname
1704 $colname = str_replace('`', '', $colname);
1705 //get the available column name without backquoting
1706 $availColumns = $this->getColumns(false);
1708 foreach ($availColumns as $eachCol) {
1709 // check if $each_col ends with $colname
1710 if (substr_compare($eachCol, $colname, mb_strlen($eachCol) - mb_strlen($colname)) === 0) {
1711 return $this->uiprefs[$property];
1716 // remove the property, since it no longer exists in database
1717 $this->removeUiProp($property);
1719 return false;
1722 if ($property === self::PROP_COLUMN_ORDER || $property === self::PROP_COLUMN_VISIB) {
1723 if ($this->isView() || ! isset($this->uiprefs[$property])) {
1724 return false;
1727 // check if the table has not been modified
1728 if ($this->getStatusInfo('Create_time') == $this->uiprefs['CREATE_TIME']) {
1729 return array_map(intval(...), $this->uiprefs[$property]);
1732 // remove the property, since the table has been modified
1733 $this->removeUiProp($property);
1735 return false;
1738 // default behaviour for other property:
1739 return $this->uiprefs[$property] ?? false;
1743 * Set a property from UI preferences.
1744 * If pmadb and table_uiprefs is set, it will save the UI preferences to
1745 * phpMyAdmin database.
1746 * Available property:
1747 * - PROP_SORTED_COLUMN
1748 * - PROP_COLUMN_ORDER
1749 * - PROP_COLUMN_VISIB
1751 * @param string $property Property
1752 * @param mixed $value Value for the property
1753 * @param string|null $tableCreateTime Needed for PROP_COLUMN_ORDER and PROP_COLUMN_VISIB
1755 public function setUiProp(string $property, mixed $value, string|null $tableCreateTime = null): bool|Message
1757 if ($this->uiprefs === []) {
1758 $this->loadUiPrefs();
1761 // we want to save the create time if the property is PROP_COLUMN_ORDER
1762 if (! $this->isView() && ($property === self::PROP_COLUMN_ORDER || $property === self::PROP_COLUMN_VISIB)) {
1763 $currCreateTime = $this->getStatusInfo('CREATE_TIME');
1764 if ($tableCreateTime === null || $tableCreateTime != $currCreateTime) {
1765 // there is no $table_create_time, or
1766 // supplied $table_create_time is older than current create time,
1767 // so don't save
1768 return Message::error(
1769 sprintf(
1771 'Cannot save UI property "%s". The changes made will ' .
1772 'not be persistent after you refresh this page. ' .
1773 'Please check if the table structure has been changed.',
1775 $property,
1780 $this->uiprefs['CREATE_TIME'] = $currCreateTime;
1783 // save the value
1784 $this->uiprefs[$property] = $value;
1786 // check if pmadb is set
1787 $uiPreferencesFeature = $this->relation->getRelationParameters()->uiPreferencesFeature;
1788 if ($uiPreferencesFeature !== null) {
1789 return $this->saveUiPrefsToDb($uiPreferencesFeature);
1792 return true;
1796 * Remove a property from UI preferences.
1798 * @param string $property the property
1800 * @return true|Message
1802 public function removeUiProp(string $property): bool|Message
1804 if ($this->uiprefs === []) {
1805 $this->loadUiPrefs();
1808 if (isset($this->uiprefs[$property])) {
1809 unset($this->uiprefs[$property]);
1811 // check if pmadb is set
1812 $uiPreferencesFeature = $this->relation->getRelationParameters()->uiPreferencesFeature;
1813 if ($uiPreferencesFeature !== null) {
1814 return $this->saveUiPrefsToDb($uiPreferencesFeature);
1818 return true;
1822 * Get all column names which are MySQL reserved words
1824 * @return string[]
1826 public function getReservedColumnNames(): array
1828 $columns = $this->getColumns(false);
1829 $return = [];
1830 foreach ($columns as $column) {
1831 $temp = explode('.', $column);
1832 $columnName = $temp[2];
1833 if (! Context::isKeyword($columnName, true)) {
1834 continue;
1837 $return[] = $columnName;
1840 return $return;
1844 * Function to get the name and type of the columns of a table
1846 * @return array<string, string>
1848 public function getNameAndTypeOfTheColumns(): array
1850 $columns = [];
1851 foreach (
1852 $this->dbi->getColumnsFull($this->dbName, $this->name) as $row
1854 if (preg_match('@^(set|enum)\((.+)\)$@i', $row['Type'], $tmp)) {
1855 $tmp[2] = mb_substr(
1856 (string) preg_replace('@([^,])\'\'@', '\\1\\\'', ',' . $tmp[2]),
1859 $columns[$row['Field']] = $tmp[1] . '('
1860 . str_replace(',', ', ', $tmp[2]) . ')';
1861 } else {
1862 $columns[$row['Field']] = $row['Type'];
1866 return $columns;
1870 * Get index with index name
1872 * @param string $index Index name
1874 public function getIndex(string $index): Index
1876 return Index::singleton($this->dbi, $this->dbName, $this->name, $index);
1880 * Function to handle update for display field
1882 * @param string $displayField display field
1884 public function updateDisplayField(string $displayField, DisplayFeature $displayFeature): void
1886 if ($displayField === '') {
1887 $updQuery = 'DELETE FROM '
1888 . Util::backquote($displayFeature->database)
1889 . '.' . Util::backquote($displayFeature->tableInfo)
1890 . ' WHERE db_name = ' . $this->dbi->quoteString($this->dbName)
1891 . ' AND table_name = ' . $this->dbi->quoteString($this->name);
1892 } else {
1893 $updQuery = 'REPLACE INTO '
1894 . Util::backquote($displayFeature->database)
1895 . '.' . Util::backquote($displayFeature->tableInfo)
1896 . '(db_name, table_name, display_field) VALUES('
1897 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser) . ','
1898 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser) . ','
1899 . $this->dbi->quoteString($displayField, ConnectionType::ControlUser) . ')';
1902 $this->dbi->queryAsControlUser($updQuery);
1906 * Function to get update query for updating internal relations
1908 * @param mixed[] $multiEditColumnsName multi edit column names
1909 * @param mixed[] $destinationDb destination tables
1910 * @param mixed[] $destinationTable destination tables
1911 * @param mixed[] $destinationColumn destination columns
1912 * @param mixed[]|null $existrel db, table, column
1914 public function updateInternalRelations(
1915 array $multiEditColumnsName,
1916 array $destinationDb,
1917 array $destinationTable,
1918 array $destinationColumn,
1919 RelationFeature $relationFeature,
1920 array|null $existrel,
1921 ): bool {
1922 $updated = false;
1923 foreach ($destinationDb as $masterFieldMd5 => $foreignDb) {
1924 $updQuery = null;
1925 // Map the fieldname's md5 back to its real name
1926 $masterField = $multiEditColumnsName[$masterFieldMd5];
1927 $foreignTable = $destinationTable[$masterFieldMd5];
1928 $foreignField = $destinationColumn[$masterFieldMd5];
1929 if (! empty($foreignDb) && ! empty($foreignTable) && ! empty($foreignField)) {
1930 if (! isset($existrel[$masterField])) {
1931 $updQuery = 'INSERT INTO '
1932 . Util::backquote($relationFeature->database)
1933 . '.' . Util::backquote($relationFeature->relation)
1934 . '(master_db, master_table, master_field, foreign_db,'
1935 . ' foreign_table, foreign_field)'
1936 . ' values('
1937 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser) . ', '
1938 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser) . ', '
1939 . $this->dbi->quoteString($masterField, ConnectionType::ControlUser) . ', '
1940 . $this->dbi->quoteString($foreignDb, ConnectionType::ControlUser) . ', '
1941 . $this->dbi->quoteString($foreignTable, ConnectionType::ControlUser) . ','
1942 . $this->dbi->quoteString($foreignField, ConnectionType::ControlUser) . ')';
1943 } elseif (
1944 $existrel[$masterField]['foreign_db'] != $foreignDb
1945 || $existrel[$masterField]['foreign_table'] != $foreignTable
1946 || $existrel[$masterField]['foreign_field'] != $foreignField
1948 $updQuery = 'UPDATE '
1949 . Util::backquote($relationFeature->database)
1950 . '.' . Util::backquote($relationFeature->relation)
1951 . ' SET foreign_db = '
1952 . $this->dbi->quoteString($foreignDb, ConnectionType::ControlUser) . ', '
1953 . ' foreign_table = '
1954 . $this->dbi->quoteString($foreignTable, ConnectionType::ControlUser) . ', '
1955 . ' foreign_field = '
1956 . $this->dbi->quoteString($foreignField, ConnectionType::ControlUser) . ' '
1957 . ' WHERE master_db = '
1958 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser)
1959 . ' AND master_table = '
1960 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser)
1961 . ' AND master_field = '
1962 . $this->dbi->quoteString($masterField, ConnectionType::ControlUser);
1964 } elseif (isset($existrel[$masterField])) {
1965 $updQuery = 'DELETE FROM '
1966 . Util::backquote($relationFeature->database)
1967 . '.' . Util::backquote($relationFeature->relation)
1968 . ' WHERE master_db = '
1969 . $this->dbi->quoteString($this->dbName, ConnectionType::ControlUser)
1970 . ' AND master_table = '
1971 . $this->dbi->quoteString($this->name, ConnectionType::ControlUser)
1972 . ' AND master_field = '
1973 . $this->dbi->quoteString($masterField, ConnectionType::ControlUser);
1976 if (! isset($updQuery)) {
1977 continue;
1980 $this->dbi->queryAsControlUser($updQuery);
1981 $updated = true;
1984 return $updated;
1988 * Function to handle foreign key updates
1990 * @param mixed[] $destinationForeignDb destination foreign database
1991 * @param mixed[] $multiEditColumnsName multi edit column names
1992 * @param mixed[] $destinationForeignTable destination foreign table
1993 * @param mixed[] $destinationForeignColumn destination foreign column
1994 * @param string[] $optionsArray options array
1995 * @param string $table current table
1996 * @param mixed[] $existrelForeign db, table, column
1998 * @return array{string, string, string, bool}
2000 public function updateForeignKeys(
2001 array $destinationForeignDb,
2002 array $multiEditColumnsName,
2003 array $destinationForeignTable,
2004 array $destinationForeignColumn,
2005 array $optionsArray,
2006 string $table,
2007 array $existrelForeign,
2008 ): array {
2009 $htmlOutput = '';
2010 $previewSqlData = '';
2011 $displayQuery = '';
2012 $seenError = false;
2014 foreach ($destinationForeignDb as $masterFieldMd5 => $foreignDb) {
2015 $create = false;
2016 $drop = false;
2018 // Map the fieldname's md5 back to its real name
2019 $masterField = $multiEditColumnsName[$masterFieldMd5];
2021 $foreignTable = $destinationForeignTable[$masterFieldMd5];
2022 $foreignField = $destinationForeignColumn[$masterFieldMd5];
2024 $refDbName = $existrelForeign[$masterFieldMd5]['ref_db_name'] ?? Current::$database;
2026 $emptyFields = false;
2027 foreach ($masterField as $key => $oneField) {
2028 if (
2029 (! empty($oneField) && empty($foreignField[$key]))
2030 || (empty($oneField) && ! empty($foreignField[$key]))
2032 $emptyFields = true;
2035 if (! empty($oneField) || ! empty($foreignField[$key])) {
2036 continue;
2039 unset($masterField[$key], $foreignField[$key]);
2042 if (! empty($foreignDb) && ! empty($foreignTable) && ! $emptyFields) {
2043 if (isset($existrelForeign[$masterFieldMd5])) {
2044 $constraintName = $existrelForeign[$masterFieldMd5]['constraint'];
2045 $onDelete = ! empty(
2046 $existrelForeign[$masterFieldMd5]['on_delete']
2048 ? $existrelForeign[$masterFieldMd5]['on_delete']
2049 : 'RESTRICT';
2050 $onUpdate = ! empty(
2051 $existrelForeign[$masterFieldMd5]['on_update']
2053 ? $existrelForeign[$masterFieldMd5]['on_update']
2054 : 'RESTRICT';
2056 if (
2057 $refDbName != $foreignDb
2058 || $existrelForeign[$masterFieldMd5]['ref_table_name'] != $foreignTable
2059 || $existrelForeign[$masterFieldMd5]['ref_index_list'] != $foreignField
2060 || $existrelForeign[$masterFieldMd5]['index_list'] != $masterField
2061 || $_POST['constraint_name'][$masterFieldMd5] != $constraintName
2062 || ($_POST['on_delete'][$masterFieldMd5] != $onDelete)
2063 || ($_POST['on_update'][$masterFieldMd5] != $onUpdate)
2065 // another foreign key is already defined for this field
2066 // or an option has been changed for ON DELETE or ON UPDATE
2067 $drop = true;
2068 $create = true;
2070 } else {
2071 // no key defined for this field(s)
2072 $create = true;
2074 } elseif (isset($existrelForeign[$masterFieldMd5])) {
2075 $drop = true;
2078 if ($drop) {
2079 $dropQuery = 'ALTER TABLE ' . Util::backquote($table)
2080 . ' DROP FOREIGN KEY '
2081 . Util::backquote($existrelForeign[$masterFieldMd5]['constraint'])
2082 . ';';
2084 if (! isset($_POST['preview_sql'])) {
2085 $displayQuery .= $dropQuery . "\n";
2086 $this->dbi->tryQuery($dropQuery);
2087 $tmpErrorDrop = $this->dbi->getError();
2089 if ($tmpErrorDrop !== '') {
2090 $seenError = true;
2091 $htmlOutput .= Generator::mysqlDie($tmpErrorDrop, $dropQuery, false, '', false);
2092 continue;
2094 } else {
2095 $previewSqlData .= $dropQuery . "\n";
2099 $tmpErrorCreate = false;
2100 if (! $create) {
2101 continue;
2104 $createQuery = $this->getSQLToCreateForeignKey(
2105 $table,
2106 $masterField,
2107 $foreignDb,
2108 $foreignTable,
2109 $foreignField,
2110 $_POST['constraint_name'][$masterFieldMd5],
2111 $optionsArray[$_POST['on_delete'][$masterFieldMd5]],
2112 $optionsArray[$_POST['on_update'][$masterFieldMd5]],
2115 if (! isset($_POST['preview_sql'])) {
2116 $displayQuery .= $createQuery . "\n";
2117 $this->dbi->tryQuery($createQuery);
2118 $tmpErrorCreate = $this->dbi->getError();
2119 if ($tmpErrorCreate !== '') {
2120 $seenError = true;
2122 if (substr($tmpErrorCreate, 1, 4) == '1005') {
2123 $message = Message::error(
2125 'Error creating foreign key on %1$s (check data types)',
2128 $message->addParam(implode(', ', $masterField));
2129 $htmlOutput .= $message->getDisplay();
2130 } else {
2131 $htmlOutput .= Generator::mysqlDie($tmpErrorCreate, $createQuery, false, '', false);
2134 $htmlOutput .= MySQLDocumentation::show('create-table-foreign-keys') . "\n";
2136 } else {
2137 $previewSqlData .= $createQuery . "\n";
2140 // this is an alteration and the old constraint has been dropped
2141 // without creation of a new one
2142 if (! $drop || $tmpErrorCreate === '' || $tmpErrorCreate === false) {
2143 continue;
2146 // a rollback may be better here
2147 $sqlQueryRecreate = '# Restoring the dropped constraint...' . "\n";
2148 $sqlQueryRecreate .= $this->getSQLToCreateForeignKey(
2149 $table,
2150 $masterField,
2151 $existrelForeign[$masterFieldMd5]['ref_db_name'],
2152 $existrelForeign[$masterFieldMd5]['ref_table_name'],
2153 $existrelForeign[$masterFieldMd5]['ref_index_list'],
2154 $existrelForeign[$masterFieldMd5]['constraint'],
2155 $optionsArray[$existrelForeign[$masterFieldMd5]['on_delete'] ?? ''] ?? null,
2156 $optionsArray[$existrelForeign[$masterFieldMd5]['on_update'] ?? ''] ?? null,
2158 if (! isset($_POST['preview_sql'])) {
2159 $displayQuery .= $sqlQueryRecreate . "\n";
2160 $this->dbi->tryQuery($sqlQueryRecreate);
2161 } else {
2162 $previewSqlData .= $sqlQueryRecreate;
2166 return [$htmlOutput, $previewSqlData, $displayQuery, $seenError];
2170 * Returns the SQL query for foreign key constraint creation
2172 * @param string $table table name
2173 * @param mixed[] $field field names
2174 * @param string $foreignDb foreign database name
2175 * @param string $foreignTable foreign table name
2176 * @param mixed[] $foreignField foreign field names
2177 * @param string|null $name name of the constraint
2178 * @param string|null $onDelete on delete action
2179 * @param string|null $onUpdate on update action
2181 * @return string SQL query for foreign key constraint creation
2183 private function getSQLToCreateForeignKey(
2184 string $table,
2185 array $field,
2186 string $foreignDb,
2187 string $foreignTable,
2188 array $foreignField,
2189 string|null $name = null,
2190 string|null $onDelete = null,
2191 string|null $onUpdate = null,
2192 ): string {
2193 $sqlQuery = 'ALTER TABLE ' . Util::backquote($table) . ' ADD ';
2194 // if user entered a constraint name
2195 if ($name !== null && $name !== '') {
2196 $sqlQuery .= ' CONSTRAINT ' . Util::backquote($name);
2199 foreach ($field as $key => $oneField) {
2200 $field[$key] = Util::backquote($oneField);
2203 foreach ($foreignField as $key => $oneField) {
2204 $foreignField[$key] = Util::backquote($oneField);
2207 $sqlQuery .= ' FOREIGN KEY (' . implode(', ', $field) . ') REFERENCES '
2208 . ($this->dbName !== $foreignDb
2209 ? Util::backquote($foreignDb) . '.' : '')
2210 . Util::backquote($foreignTable)
2211 . '(' . implode(', ', $foreignField) . ')';
2213 if ($onDelete !== null && $onDelete !== '') {
2214 $sqlQuery .= ' ON DELETE ' . $onDelete;
2217 if ($onUpdate !== null && $onUpdate !== '') {
2218 $sqlQuery .= ' ON UPDATE ' . $onUpdate;
2221 $sqlQuery .= ';';
2223 return $sqlQuery;
2227 * Returns the generation expression for virtual columns
2229 * @param string|null $column name of the column
2231 * @return mixed[]|bool associative array of column name and their expressions
2232 * or false on failure
2234 public function getColumnGenerationExpression(string|null $column = null): array|bool
2236 if (
2237 Compatibility::isMySqlOrPerconaDb()
2238 && $this->dbi->getVersion() > 50705
2239 && ! Config::getInstance()->selectedServer['DisableIS']
2241 $sql = 'SELECT
2242 `COLUMN_NAME` AS `Field`,
2243 `GENERATION_EXPRESSION` AS `Expression`
2244 FROM
2245 `information_schema`.`COLUMNS`
2246 WHERE
2247 `TABLE_SCHEMA` = ' . $this->dbi->quoteString($this->dbName) . '
2248 AND `TABLE_NAME` = ' . $this->dbi->quoteString($this->name);
2249 if ($column !== null) {
2250 $sql .= ' AND `COLUMN_NAME` = ' . $this->dbi->quoteString($column);
2253 return $this->dbi->fetchResult($sql, 'Field', 'Expression');
2256 $createTable = $this->showCreate();
2257 if ($createTable === '') {
2258 return false;
2261 $parser = new Parser($createTable);
2262 $stmt = $parser->statements[0];
2263 $fields = [];
2264 if ($stmt instanceof CreateStatement) {
2265 $fields = TableUtils::getFields($stmt);
2268 if ($column != null) {
2269 $expression = isset($fields[$column]['expr']) ? substr($fields[$column]['expr'], 1, -1) : '';
2271 return [$column => $expression];
2274 $ret = [];
2275 foreach ($fields as $field => $options) {
2276 if (! isset($options['expr'])) {
2277 continue;
2280 $ret[$field] = substr($options['expr'], 1, -1);
2283 return $ret;
2287 * Returns the CREATE statement for this table
2289 public function showCreate(): string
2291 return (string) $this->dbi->fetchValue(
2292 'SHOW CREATE TABLE ' . Util::backquote($this->dbName) . '.'
2293 . Util::backquote($this->name),
2299 * Returns the real row count for a table
2301 public function getRealRowCountTable(): int|null
2303 // SQL query to get row count for a table.
2304 $result = $this->dbi->fetchSingleRow(
2305 sprintf(
2306 'SELECT COUNT(*) AS %s FROM %s.%s',
2307 Util::backquote('row_count'),
2308 Util::backquote($this->dbName),
2309 Util::backquote($this->name),
2313 if (! is_array($result)) {
2314 return null;
2317 return (int) $result['row_count'];
2321 * Get columns with indexes
2323 * @param int $types types bitmask
2325 * @return (string|int)[] an array of columns
2327 public function getColumnsWithIndex(int $types): array
2329 $columnsWithIndex = [];
2330 foreach (
2331 Index::getFromTableByChoice($this->name, $this->dbName, $types) as $index
2333 $columns = $index->getColumns();
2334 $columnsWithIndex = array_merge($columnsWithIndex, array_keys($columns));
2337 return $columnsWithIndex;