Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / DatabaseInterface.php
blob9c94c7d89da22bab59c6014cd006eaa1d03577c6
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use PhpMyAdmin\Config\Settings\Server;
8 use PhpMyAdmin\Dbal\Connection;
9 use PhpMyAdmin\Dbal\ConnectionException;
10 use PhpMyAdmin\Dbal\ConnectionType;
11 use PhpMyAdmin\Dbal\DbalInterface;
12 use PhpMyAdmin\Dbal\DbiExtension;
13 use PhpMyAdmin\Dbal\DbiMysqli;
14 use PhpMyAdmin\Dbal\ResultInterface;
15 use PhpMyAdmin\Dbal\Statement;
16 use PhpMyAdmin\Dbal\Warning;
17 use PhpMyAdmin\Error\ErrorHandler;
18 use PhpMyAdmin\Html\Generator;
19 use PhpMyAdmin\Identifiers\DatabaseName;
20 use PhpMyAdmin\Query\Cache;
21 use PhpMyAdmin\Query\Compatibility;
22 use PhpMyAdmin\Query\Generator as QueryGenerator;
23 use PhpMyAdmin\Query\Utilities;
24 use PhpMyAdmin\Routing\Routing;
25 use PhpMyAdmin\SqlParser\Context;
26 use PhpMyAdmin\Table\Table;
27 use PhpMyAdmin\Tracking\Tracker;
28 use PhpMyAdmin\Utils\SessionCache;
29 use stdClass;
31 use function __;
32 use function array_column;
33 use function array_combine;
34 use function array_diff;
35 use function array_keys;
36 use function array_map;
37 use function array_multisort;
38 use function array_reverse;
39 use function array_shift;
40 use function array_slice;
41 use function basename;
42 use function closelog;
43 use function defined;
44 use function explode;
45 use function implode;
46 use function is_array;
47 use function is_int;
48 use function mb_strtolower;
49 use function microtime;
50 use function openlog;
51 use function reset;
52 use function sprintf;
53 use function str_contains;
54 use function str_replace;
55 use function str_starts_with;
56 use function stripos;
57 use function strnatcasecmp;
58 use function strtolower;
59 use function strtoupper;
60 use function strtr;
61 use function substr;
62 use function syslog;
63 use function trigger_error;
64 use function uasort;
65 use function uksort;
66 use function usort;
68 use const E_USER_WARNING;
69 use const LOG_INFO;
70 use const LOG_NDELAY;
71 use const LOG_PID;
72 use const LOG_USER;
73 use const SORT_ASC;
74 use const SORT_DESC;
76 /**
77 * Main interface for database interactions
79 class DatabaseInterface implements DbalInterface
81 public static self|null $instance = null;
83 /**
84 * Force STORE_RESULT method, ignored by classic MySQL.
86 public const QUERY_BUFFERED = 0;
88 /**
89 * Do not read all rows immediately.
91 public const QUERY_UNBUFFERED = 2;
93 /**
94 * Get session variable.
96 public const GETVAR_SESSION = 1;
98 /**
99 * Get global variable.
101 public const GETVAR_GLOBAL = 2;
104 * Opened database connections.
106 * @var array<int, Connection>
107 * @psalm-var array<value-of<ConnectionType>, Connection>
109 private array $connections = [];
111 /** @var array<int, string>|null */
112 private array|null $currentUserAndHost = null;
114 /** @var array<int, array<int, string>>|null Current role and host cache */
115 private array|null $currentRoleAndHost = null;
118 * @var int|null lower_case_table_names value cache
119 * @psalm-var 0|1|2|null
121 private int|null $lowerCaseTableNames = null;
123 /** @var bool Whether connection is MariaDB */
124 private bool $isMariaDb = false;
125 /** @var bool Whether connection is Percona */
126 private bool $isPercona = false;
127 /** @var int Server version as number */
128 private int $versionInt = 55000;
129 /** @var string Server version */
130 private string $versionString = '5.50.0';
131 /** @var string Server version comment */
132 private string $versionComment = '';
134 /** @var Types MySQL types data */
135 public Types $types;
137 private Cache $cache;
139 public float $lastQueryExecutionTime = 0;
141 private ListDatabase|null $databaseList = null;
142 private readonly Config $config;
144 /** @param DbiExtension $extension Object to be used for database queries */
145 public function __construct(private DbiExtension $extension)
147 if (defined('TESTSUITE')) {
148 $this->connections[ConnectionType::User->value] = new Connection(new stdClass());
149 $this->connections[ConnectionType::ControlUser->value] = new Connection(new stdClass());
152 $this->cache = new Cache();
153 $this->types = new Types($this);
154 $this->config = Config::getInstance();
157 /** @deprecated Use dependency injection instead. */
158 public static function getInstance(): self
160 if (self::$instance === null) {
161 self::$instance = new self(new DbiMysqli());
164 return self::$instance;
168 * runs a query
170 * @param string $query SQL query to execute
171 * @param int $options optional query options
172 * @param bool $cacheAffectedRows whether to cache affected rows
174 public function query(
175 string $query,
176 ConnectionType $connectionType = ConnectionType::User,
177 int $options = self::QUERY_BUFFERED,
178 bool $cacheAffectedRows = true,
179 ): ResultInterface {
180 $result = $this->tryQuery($query, $connectionType, $options, $cacheAffectedRows);
182 if (! $result) {
183 // The following statement will exit
184 Generator::mysqlDie($this->getError($connectionType), $query);
186 ResponseRenderer::getInstance()->callExit();
189 return $result;
192 public function getCache(): Cache
194 return $this->cache;
198 * runs a query and returns the result
200 * @param string $query query to run
201 * @param int $options if DatabaseInterface::QUERY_UNBUFFERED
202 * is provided, it will instruct the extension
203 * to use unbuffered mode
204 * @param bool $cacheAffectedRows whether to cache affected row
206 public function tryQuery(
207 string $query,
208 ConnectionType $connectionType = ConnectionType::User,
209 int $options = self::QUERY_BUFFERED,
210 bool $cacheAffectedRows = true,
211 ): ResultInterface|false {
212 if (! isset($this->connections[$connectionType->value])) {
213 return false;
216 $time = microtime(true);
218 $result = $this->extension->realQuery($query, $this->connections[$connectionType->value], $options);
220 if ($connectionType === ConnectionType::User) {
221 $this->lastQueryExecutionTime = microtime(true) - $time;
224 if ($cacheAffectedRows) {
225 $GLOBALS['cached_affected_rows'] = $this->affectedRows($connectionType, false);
228 if ($this->config->config->debug->sql) {
229 $errorMessage = $this->getError($connectionType);
230 Utilities::debugLogQueryIntoSession(
231 $query,
232 $errorMessage !== '' ? $errorMessage : null,
233 $result,
234 $this->lastQueryExecutionTime,
236 if ($this->config->config->debug->sqllog) {
237 openlog('phpMyAdmin', LOG_NDELAY | LOG_PID, LOG_USER);
239 syslog(
240 LOG_INFO,
241 sprintf(
242 'SQL[%s?route=%s]: %0.3f(W:%d,C:%s,L:0x%02X) > %s',
243 basename($_SERVER['SCRIPT_NAME']),
244 Routing::$route,
245 $this->lastQueryExecutionTime,
246 $this->getWarningCount($connectionType),
247 $cacheAffectedRows ? 'y' : 'n',
248 $connectionType->value,
249 $query,
252 closelog();
256 if ($result !== false && Tracker::isActive()) {
257 Tracker::handleQuery($query);
260 return $result;
264 * Send multiple SQL queries to the database server and execute the first one
266 * @param string $multiQuery multi query statement to execute
268 public function tryMultiQuery(
269 string $multiQuery = '',
270 ConnectionType $connectionType = ConnectionType::User,
271 ): bool {
272 if (! isset($this->connections[$connectionType->value])) {
273 return false;
276 return $this->extension->realMultiQuery($this->connections[$connectionType->value], $multiQuery);
280 * Executes a query as controluser.
281 * The result is always buffered and never cached
283 * @param string $sql the query to execute
285 * @return ResultInterface the result set
287 public function queryAsControlUser(string $sql): ResultInterface
289 // Avoid caching of the number of rows affected; for example, this function
290 // is called for tracking purposes but we want to display the correct number
291 // of rows affected by the original query, not by the query generated for
292 // tracking.
293 return $this->query($sql, ConnectionType::ControlUser, self::QUERY_BUFFERED, false);
297 * Executes a query as controluser.
298 * The result is always buffered and never cached
300 * @param string $sql the query to execute
302 * @return ResultInterface|false the result set, or false if the query failed
304 public function tryQueryAsControlUser(string $sql): ResultInterface|false
306 // Avoid caching of the number of rows affected; for example, this function
307 // is called for tracking purposes but we want to display the correct number
308 // of rows affected by the original query, not by the query generated for
309 // tracking.
310 return $this->tryQuery($sql, ConnectionType::ControlUser, self::QUERY_BUFFERED, false);
314 * returns array with table names for given db
316 * @param string $database name of database
318 * @return array<int, string> tables names
320 public function getTables(string $database, ConnectionType $connectionType = ConnectionType::User): array
322 if ($database === '') {
323 return [];
326 /** @var array<int, string> $tables */
327 $tables = $this->fetchResult(
328 'SHOW TABLES FROM ' . Util::backquote($database) . ';',
329 null,
331 $connectionType,
333 if ($this->config->settings['NaturalOrder']) {
334 usort($tables, strnatcasecmp(...));
337 return $tables;
341 * returns array of all tables in given db or dbs
342 * this function expects unquoted names:
343 * RIGHT: my_database
344 * WRONG: `my_database`
345 * WRONG: my\_database
346 * if $tbl_is_group is true, $table is used as filter for table names
348 * <code>
349 * $dbi->getTablesFull('my_database');
350 * $dbi->getTablesFull('my_database', 'my_table'));
351 * $dbi->getTablesFull('my_database', 'my_tables_', true));
352 * </code>
354 * @param string $database database
355 * @param string|mixed[] $table table name(s)
356 * @param bool $tableIsGroup $table is a table group
357 * @param int $limitOffset zero-based offset for the count
358 * @param bool|int $limitCount number of tables to return
359 * @param string $sortBy table attribute to sort by
360 * @param string $sortOrder direction to sort (ASC or DESC)
361 * @param string|null $tableType whether table or view
363 * @return (string|int|null)[][] list of tables in given db(s)
365 * @todo move into Table
367 public function getTablesFull(
368 string $database,
369 string|array $table = '',
370 bool $tableIsGroup = false,
371 int $limitOffset = 0,
372 bool|int $limitCount = false,
373 string $sortBy = 'Name',
374 string $sortOrder = 'ASC',
375 string|null $tableType = null,
376 ConnectionType $connectionType = ConnectionType::User,
377 ): array {
378 if ($limitCount === true) {
379 $limitCount = $this->config->settings['MaxTableList'];
382 $tables = [];
383 $pagingApplied = false;
385 if ($limitCount && is_array($table) && $sortBy === 'Name') {
386 if ($sortOrder === 'DESC') {
387 $table = array_reverse($table);
390 $table = array_slice($table, $limitOffset, $limitCount);
391 $pagingApplied = true;
394 if (! $this->config->selectedServer['DisableIS']) {
395 $sqlWhereTable = '';
396 if ($table !== [] && $table !== '') {
397 if (is_array($table)) {
398 $sqlWhereTable = QueryGenerator::getTableNameConditionForMultiple(
399 array_map($this->quoteString(...), $table),
401 } else {
402 $sqlWhereTable = QueryGenerator::getTableNameCondition(
403 $this->quoteString($tableIsGroup ? $this->escapeMysqlWildcards($table) : $table),
404 $tableIsGroup,
409 $sqlWhereTable .= QueryGenerator::getTableTypeCondition($tableType);
411 // for PMA bc:
412 // `SCHEMA_FIELD_NAME` AS `SHOW_TABLE_STATUS_FIELD_NAME`
414 // on non-Windows servers,
415 // added BINARY in the WHERE clause to force a case sensitive
416 // comparison (if we are looking for the db Aa we don't want
417 // to find the db aa)
419 $sql = QueryGenerator::getSqlForTablesFull($this->quoteString($database), $sqlWhereTable);
421 // Sort the tables
422 $sql .= ' ORDER BY ' . $sortBy . ' ' . $sortOrder;
424 if ($limitCount && ! $pagingApplied) {
425 $sql .= ' LIMIT ' . $limitCount . ' OFFSET ' . $limitOffset;
428 /** @var (string|int|null)[][][] $tables */
429 $tables = $this->fetchResult(
430 $sql,
431 ['TABLE_SCHEMA', 'TABLE_NAME'],
432 null,
433 $connectionType,
436 // here, we check for Mroonga engine and compute the good data_length and index_length
437 // in the StructureController only we need to sum the two values as the other engines
438 foreach ($tables as $oneDatabaseName => $oneDatabaseTables) {
439 foreach ($oneDatabaseTables as $oneTableName => $oneTableData) {
440 if ($oneTableData['Engine'] !== 'Mroonga') {
441 continue;
444 if (! StorageEngine::hasMroongaEngine()) {
445 continue;
449 $tables[$oneDatabaseName][$oneTableName]['Data_length'],
450 $tables[$oneDatabaseName][$oneTableName]['Index_length'],
451 ] = StorageEngine::getMroongaLengths((string) $oneDatabaseName, (string) $oneTableName);
455 if ($sortBy === 'Name' && $this->config->settings['NaturalOrder']) {
456 // here, the array's first key is by schema name
457 foreach ($tables as $oneDatabaseName => $oneDatabaseTables) {
458 uksort($oneDatabaseTables, strnatcasecmp(...));
460 if ($sortOrder === 'DESC') {
461 $oneDatabaseTables = array_reverse($oneDatabaseTables);
464 $tables[$oneDatabaseName] = $oneDatabaseTables;
466 } elseif ($sortBy === 'Data_length') {
467 // Size = Data_length + Index_length
468 foreach ($tables as $oneDatabaseName => $oneDatabaseTables) {
469 uasort(
470 $oneDatabaseTables,
471 static function (array $a, array $b): int {
472 $aLength = $a['Data_length'] + $a['Index_length'];
473 $bLength = $b['Data_length'] + $b['Index_length'];
475 return $aLength <=> $bLength;
479 if ($sortOrder === 'DESC') {
480 $oneDatabaseTables = array_reverse($oneDatabaseTables);
483 $tables[$oneDatabaseName] = $oneDatabaseTables;
487 // on windows with lower_case_table_names = 1
488 // MySQL returns
489 // with SHOW DATABASES or information_schema.SCHEMATA: `Test`
490 // but information_schema.TABLES gives `test`
491 // see https://github.com/phpmyadmin/phpmyadmin/issues/8402
492 $tables = $tables[$database]
493 ?? $tables[mb_strtolower($database)]
494 ?? [];
497 // If permissions are wrong on even one database directory,
498 // information_schema does not return any table info for any database
499 // this is why we fall back to SHOW TABLE STATUS even for MySQL >= 50002
500 if ($tables === []) {
501 $sql = 'SHOW TABLE STATUS FROM ' . Util::backquote($database);
502 if (($table !== '' && $table !== []) || $tableIsGroup || ($tableType !== null && $tableType !== '')) {
503 $sql .= ' WHERE';
504 $needAnd = false;
505 if (($table !== '' && $table !== []) || $tableIsGroup) {
506 if (is_array($table)) {
507 $sql .= ' `Name` IN ('
508 . implode(
509 ', ',
510 array_map(
511 fn (string $string): string => $this->quoteString($string, $connectionType),
512 $table,
514 ) . ')';
515 } else {
516 $sql .= ' `Name` LIKE '
517 . $this->quoteString($this->escapeMysqlWildcards($table) . '%', $connectionType);
520 $needAnd = true;
523 if ($tableType !== null && $tableType !== '') {
524 if ($needAnd) {
525 $sql .= ' AND';
528 if ($tableType === 'view') {
529 $sql .= " `Comment` = 'VIEW'";
530 } elseif ($tableType === 'table') {
531 $sql .= " `Comment` != 'VIEW'";
536 /** @var (string|int|null)[][] $eachTables */
537 $eachTables = $this->fetchResult($sql, 'Name', null, $connectionType);
539 // here, we check for Mroonga engine and compute the good data_length and index_length
540 // in the StructureController only we need to sum the two values as the other engines
541 foreach ($eachTables as $tableName => $tableData) {
542 if ($tableData['Engine'] !== 'Mroonga') {
543 continue;
546 if (! StorageEngine::hasMroongaEngine()) {
547 continue;
551 $eachTables[$tableName]['Data_length'],
552 $eachTables[$tableName]['Index_length'],
553 ] = StorageEngine::getMroongaLengths($database, (string) $tableName);
556 // Sort naturally if the config allows it and we're sorting
557 // the Name column.
558 if ($sortBy === 'Name' && $this->config->settings['NaturalOrder']) {
559 uksort($eachTables, strnatcasecmp(...));
561 if ($sortOrder === 'DESC') {
562 $eachTables = array_reverse($eachTables);
564 } else {
565 // Prepare to sort by creating array of the selected sort
566 // value to pass to array_multisort
568 // Size = Data_length + Index_length
569 $sortValues = [];
570 if ($sortBy === 'Data_length') {
571 foreach ($eachTables as $tableName => $tableData) {
572 $sortValues[$tableName] = strtolower(
573 (string) ($tableData['Data_length']
574 + $tableData['Index_length']),
577 } else {
578 foreach ($eachTables as $tableName => $tableData) {
579 $sortValues[$tableName] = strtolower($tableData[$sortBy] ?? '');
583 if ($sortValues !== []) {
584 // See https://stackoverflow.com/a/32461188 for the explanation of below hack
585 $keys = array_keys($eachTables);
586 if ($sortOrder === 'DESC') {
587 array_multisort($sortValues, SORT_DESC, $eachTables, $keys);
588 } else {
589 array_multisort($sortValues, SORT_ASC, $eachTables, $keys);
592 $eachTables = array_combine($keys, $eachTables);
595 // cleanup the temporary sort array
596 unset($sortValues);
599 if ($limitCount && ! $pagingApplied) {
600 $eachTables = array_slice($eachTables, $limitOffset, $limitCount, true);
603 $tables = Compatibility::getISCompatForGetTablesFull($eachTables, $database);
606 if ($tables !== []) {
607 // cache table data, so Table does not require to issue SHOW TABLE STATUS again
608 $this->cache->cacheTableData($database, $tables);
611 return $tables;
615 * returns array with databases containing extended infos about them
617 * @param string|null $database database
618 * @param bool $forceStats retrieve stats also for MySQL < 5
619 * @param string $sortBy column to order by
620 * @param string $sortOrder ASC or DESC
621 * @param int $limitOffset starting offset for LIMIT
622 * @param bool|int $limitCount row count for LIMIT or true for $cfg['MaxDbList']
624 * @return mixed[]
626 * @todo move into ListDatabase?
628 public function getDatabasesFull(
629 string|null $database = null,
630 bool $forceStats = false,
631 ConnectionType $connectionType = ConnectionType::User,
632 string $sortBy = 'SCHEMA_NAME',
633 string $sortOrder = 'ASC',
634 int $limitOffset = 0,
635 bool|int $limitCount = false,
636 ): array {
637 $sortOrder = strtoupper($sortOrder);
639 if ($limitCount === true) {
640 $limitCount = $this->config->settings['MaxDbList'];
643 $applyLimitAndOrderManual = true;
645 if (! $this->config->selectedServer['DisableIS']) {
647 * if NaturalOrder config is enabled, we cannot use LIMIT
648 * cause MySQL does not support natural ordering,
649 * we have to do it afterward
651 $limit = '';
652 if (! $this->config->settings['NaturalOrder']) {
653 if ($limitCount) {
654 $limit = ' LIMIT ' . $limitCount . ' OFFSET ' . $limitOffset;
657 $applyLimitAndOrderManual = false;
660 // get table information from information_schema
661 $sqlWhereSchema = '';
662 if ($database !== null) {
663 $sqlWhereSchema = 'WHERE `SCHEMA_NAME` LIKE ' . $this->quoteString($database, $connectionType);
666 $sql = QueryGenerator::getInformationSchemaDatabasesFullRequest(
667 $forceStats,
668 $sqlWhereSchema,
669 $sortBy,
670 $sortOrder,
671 $limit,
674 $databases = $this->fetchResult($sql, 'SCHEMA_NAME', null, $connectionType);
676 $mysqlError = $this->getError($connectionType);
677 if ($databases === [] && isset($GLOBALS['errno'])) {
678 Generator::mysqlDie($mysqlError, $sql);
681 // display only databases also in official database list
682 // f.e. to apply hide_db and only_db
683 $drops = array_diff(
684 array_keys($databases),
685 (array) $this->getDatabaseList(),
687 foreach ($drops as $drop) {
688 unset($databases[$drop]);
690 } else {
691 $databases = [];
692 foreach ($this->getDatabaseList() as $databaseName) {
693 // Compatibility with INFORMATION_SCHEMA output
694 $databases[$databaseName]['SCHEMA_NAME'] = $databaseName;
696 $databases[$databaseName]['DEFAULT_COLLATION_NAME'] = $this->getDbCollation($databaseName);
698 if (! $forceStats) {
699 continue;
702 // get additional info about tables
703 $databases[$databaseName]['SCHEMA_TABLES'] = 0;
704 $databases[$databaseName]['SCHEMA_TABLE_ROWS'] = 0;
705 $databases[$databaseName]['SCHEMA_DATA_LENGTH'] = 0;
706 $databases[$databaseName]['SCHEMA_MAX_DATA_LENGTH'] = 0;
707 $databases[$databaseName]['SCHEMA_INDEX_LENGTH'] = 0;
708 $databases[$databaseName]['SCHEMA_LENGTH'] = 0;
709 $databases[$databaseName]['SCHEMA_DATA_FREE'] = 0;
711 $res = $this->query(
712 'SHOW TABLE STATUS FROM '
713 . Util::backquote($databaseName) . ';',
716 while ($row = $res->fetchAssoc()) {
717 $databases[$databaseName]['SCHEMA_TABLES']++;
718 $databases[$databaseName]['SCHEMA_TABLE_ROWS'] += $row['Rows'];
719 $databases[$databaseName]['SCHEMA_DATA_LENGTH'] += $row['Data_length'];
720 $databases[$databaseName]['SCHEMA_MAX_DATA_LENGTH'] += $row['Max_data_length'];
721 $databases[$databaseName]['SCHEMA_INDEX_LENGTH'] += $row['Index_length'];
723 // for InnoDB, this does not contain the number of
724 // overhead bytes but the total free space
725 if ($row['Engine'] !== 'InnoDB') {
726 $databases[$databaseName]['SCHEMA_DATA_FREE'] += $row['Data_free'];
729 $databases[$databaseName]['SCHEMA_LENGTH'] += $row['Data_length'] + $row['Index_length'];
732 unset($res);
737 * apply limit and order manually now
738 * (caused by older MySQL < 5 or NaturalOrder config)
740 if ($applyLimitAndOrderManual) {
741 usort(
742 $databases,
743 static fn ($a, $b): int => Utilities::usortComparisonCallback($a, $b, $sortBy, $sortOrder),
747 * now apply limit
749 if ($limitCount) {
750 $databases = array_slice($databases, $limitOffset, $limitCount);
754 return $databases;
758 * returns detailed array with all columns for given table in database,
759 * or all tables/databases
761 * @param string|null $database name of database
762 * @param string|null $table name of table to retrieve columns from
763 * @param string|null $column name of specific column
765 * @return mixed[]
767 public function getColumnsFull(
768 string|null $database = null,
769 string|null $table = null,
770 string|null $column = null,
771 ConnectionType $connectionType = ConnectionType::User,
772 ): array {
773 if (! $this->config->selectedServer['DisableIS']) {
774 $sql = QueryGenerator::getInformationSchemaColumnsFullRequest(
775 $database !== null ? $this->quoteString($database, $connectionType) : null,
776 $table !== null ? $this->quoteString($table, $connectionType) : null,
777 $column !== null ? $this->quoteString($column, $connectionType) : null,
779 $arrayKeys = QueryGenerator::getInformationSchemaColumns($database, $table, $column);
781 return $this->fetchResult($sql, $arrayKeys, null, $connectionType);
784 $columns = [];
785 if ($database === null) {
786 foreach ($this->getDatabaseList() as $database) {
787 $columns[$database] = $this->getColumnsFull($database, null, null, $connectionType);
790 return $columns;
793 if ($table === null) {
794 $tables = $this->getTables($database);
795 foreach ($tables as $table) {
796 $columns[$table] = $this->getColumnsFull($database, $table, null, $connectionType);
799 return $columns;
802 $sql = 'SHOW FULL COLUMNS FROM '
803 . Util::backquote($database) . '.' . Util::backquote($table);
804 if ($column !== null) {
805 $sql .= ' LIKE ' . $this->quoteString($column, $connectionType);
808 $columns = $this->fetchResult($sql, 'Field', null, $connectionType);
810 $columns = Compatibility::getISCompatForGetColumnsFull($columns, $database, $table);
812 if ($column !== null) {
813 return reset($columns);
816 return $columns;
820 * Returns description of a $column in given table
822 * @param string $database name of database
823 * @param string $table name of table to retrieve columns from
824 * @param string $column name of column
825 * @param T $full whether to return full info or only column names
827 * @psalm-return (T is true ? ColumnFull : Column)|null
829 * @template T of bool
831 public function getColumn(
832 string $database,
833 string $table,
834 string $column,
835 bool $full = false,
836 ConnectionType $connectionType = ConnectionType::User,
837 ): ColumnFull|Column|null {
838 $sql = QueryGenerator::getColumnsSql(
839 $database,
840 $table,
841 $this->quoteString($this->escapeMysqlWildcards($column)),
842 $full,
844 /** @var (string|null)[][] $fields */
845 $fields = $this->fetchResult($sql, 'Field', null, $connectionType);
848 * @var array{
849 * Field: string,
850 * Type: string,
851 * Collation: string|null,
852 * Null:'YES'|'NO',
853 * Key: string,
854 * Default: string|null,
855 * Extra: string,
856 * Privileges: string,
857 * Comment: string
858 * }[] $columns
860 $columns = $this->attachIndexInfoToColumns($database, $table, $fields);
862 $columns = $this->convertToColumns($columns, $full);
864 return array_shift($columns);
868 * Returns descriptions of columns in given table
870 * @param string $database name of database
871 * @param string $table name of table to retrieve columns from
872 * @param T $full whether to return full info or only column names
874 * @return ColumnFull[]|Column[]
875 * @psalm-return (T is true ? ColumnFull[] : Column[])
877 * @template T of bool
879 public function getColumns(
880 string $database,
881 string $table,
882 bool $full = false,
883 ConnectionType $connectionType = ConnectionType::User,
884 ): array {
885 $sql = QueryGenerator::getColumnsSql($database, $table, null, $full);
886 /** @var (string|null)[][] $fields */
887 $fields = $this->fetchResult($sql, 'Field', null, $connectionType);
890 * @var array{
891 * Field: string,
892 * Type: string,
893 * Collation: string|null,
894 * Null:'YES'|'NO',
895 * Key: string,
896 * Default: string|null,
897 * Extra: string,
898 * Privileges: string,
899 * Comment: string
900 * }[] $columns
902 $columns = $this->attachIndexInfoToColumns($database, $table, $fields);
904 return $this->convertToColumns($columns, $full);
908 * Attach index information to the column definition
910 * @param string $database name of database
911 * @param string $table name of table to retrieve columns from
912 * @param (string|null)[][] $fields column array indexed by their names
914 * @return (string|null)[][] Column defintions with index information
916 private function attachIndexInfoToColumns(
917 string $database,
918 string $table,
919 array $fields,
920 ): array {
921 if ($fields === []) {
922 return [];
925 // Check if column is a part of multiple-column index and set its 'Key'.
926 $indexes = Index::getFromTable($this, $table, $database);
927 foreach ($fields as $field => $fieldData) {
928 if (! empty($fieldData['Key'])) {
929 continue;
932 foreach ($indexes as $index) {
933 if (! $index->hasColumn((string) $field)) {
934 continue;
937 $indexColumns = $index->getColumns();
938 if ($indexColumns[$field]->getSeqInIndex() <= 1) {
939 continue;
942 $fields[$field]['Key'] = $index->isUnique() ? 'UNI' : 'MUL';
946 return $fields;
950 * @psalm-param array{
951 * Field: string,
952 * Type: string,
953 * Collation: string|null,
954 * Null:'YES'|'NO',
955 * Key: string,
956 * Default: string|null,
957 * Extra: string,
958 * Privileges: string,
959 * Comment: string
960 * }[] $fields column array indexed by their names
962 * @return (ColumnFull|Column)[]
964 private function convertToColumns(array $fields, bool $full = false): array
966 $columns = [];
967 foreach ($fields as $field => $column) {
968 $columns[$field] = $full ? new ColumnFull(
969 $column['Field'],
970 $column['Type'],
971 $column['Collation'],
972 $column['Null'] === 'YES',
973 $column['Key'],
974 $column['Default'],
975 $column['Extra'],
976 $column['Privileges'],
977 $column['Comment'],
978 ) : new Column(
979 $column['Field'],
980 $column['Type'],
981 $column['Null'] === 'YES',
982 $column['Key'],
983 $column['Default'],
984 $column['Extra'],
988 return $columns;
992 * Returns all column names in given table
994 * @param string $database name of database
995 * @param string $table name of table to retrieve columns from
997 * @return string[]
999 public function getColumnNames(
1000 string $database,
1001 string $table,
1002 ConnectionType $connectionType = ConnectionType::User,
1003 ): array {
1004 $sql = QueryGenerator::getColumnsSql($database, $table);
1006 // We only need the 'Field' column which contains the table's column names
1007 return $this->fetchResult($sql, null, 'Field', $connectionType);
1011 * Returns indexes of a table
1013 * @param string $database name of database
1014 * @param string $table name of the table whose indexes are to be retrieved
1016 * @return array<int, array<string, string|null>>
1017 * @psalm-return array<int, array{
1018 * Table: string,
1019 * Non_unique: '0'|'1',
1020 * Key_name: string,
1021 * Seq_in_index: string,
1022 * Column_name: string|null,
1023 * Collation: 'A'|'D'|null,
1024 * Cardinality: string,
1025 * Sub_part: string|null,
1026 * Packed: string|null,
1027 * Null: string|null,
1028 * Index_type: 'BTREE'|'FULLTEXT'|'HASH'|'RTREE',
1029 * Comment: string,
1030 * Index_comment: string,
1031 * Ignored?: string,
1032 * Visible?: string,
1033 * Expression?: string|null
1034 * }>
1036 public function getTableIndexes(
1037 string $database,
1038 string $table,
1039 ConnectionType $connectionType = ConnectionType::User,
1040 ): array {
1041 $sql = QueryGenerator::getTableIndexesSql($database, $table);
1043 return $this->fetchResult($sql, null, null, $connectionType);
1047 * returns value of given mysql server variable
1049 * @param string $var mysql server variable name
1050 * @param int $type DatabaseInterface::GETVAR_SESSION | DatabaseInterface::GETVAR_GLOBAL
1052 * @return false|string|null value for mysql server variable
1054 public function getVariable(
1055 string $var,
1056 int $type = self::GETVAR_SESSION,
1057 ConnectionType $connectionType = ConnectionType::User,
1058 ): false|string|null {
1059 $modifier = match ($type) {
1060 self::GETVAR_SESSION => ' SESSION',
1061 self::GETVAR_GLOBAL => ' GLOBAL',
1062 default => '',
1065 return $this->fetchValue('SHOW' . $modifier . ' VARIABLES LIKE \'' . $var . '\';', 1, $connectionType);
1069 * Sets new value for a variable if it is different from the current value
1071 * @param string $var variable name
1072 * @param string $value value to set
1074 public function setVariable(
1075 string $var,
1076 string $value,
1077 ConnectionType $connectionType = ConnectionType::User,
1078 ): bool {
1079 $currentValue = $this->getVariable($var, self::GETVAR_SESSION, $connectionType);
1080 if ($currentValue == $value) {
1081 return true;
1084 return (bool) $this->query('SET ' . $var . ' = ' . $value . ';', $connectionType);
1087 public function getDefaultCharset(): string
1089 return $this->versionInt > 50503 ? 'utf8mb4' : 'utf8';
1092 public function getDefaultCollation(): string
1094 return $this->versionInt > 50503 ? 'utf8mb4_general_ci' : 'utf8_general_ci';
1098 * Function called just after a connection to the MySQL database server has
1099 * been established. It sets the connection collation, and determines the
1100 * version of MySQL which is running.
1102 public function postConnect(Server $currentServer): void
1104 $version = $this->fetchSingleRow('SELECT @@version, @@version_comment');
1106 if (is_array($version)) {
1107 $this->setVersion($version);
1110 $this->query(
1111 sprintf('SET NAMES \'%s\' COLLATE \'%s\';', $this->getDefaultCharset(), $this->getDefaultCollation()),
1114 /* Locale for messages */
1115 $locale = LanguageManager::getInstance()->getCurrentLanguage()->getMySQLLocale();
1116 if ($locale !== '') {
1117 $this->query("SET lc_messages = '" . $locale . "';");
1120 // Set timezone for the session, if required.
1121 if ($currentServer->sessionTimeZone !== '') {
1122 $sqlQueryTz = 'SET ' . Util::backquote('time_zone') . ' = '
1123 . $this->quoteString($currentServer->sessionTimeZone);
1125 if (! $this->tryQuery($sqlQueryTz)) {
1126 $errorMessageTz = sprintf(
1128 'Unable to use timezone "%1$s" for server %2$d. '
1129 . 'Please check your configuration setting for '
1130 . '[em]$cfg[\'Servers\'][%3$d][\'SessionTimeZone\'][/em]. '
1131 . 'phpMyAdmin is currently using the default time zone '
1132 . 'of the database server.',
1134 $currentServer->sessionTimeZone,
1135 Current::$server,
1136 Current::$server,
1139 trigger_error($errorMessageTz, E_USER_WARNING);
1143 /* Loads closest context to this version. */
1144 Context::loadClosest(($this->isMariaDb ? 'MariaDb' : 'MySql') . $this->versionInt);
1146 $this->databaseList = null;
1150 * Sets collation connection for user link
1152 * @param string $collation collation to set
1154 public function setCollation(string $collation): void
1156 $charset = $this->getDefaultCharset();
1157 /* Automatically adjust collation if not supported by server */
1158 if ($charset === 'utf8' && str_starts_with($collation, 'utf8mb4_')) {
1159 $collation = 'utf8_' . substr($collation, 8);
1162 $result = $this->tryQuery(
1163 'SET collation_connection = '
1164 . $this->quoteString($collation)
1165 . ';',
1168 if ($result === false) {
1169 trigger_error(
1170 __('Failed to set configured collation connection!'),
1171 E_USER_WARNING,
1174 return;
1179 * returns a single value from the given result or query,
1180 * if the query or the result has more than one row or field
1181 * the first field of the first row is returned
1183 * <code>
1184 * $sql = 'SELECT `name` FROM `user` WHERE `id` = 123';
1185 * $user_name = $dbi->fetchValue($sql);
1186 * // produces
1187 * // $user_name = 'John Doe'
1188 * </code>
1190 * @param string $query The query to execute
1191 * @param int|string $field field to fetch the value from, starting at 0, with 0 being default
1193 * @return string|false|null value of first field in first row from result or false if not found
1195 public function fetchValue(
1196 string $query,
1197 int|string $field = 0,
1198 ConnectionType $connectionType = ConnectionType::User,
1199 ): string|false|null {
1200 $result = $this->tryQuery($query, $connectionType, self::QUERY_BUFFERED, false);
1201 if ($result === false) {
1202 return false;
1205 return $result->fetchValue($field);
1209 * Returns only the first row from the result or null if result is empty.
1211 * <code>
1212 * $sql = 'SELECT * FROM `user` WHERE `id` = 123';
1213 * $user = $dbi->fetchSingleRow($sql);
1214 * // produces
1215 * // $user = array('id' => 123, 'name' => 'John Doe')
1216 * </code>
1218 * @param string $query The query to execute
1219 * @param string $type NUM|ASSOC|BOTH returned array should either numeric associative or both
1220 * @psalm-param DatabaseInterface::FETCH_NUM|DatabaseInterface::FETCH_ASSOC $type
1222 * @return array<string|null>|null
1224 public function fetchSingleRow(
1225 string $query,
1226 string $type = DbalInterface::FETCH_ASSOC,
1227 ConnectionType $connectionType = ConnectionType::User,
1228 ): array|null {
1229 $result = $this->tryQuery($query, $connectionType, self::QUERY_BUFFERED, false);
1230 if ($result === false) {
1231 return null;
1234 return $this->fetchByMode($result, $type) ?: null;
1238 * Returns row or element of a row
1240 * @param mixed[]|string $row Row to process
1241 * @param string|int|null $value Which column to return
1243 private function fetchValueOrValueByIndex(array|string $row, string|int|null $value): mixed
1245 return $value === null ? $row : $row[$value];
1249 * returns array of rows with numeric or associative keys
1251 * @param ResultInterface $result result set identifier
1252 * @param string $mode either self::FETCH_NUM, self::FETCH_ASSOC or self::FETCH_BOTH
1253 * @psalm-param self::FETCH_NUM|self::FETCH_ASSOC $mode
1255 * @return array<string|null>
1257 private function fetchByMode(ResultInterface $result, string $mode): array
1259 return $mode === self::FETCH_NUM ? $result->fetchRow() : $result->fetchAssoc();
1263 * returns all rows in the resultset in one array
1265 * <code>
1266 * $sql = 'SELECT * FROM `user`';
1267 * $users = $dbi->fetchResult($sql);
1268 * // produces
1269 * // $users[] = array('id' => 123, 'name' => 'John Doe')
1271 * $sql = 'SELECT `id`, `name` FROM `user`';
1272 * $users = $dbi->fetchResult($sql, 'id');
1273 * // produces
1274 * // $users['123'] = array('id' => 123, 'name' => 'John Doe')
1276 * $sql = 'SELECT `id`, `name` FROM `user`';
1277 * $users = $dbi->fetchResult($sql, 0);
1278 * // produces
1279 * // $users['123'] = array(0 => 123, 1 => 'John Doe')
1281 * $sql = 'SELECT `id`, `name` FROM `user`';
1282 * $users = $dbi->fetchResult($sql, 'id', 'name');
1283 * // or
1284 * $users = $dbi->fetchResult($sql, 0, 1);
1285 * // produces
1286 * // $users['123'] = 'John Doe'
1288 * $sql = 'SELECT `name` FROM `user`';
1289 * $users = $dbi->fetchResult($sql);
1290 * // produces
1291 * // $users[] = 'John Doe'
1293 * $sql = 'SELECT `group`, `name` FROM `user`'
1294 * $users = $dbi->fetchResult($sql, array('group', null), 'name');
1295 * // produces
1296 * // $users['admin'][] = 'John Doe'
1298 * $sql = 'SELECT `group`, `name` FROM `user`'
1299 * $users = $dbi->fetchResult($sql, array('group', 'name'), 'id');
1300 * // produces
1301 * // $users['admin']['John Doe'] = '123'
1302 * </code>
1304 * @param string $query query to execute
1305 * @param string|int|mixed[]|null $key field-name or offset
1306 * used as key for array
1307 * or array of those
1308 * @param string|int|null $value value-name or offset used as value for array
1310 * @return mixed[] resultrows or values indexed by $key
1312 public function fetchResult(
1313 string $query,
1314 string|int|array|null $key = null,
1315 string|int|null $value = null,
1316 ConnectionType $connectionType = ConnectionType::User,
1317 ): array {
1318 $resultRows = [];
1320 $result = $this->tryQuery($query, $connectionType, self::QUERY_BUFFERED, false);
1322 // return empty array if result is empty or false
1323 if ($result === false) {
1324 return [];
1327 if ($key === null) {
1328 // no nested array if only one field is in result
1329 if ($value === 0 || $result->numFields() === 1) {
1330 return $result->fetchAllColumn();
1333 return $value === null ? $result->fetchAllAssoc() : array_column($result->fetchAllAssoc(), $value);
1336 if (is_array($key)) {
1337 while ($row = $result->fetchAssoc()) {
1338 $resultTarget =& $resultRows;
1339 foreach ($key as $keyIndex) {
1340 if ($keyIndex === null) {
1341 $resultTarget =& $resultTarget[];
1342 continue;
1345 if (! isset($resultTarget[$row[$keyIndex]])) {
1346 $resultTarget[$row[$keyIndex]] = [];
1349 $resultTarget =& $resultTarget[$row[$keyIndex]];
1352 $resultTarget = $this->fetchValueOrValueByIndex($row, $value);
1355 return $resultRows;
1358 if ($key === 0 && $value === 1) {
1359 return $result->fetchAllKeyPair();
1362 // if $key is an integer use non associative mysql fetch function
1363 $fetchFunction = is_int($key) ? self::FETCH_NUM : self::FETCH_ASSOC;
1365 while ($row = $this->fetchByMode($result, $fetchFunction)) {
1366 $resultRows[$row[$key]] = $this->fetchValueOrValueByIndex($row, $value);
1369 return $resultRows;
1373 * Get supported SQL compatibility modes
1375 * @return string[] supported SQL compatibility modes
1377 public function getCompatibilities(): array
1379 return [
1380 'NONE',
1381 'ANSI',
1382 'DB2',
1383 'MAXDB',
1384 'MYSQL323',
1385 'MYSQL40',
1386 'MSSQL',
1387 'ORACLE',
1388 // removed; in MySQL 5.0.33, this produces exports that
1389 // can't be read by POSTGRESQL (see our bug #1596328)
1390 // 'POSTGRESQL',
1391 'TRADITIONAL',
1396 * returns warnings for last query
1398 * @return Warning[] warnings
1400 public function getWarnings(ConnectionType $connectionType = ConnectionType::User): array
1402 $result = $this->tryQuery('SHOW WARNINGS', $connectionType, 0, false);
1403 if ($result === false) {
1404 return [];
1407 $warnings = [];
1408 while ($row = $result->fetchAssoc()) {
1409 $warnings[] = Warning::fromArray($row);
1412 return $warnings;
1416 * gets the current user with host
1418 * @return string the current user i.e. user@host
1420 public function getCurrentUser(): string
1422 if (SessionCache::has('mysql_cur_user')) {
1423 return SessionCache::get('mysql_cur_user');
1426 $user = $this->fetchValue('SELECT CURRENT_USER();');
1427 if ($user !== false) {
1428 SessionCache::set('mysql_cur_user', $user);
1430 return $user;
1433 return '@';
1437 * gets the current role with host. Role maybe multiple separated by comma
1438 * Support start from MySQL 8.x / MariaDB 10.0.5
1440 * @see https://dev.mysql.com/doc/refman/8.0/en/roles.html
1441 * @see https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_current-role
1442 * @see https://mariadb.com/kb/en/mariadb-1005-release-notes/#newly-implemented-features
1443 * @see https://mariadb.com/kb/en/roles_overview/
1445 * @return list<string> the current roles i.e. array of role@host
1447 public function getCurrentRoles(): array
1449 if (($this->isMariaDB() && $this->getVersion() < 100500) || $this->getVersion() < 80000) {
1450 return [];
1453 if (SessionCache::has('mysql_cur_role')) {
1454 return SessionCache::get('mysql_cur_role');
1457 $role = $this->fetchValue('SELECT CURRENT_ROLE();');
1458 if ($role === false || $role === null || $role === 'NONE') {
1459 return [];
1462 $role = array_map('trim', explode(',', str_replace('`', '', $role)));
1463 SessionCache::set('mysql_cur_role', $role);
1465 return $role;
1468 public function isSuperUser(): bool
1470 if (SessionCache::has('is_superuser')) {
1471 return (bool) SessionCache::get('is_superuser');
1474 if (! $this->isConnected()) {
1475 return false;
1478 $isSuperUser = (bool) $this->fetchValue('SELECT 1 FROM mysql.user LIMIT 1');
1480 SessionCache::set('is_superuser', $isSuperUser);
1482 return $isSuperUser;
1485 public function isGrantUser(): bool
1487 if (SessionCache::has('is_grantuser')) {
1488 return (bool) SessionCache::get('is_grantuser');
1491 if (! $this->isConnected()) {
1492 return false;
1495 $hasGrantPrivilege = false;
1497 if ($this->config->selectedServer['DisableIS']) {
1498 $grants = $this->getCurrentUserGrants();
1500 foreach ($grants as $grant) {
1501 if (str_contains($grant, 'WITH GRANT OPTION')) {
1502 $hasGrantPrivilege = true;
1503 break;
1507 SessionCache::set('is_grantuser', $hasGrantPrivilege);
1509 return $hasGrantPrivilege;
1512 [$user, $host] = $this->getCurrentUserAndHost();
1513 $query = QueryGenerator::getInformationSchemaDataForGranteeRequest($user, $host);
1514 $hasGrantPrivilege = (bool) $this->fetchValue($query);
1516 if (! $hasGrantPrivilege) {
1517 foreach ($this->getCurrentRolesAndHost() as [$role, $roleHost]) {
1518 $query = QueryGenerator::getInformationSchemaDataForGranteeRequest($role, $roleHost ?? '');
1519 $hasGrantPrivilege = (bool) $this->fetchValue($query);
1521 if ($hasGrantPrivilege) {
1522 break;
1527 SessionCache::set('is_grantuser', $hasGrantPrivilege);
1529 return $hasGrantPrivilege;
1532 public function isCreateUser(): bool
1534 if (SessionCache::has('is_createuser')) {
1535 return (bool) SessionCache::get('is_createuser');
1538 if (! $this->isConnected()) {
1539 return false;
1542 $hasCreatePrivilege = false;
1544 if ($this->config->selectedServer['DisableIS']) {
1545 $grants = $this->getCurrentUserGrants();
1547 foreach ($grants as $grant) {
1548 if (str_contains($grant, 'ALL PRIVILEGES ON *.*') || str_contains($grant, 'CREATE USER')) {
1549 $hasCreatePrivilege = true;
1550 break;
1554 SessionCache::set('is_createuser', $hasCreatePrivilege);
1556 return $hasCreatePrivilege;
1559 [$user, $host] = $this->getCurrentUserAndHost();
1560 $query = QueryGenerator::getInformationSchemaDataForCreateRequest($user, $host);
1561 $hasCreatePrivilege = (bool) $this->fetchValue($query);
1563 if (! $hasCreatePrivilege) {
1564 foreach ($this->getCurrentRolesAndHost() as [$role, $roleHost]) {
1565 $query = QueryGenerator::getInformationSchemaDataForCreateRequest($role, $roleHost ?? '');
1566 $hasCreatePrivilege = (bool) $this->fetchValue($query);
1568 if ($hasCreatePrivilege) {
1569 break;
1574 SessionCache::set('is_createuser', $hasCreatePrivilege);
1576 return $hasCreatePrivilege;
1579 public function isConnected(): bool
1581 return isset($this->connections[ConnectionType::User->value]);
1584 /** @return string[] */
1585 private function getCurrentUserGrants(): array
1587 /** @var string[] $grants */
1588 $grants = $this->fetchResult('SHOW GRANTS FOR CURRENT_USER();');
1590 return $grants;
1594 * Get the current user and host
1596 * @return array<int, string> array of username and hostname
1598 public function getCurrentUserAndHost(): array
1600 if ($this->currentUserAndHost === null) {
1601 $user = $this->getCurrentUser();
1602 $this->currentUserAndHost = explode('@', $user);
1605 return $this->currentUserAndHost;
1609 * Get the current role and host.
1611 * @return array<int, array<int, string>> array of role and hostname
1613 public function getCurrentRolesAndHost(): array
1615 if ($this->currentRoleAndHost === null) {
1616 $roles = $this->getCurrentRoles();
1618 $this->currentRoleAndHost = array_map(static function (string $role) {
1619 return explode('@', $role);
1620 }, $roles);
1623 return $this->currentRoleAndHost;
1627 * Returns value for lower_case_table_names variable
1629 * @see https://mariadb.com/kb/en/server-system-variables/#lower_case_table_names
1630 * @see https://dev.mysql.com/doc/refman/en/server-system-variables.html#sysvar_lower_case_table_names
1632 * @psalm-return 0|1|2
1634 public function getLowerCaseNames(): int
1636 if ($this->lowerCaseTableNames === null) {
1637 $value = (int) $this->fetchValue('SELECT @@lower_case_table_names');
1638 $this->lowerCaseTableNames = $value >= 0 && $value <= 2 ? $value : 0;
1641 return $this->lowerCaseTableNames;
1645 * Connects to the database server.
1647 * @param ConnectionType|null $target How to store connection link, defaults to $connectionType
1649 public function connect(
1650 Server $currentServer,
1651 ConnectionType $connectionType,
1652 ConnectionType|null $target = null,
1653 ): Connection|null {
1654 $server = Config::getConnectionParams($currentServer, $connectionType);
1656 $target ??= $connectionType;
1658 // Do not show location and backtrace for connection errors
1659 $errorHandler = ErrorHandler::getInstance();
1660 $errorHandler->setHideLocation(true);
1661 try {
1662 $result = $this->extension->connect($server);
1663 } catch (ConnectionException $exception) {
1664 trigger_error($exception->getMessage(), E_USER_WARNING);
1666 return null;
1669 $errorHandler->setHideLocation(false);
1671 if ($result !== null) {
1672 $this->connections[$target->value] = $result;
1673 /* Run post connect for user connections */
1674 if ($target === ConnectionType::User) {
1675 $this->postConnect($currentServer);
1678 return $result;
1681 if ($connectionType === ConnectionType::ControlUser) {
1682 trigger_error(
1684 'Connection for controluser as defined in your configuration failed.',
1686 E_USER_WARNING,
1690 return null;
1694 * selects given database
1696 * @param string|DatabaseName $dbname database name to select
1698 public function selectDb(string|DatabaseName $dbname, ConnectionType $connectionType = ConnectionType::User): bool
1700 if (! isset($this->connections[$connectionType->value])) {
1701 return false;
1704 return $this->extension->selectDb($dbname, $this->connections[$connectionType->value]);
1708 * Prepare next result from multi_query
1710 public function nextResult(ConnectionType $connectionType = ConnectionType::User): ResultInterface|false
1712 if (! isset($this->connections[$connectionType->value])) {
1713 return false;
1716 // TODO: Figure out if we really need to check the return value of this function.
1717 if (! $this->extension->nextResult($this->connections[$connectionType->value])) {
1718 return false;
1721 return $this->extension->storeResult($this->connections[$connectionType->value]);
1725 * Returns a string representing the type of connection used
1727 * @return string|bool type of connection used
1729 public function getHostInfo(ConnectionType $connectionType = ConnectionType::User): string|bool
1731 if (! isset($this->connections[$connectionType->value])) {
1732 return false;
1735 return $this->extension->getHostInfo($this->connections[$connectionType->value]);
1739 * Returns the version of the MySQL protocol used
1741 * @return int|bool version of the MySQL protocol used
1743 public function getProtoInfo(ConnectionType $connectionType = ConnectionType::User): int|bool
1745 if (! isset($this->connections[$connectionType->value])) {
1746 return false;
1749 return $this->extension->getProtoInfo($this->connections[$connectionType->value]);
1753 * returns a string that represents the client library version
1755 * @return string MySQL client library version
1757 public function getClientInfo(): string
1759 return $this->extension->getClientInfo();
1763 * Returns last error message or an empty string if no errors occurred.
1765 public function getError(ConnectionType $connectionType = ConnectionType::User): string
1767 if (! isset($this->connections[$connectionType->value])) {
1768 return '';
1771 return $this->extension->getError($this->connections[$connectionType->value]);
1775 * returns the number of rows returned by last query
1776 * used with tryQuery as it accepts false
1778 * @param string $query query to run
1780 * @psalm-return int|numeric-string
1782 public function queryAndGetNumRows(string $query): string|int
1784 $result = $this->tryQuery($query);
1786 if (! $result) {
1787 return 0;
1790 return $result->numRows();
1794 * returns last inserted auto_increment id for given $link
1795 * or $GLOBALS['userlink']
1797 public function insertId(ConnectionType $connectionType = ConnectionType::User): int
1799 // If the primary key is BIGINT we get an incorrect result
1800 // (sometimes negative, sometimes positive)
1801 // and in the present function we don't know if the PK is BIGINT
1802 // so better play safe and use LAST_INSERT_ID()
1804 // When no controluser is defined, using mysqli_insert_id($link)
1805 // does not always return the last insert id due to a mixup with
1806 // the tracking mechanism, but this works:
1807 return (int) $this->fetchValue('SELECT LAST_INSERT_ID();', 0, $connectionType);
1811 * returns the number of rows affected by last query
1813 * @param bool $getFromCache whether to retrieve from cache
1815 * @psalm-return int|numeric-string
1817 public function affectedRows(
1818 ConnectionType $connectionType = ConnectionType::User,
1819 bool $getFromCache = true,
1820 ): int|string {
1821 if (! isset($this->connections[$connectionType->value])) {
1822 return -1;
1825 if ($getFromCache) {
1826 return $GLOBALS['cached_affected_rows'];
1829 return $this->extension->affectedRows($this->connections[$connectionType->value]);
1833 * returns metainfo for fields in $result
1835 * @param ResultInterface $result result set identifier
1837 * @return FieldMetadata[] meta info for fields in $result
1839 public function getFieldsMeta(ResultInterface $result): array
1841 $fields = $result->getFieldsMeta();
1843 if ($this->getLowerCaseNames() === 2) {
1845 * Fixup orgtable for lower_case_table_names = 2
1847 * In this setup MySQL server reports table name lower case
1848 * but we still need to operate on original case to properly
1849 * match existing strings
1851 foreach ($fields as $value) {
1852 if (
1853 $value->orgtable === '' ||
1854 mb_strtolower($value->orgtable) !== mb_strtolower($value->table)
1856 continue;
1859 $value->orgtable = $value->table;
1863 return $fields;
1867 * Returns properly quoted string for use in MySQL queries.
1869 * @param string $str string to be quoted
1871 * @psalm-return non-empty-string
1873 * @psalm-taint-escape sql
1875 public function quoteString(string $str, ConnectionType $connectionType = ConnectionType::User): string
1877 return "'" . $this->extension->escapeString($this->connections[$connectionType->value], $str) . "'";
1881 * Returns properly escaped string for use in MySQL LIKE clauses.
1882 * This method escapes only _, %, and /. It does not escape quotes or any other characters.
1884 * @param string $str string to be escaped
1886 * @return string a MySQL escaped LIKE string
1888 public function escapeMysqlWildcards(string $str): string
1890 return strtr($str, ['\\' => '\\\\', '_' => '\\_', '%' => '\\%']);
1894 * Checks if this database server is running on Amazon RDS.
1896 public function isAmazonRds(): bool
1898 if (SessionCache::has('is_amazon_rds')) {
1899 return (bool) SessionCache::get('is_amazon_rds');
1902 $sql = 'SELECT @@basedir';
1903 $result = (string) $this->fetchValue($sql);
1904 $rds = str_starts_with($result, '/rdsdbbin/');
1905 SessionCache::set('is_amazon_rds', $rds);
1907 return $rds;
1911 * Gets SQL for killing a process.
1913 * @param int $process Process ID
1915 public function getKillQuery(int $process): string
1917 if ($this->isAmazonRds()) {
1918 return 'CALL mysql.rds_kill(' . $process . ');';
1921 return 'KILL ' . $process . ';';
1925 * Get a table with database name and table name
1927 * @param string $dbName DB name
1928 * @param string $tableName Table name
1930 public function getTable(string $dbName, string $tableName): Table
1932 return new Table($tableName, $dbName, $this);
1936 * returns collation of given db
1938 * @param string $db name of db
1940 * @return string collation of $db
1942 public function getDbCollation(string $db): string
1944 if (! $this->config->selectedServer['DisableIS']) {
1945 // this is slow with thousands of databases
1946 $sql = 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA'
1947 . ' WHERE SCHEMA_NAME = ' . $this->quoteString($db)
1948 . ' LIMIT 1';
1950 return (string) $this->fetchValue($sql);
1953 $this->selectDb($db);
1954 $return = (string) $this->fetchValue('SELECT @@collation_database');
1955 if ($db !== Current::$database) {
1956 $this->selectDb(Current::$database);
1959 return $return;
1963 * returns default server collation from show variables
1965 public function getServerCollation(): string
1967 return (string) $this->fetchValue('SELECT @@collation_server');
1971 * Server version as number
1973 * @example 80011
1975 public function getVersion(): int
1977 return $this->versionInt;
1981 * Server version
1983 public function getVersionString(): string
1985 return $this->versionString;
1989 * Server version comment
1991 public function getVersionComment(): string
1993 return $this->versionComment;
1997 * Whether connection is MariaDB
1999 public function isMariaDB(): bool
2001 return $this->isMariaDb;
2005 * Whether connection is PerconaDB
2007 public function isPercona(): bool
2009 return $this->isPercona;
2013 * Set version
2015 * @param array $version Database version information
2016 * @phpstan-param array<array-key, mixed> $version
2018 public function setVersion(array $version): void
2020 $this->versionString = $version['@@version'] ?? '';
2021 $this->versionInt = Utilities::versionToInt($this->versionString);
2022 $this->versionComment = $version['@@version_comment'] ?? '';
2024 $this->isMariaDb = stripos($this->versionString, 'mariadb') !== false;
2025 $this->isPercona = stripos($this->versionComment, 'percona') !== false;
2029 * Prepare an SQL statement for execution.
2031 * @param string $query The query, as a string.
2033 public function prepare(string $query, ConnectionType $connectionType = ConnectionType::User): Statement|null
2035 return $this->extension->prepare($this->connections[$connectionType->value], $query);
2038 public function getDatabaseList(): ListDatabase
2040 if ($this->databaseList === null) {
2041 $this->databaseList = new ListDatabase($this, $this->config, new UserPrivilegesFactory($this));
2044 return $this->databaseList;
2048 * Returns the number of warnings from the last query.
2050 private function getWarningCount(ConnectionType $connectionType): int
2052 if (! isset($this->connections[$connectionType->value])) {
2053 return 0;
2056 return $this->extension->getWarningCount($this->connections[$connectionType->value]);