3 declare(strict_types
=1);
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
;
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
;
46 use function is_array
;
48 use function mb_strtolower
;
49 use function microtime
;
53 use function str_contains
;
54 use function str_replace
;
55 use function str_starts_with
;
57 use function strnatcasecmp
;
58 use function strtolower
;
59 use function strtoupper
;
63 use function trigger_error
;
68 use const E_USER_WARNING
;
77 * Main interface for database interactions
79 class DatabaseInterface
implements DbalInterface
81 public static self|
null $instance = null;
84 * Force STORE_RESULT method, ignored by classic MySQL.
86 public const QUERY_BUFFERED
= 0;
89 * Do not read all rows immediately.
91 public const QUERY_UNBUFFERED
= 2;
94 * Get session variable.
96 public const GETVAR_SESSION
= 1;
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 */
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;
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(
176 ConnectionType
$connectionType = ConnectionType
::User
,
177 int $options = self
::QUERY_BUFFERED
,
178 bool $cacheAffectedRows = true,
180 $result = $this->tryQuery($query, $connectionType, $options, $cacheAffectedRows);
183 // The following statement will exit
184 Generator
::mysqlDie($this->getError($connectionType), $query);
186 ResponseRenderer
::getInstance()->callExit();
192 public function getCache(): 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(
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
])) {
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(
232 $errorMessage !== '' ?
$errorMessage : null,
234 $this->lastQueryExecutionTime
,
236 if ($this->config
->config
->debug
->sqllog
) {
237 openlog('phpMyAdmin', LOG_NDELAY | LOG_PID
, LOG_USER
);
242 'SQL[%s?route=%s]: %0.3f(W:%d,C:%s,L:0x%02X) > %s',
243 basename($_SERVER['SCRIPT_NAME']),
245 $this->lastQueryExecutionTime
,
246 $this->getWarningCount($connectionType),
247 $cacheAffectedRows ?
'y' : 'n',
248 $connectionType->value
,
256 if ($result !== false && Tracker
::isActive()) {
257 Tracker
::handleQuery($query);
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
,
272 if (! isset($this->connections
[$connectionType->value
])) {
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
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
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 === '') {
326 /** @var array<int, string> $tables */
327 $tables = $this->fetchResult(
328 'SHOW TABLES FROM ' . Util
::backquote($database) . ';',
333 if ($this->config
->settings
['NaturalOrder']) {
334 usort($tables, strnatcasecmp(...));
341 * returns array of all tables in given db or dbs
342 * this function expects unquoted names:
344 * WRONG: `my_database`
345 * WRONG: my\_database
346 * if $tbl_is_group is true, $table is used as filter for table names
349 * $dbi->getTablesFull('my_database');
350 * $dbi->getTablesFull('my_database', 'my_table'));
351 * $dbi->getTablesFull('my_database', 'my_tables_', true));
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(
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
,
378 if ($limitCount === true) {
379 $limitCount = $this->config
->settings
['MaxTableList'];
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']) {
396 if ($table !== [] && $table !== '') {
397 if (is_array($table)) {
398 $sqlWhereTable = QueryGenerator
::getTableNameConditionForMultiple(
399 array_map($this->quoteString(...), $table),
402 $sqlWhereTable = QueryGenerator
::getTableNameCondition(
403 $this->quoteString($tableIsGroup ?
$this->escapeMysqlWildcards($table) : $table),
409 $sqlWhereTable .= QueryGenerator
::getTableTypeCondition($tableType);
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);
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(
431 ['TABLE_SCHEMA', 'TABLE_NAME'],
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') {
444 if (! StorageEngine
::hasMroongaEngine()) {
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) {
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
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)]
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 !== '')) {
505 if (($table !== '' && $table !== []) ||
$tableIsGroup) {
506 if (is_array($table)) {
507 $sql .= ' `Name` IN ('
511 fn (string $string): string => $this->quoteString($string, $connectionType),
516 $sql .= ' `Name` LIKE '
517 . $this->quoteString($this->escapeMysqlWildcards($table) . '%', $connectionType);
523 if ($tableType !== null && $tableType !== '') {
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') {
546 if (! StorageEngine
::hasMroongaEngine()) {
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
558 if ($sortBy === 'Name' && $this->config
->settings
['NaturalOrder']) {
559 uksort($eachTables, strnatcasecmp(...));
561 if ($sortOrder === 'DESC') {
562 $eachTables = array_reverse($eachTables);
565 // Prepare to sort by creating array of the selected sort
566 // value to pass to array_multisort
568 // Size = Data_length + Index_length
570 if ($sortBy === 'Data_length') {
571 foreach ($eachTables as $tableName => $tableData) {
572 $sortValues[$tableName] = strtolower(
573 (string) ($tableData['Data_length']
574 +
$tableData['Index_length']),
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);
589 array_multisort($sortValues, SORT_ASC
, $eachTables, $keys);
592 $eachTables = array_combine($keys, $eachTables);
595 // cleanup the temporary sort array
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);
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']
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,
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
652 if (! $this->config
->settings
['NaturalOrder']) {
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(
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
684 array_keys($databases),
685 (array) $this->getDatabaseList(),
687 foreach ($drops as $drop) {
688 unset($databases[$drop]);
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);
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;
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'];
737 * apply limit and order manually now
738 * (caused by older MySQL < 5 or NaturalOrder config)
740 if ($applyLimitAndOrderManual) {
743 static fn ($a, $b): int => Utilities
::usortComparisonCallback($a, $b, $sortBy, $sortOrder),
750 $databases = array_slice($databases, $limitOffset, $limitCount);
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
767 public function getColumnsFull(
768 string|
null $database = null,
769 string|
null $table = null,
770 string|
null $column = null,
771 ConnectionType
$connectionType = ConnectionType
::User
,
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);
785 if ($database === null) {
786 foreach ($this->getDatabaseList() as $database) {
787 $columns[$database] = $this->getColumnsFull($database, null, null, $connectionType);
793 if ($table === null) {
794 $tables = $this->getTables($database);
795 foreach ($tables as $table) {
796 $columns[$table] = $this->getColumnsFull($database, $table, null, $connectionType);
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);
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(
836 ConnectionType
$connectionType = ConnectionType
::User
,
837 ): ColumnFull|Column|
null {
838 $sql = QueryGenerator
::getColumnsSql(
841 $this->quoteString($this->escapeMysqlWildcards($column)),
844 /** @var (string|null)[][] $fields */
845 $fields = $this->fetchResult($sql, 'Field', null, $connectionType);
851 * Collation: string|null,
854 * Default: string|null,
856 * Privileges: string,
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(
883 ConnectionType
$connectionType = ConnectionType
::User
,
885 $sql = QueryGenerator
::getColumnsSql($database, $table, null, $full);
886 /** @var (string|null)[][] $fields */
887 $fields = $this->fetchResult($sql, 'Field', null, $connectionType);
893 * Collation: string|null,
896 * Default: string|null,
898 * Privileges: string,
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(
921 if ($fields === []) {
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'])) {
932 foreach ($indexes as $index) {
933 if (! $index->hasColumn((string) $field)) {
937 $indexColumns = $index->getColumns();
938 if ($indexColumns[$field]->getSeqInIndex() <= 1) {
942 $fields[$field]['Key'] = $index->isUnique() ?
'UNI' : 'MUL';
950 * @psalm-param array{
953 * Collation: string|null,
956 * Default: string|null,
958 * Privileges: string,
960 * }[] $fields column array indexed by their names
962 * @return (ColumnFull|Column)[]
964 private function convertToColumns(array $fields, bool $full = false): array
967 foreach ($fields as $field => $column) {
968 $columns[$field] = $full ?
new ColumnFull(
971 $column['Collation'],
972 $column['Null'] === 'YES',
976 $column['Privileges'],
981 $column['Null'] === 'YES',
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
999 public function getColumnNames(
1002 ConnectionType
$connectionType = ConnectionType
::User
,
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{
1019 * Non_unique: '0'|'1',
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',
1030 * Index_comment: string,
1033 * Expression?: string|null
1036 public function getTableIndexes(
1039 ConnectionType
$connectionType = ConnectionType
::User
,
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(
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',
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(
1077 ConnectionType
$connectionType = ConnectionType
::User
,
1079 $currentValue = $this->getVariable($var, self
::GETVAR_SESSION
, $connectionType);
1080 if ($currentValue == $value) {
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);
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
,
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)
1168 if ($result === false) {
1170 __('Failed to set configured collation connection!'),
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
1184 * $sql = 'SELECT `name` FROM `user` WHERE `id` = 123';
1185 * $user_name = $dbi->fetchValue($sql);
1187 * // $user_name = 'John Doe'
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(
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) {
1205 return $result->fetchValue($field);
1209 * Returns only the first row from the result or null if result is empty.
1212 * $sql = 'SELECT * FROM `user` WHERE `id` = 123';
1213 * $user = $dbi->fetchSingleRow($sql);
1215 * // $user = array('id' => 123, 'name' => 'John Doe')
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(
1226 string $type = DbalInterface
::FETCH_ASSOC
,
1227 ConnectionType
$connectionType = ConnectionType
::User
,
1229 $result = $this->tryQuery($query, $connectionType, self
::QUERY_BUFFERED
, false);
1230 if ($result === false) {
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
1266 * $sql = 'SELECT * FROM `user`';
1267 * $users = $dbi->fetchResult($sql);
1269 * // $users[] = array('id' => 123, 'name' => 'John Doe')
1271 * $sql = 'SELECT `id`, `name` FROM `user`';
1272 * $users = $dbi->fetchResult($sql, 'id');
1274 * // $users['123'] = array('id' => 123, 'name' => 'John Doe')
1276 * $sql = 'SELECT `id`, `name` FROM `user`';
1277 * $users = $dbi->fetchResult($sql, 0);
1279 * // $users['123'] = array(0 => 123, 1 => 'John Doe')
1281 * $sql = 'SELECT `id`, `name` FROM `user`';
1282 * $users = $dbi->fetchResult($sql, 'id', 'name');
1284 * $users = $dbi->fetchResult($sql, 0, 1);
1286 * // $users['123'] = 'John Doe'
1288 * $sql = 'SELECT `name` FROM `user`';
1289 * $users = $dbi->fetchResult($sql);
1291 * // $users[] = 'John Doe'
1293 * $sql = 'SELECT `group`, `name` FROM `user`'
1294 * $users = $dbi->fetchResult($sql, array('group', null), 'name');
1296 * // $users['admin'][] = 'John Doe'
1298 * $sql = 'SELECT `group`, `name` FROM `user`'
1299 * $users = $dbi->fetchResult($sql, array('group', 'name'), 'id');
1301 * // $users['admin']['John Doe'] = '123'
1304 * @param string $query query to execute
1305 * @param string|int|mixed[]|null $key field-name or offset
1306 * used as key for array
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(
1314 string|
int|
array|
null $key = null,
1315 string|
int|
null $value = null,
1316 ConnectionType
$connectionType = ConnectionType
::User
,
1320 $result = $this->tryQuery($query, $connectionType, self
::QUERY_BUFFERED
, false);
1322 // return empty array if result is empty or false
1323 if ($result === false) {
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[];
1345 if (! isset($resultTarget[$row[$keyIndex]])) {
1346 $resultTarget[$row[$keyIndex]] = [];
1349 $resultTarget =& $resultTarget[$row[$keyIndex]];
1352 $resultTarget = $this->fetchValueOrValueByIndex($row, $value);
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);
1373 * Get supported SQL compatibility modes
1375 * @return string[] supported SQL compatibility modes
1377 public function getCompatibilities(): array
1388 // removed; in MySQL 5.0.33, this produces exports that
1389 // can't be read by POSTGRESQL (see our bug #1596328)
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) {
1408 while ($row = $result->fetchAssoc()) {
1409 $warnings[] = Warning
::fromArray($row);
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);
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) {
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') {
1462 $role = array_map('trim', explode(',', str_replace('`', '', $role)));
1463 SessionCache
::set('mysql_cur_role', $role);
1468 public function isSuperUser(): bool
1470 if (SessionCache
::has('is_superuser')) {
1471 return (bool) SessionCache
::get('is_superuser');
1474 if (! $this->isConnected()) {
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()) {
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;
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) {
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()) {
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;
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) {
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();');
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);
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);
1662 $result = $this->extension
->connect($server);
1663 } catch (ConnectionException
$exception) {
1664 trigger_error($exception->getMessage(), E_USER_WARNING
);
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);
1681 if ($connectionType === ConnectionType
::ControlUser
) {
1684 'Connection for controluser as defined in your configuration failed.',
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
])) {
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
])) {
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
])) {
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
])) {
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
])) {
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
])) {
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);
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,
1821 if (! isset($this->connections
[$connectionType->value
])) {
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) {
1853 $value->orgtable
=== '' ||
1854 mb_strtolower($value->orgtable
) !== mb_strtolower($value->table
)
1859 $value->orgtable
= $value->table
;
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);
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)
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);
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
1975 public function getVersion(): int
1977 return $this->versionInt
;
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
;
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
])) {
2056 return $this->extension
->getWarningCount($this->connections
[$connectionType->value
]);