Files
adminer/adminer/drivers/mysql.inc.php
2025-03-28 15:41:38 +01:00

1093 lines
37 KiB
PHP

<?php
namespace Adminer;
$drivers = array("server" => "MySQL / MariaDB") + $drivers;
if (!defined('Adminer\DRIVER')) {
define('Adminer\DRIVER', "server"); // server - backwards compatibility
// MySQLi supports everything, MySQL doesn't support multiple result sets, PDO_MySQL doesn't support orgtable
if (extension_loaded("mysqli") && $_GET["ext"] != "pdo") {
class Db extends \MySQLi {
public string $extension = "MySQLi", $flavor = '';
function __construct() {
parent::init();
}
/** @see https://php.net/mysqli.construct */
function connect($server = "", $username = "", $password = "", $database = null, $port = null, $socket = null) {
global $adminer;
mysqli_report(MYSQLI_REPORT_OFF); // stays between requests, not required since PHP 5.3.4
list($host, $port) = explode(":", $server, 2); // part after : is used for port or socket
$ssl = $adminer->connectSsl();
if ($ssl) {
$this->ssl_set($ssl['key'], $ssl['cert'], $ssl['ca'], '', '');
}
$return = @$this->real_connect(
($server != "" ? $host : null),
($server . $username != "" ? $username : null),
($server . $username . $password != "" ? $password : null),
$database,
(is_numeric($port) ? intval($port) : null),
(!is_numeric($port) ? $port : $socket),
($ssl ? ($ssl['verify'] !== false ? 2048 : 64) : 0) // 2048 - MYSQLI_CLIENT_SSL, 64 - MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT (not available before PHP 5.6.16)
);
$this->options(MYSQLI_OPT_LOCAL_INFILE, false);
return $return;
}
function set_charset($charset) {
if (parent::set_charset($charset)) {
return true;
}
// the client library may not support utf8mb4
parent::set_charset('utf8');
return $this->query("SET NAMES $charset");
}
function result($query, $field = 0) {
$result = $this->query($query);
if (!is_object($result)) {
return false;
}
$row = $result->fetch_array();
return ($row ? $row[$field] : false);
}
function quote(string $string): string {
return "'" . $this->escape_string($string) . "'";
}
}
} elseif (extension_loaded("mysql") && !((ini_bool("sql.safe_mode") || ini_bool("mysql.allow_local_infile")) && extension_loaded("pdo_mysql"))) {
class Db extends SqlDb {
/** @var resource */ private $link;
function connect(string $server, string $username, string $password): bool {
if (ini_bool("mysql.allow_local_infile")) {
$this->error = lang('Disable %s or enable %s or %s extensions.', "'mysql.allow_local_infile'", "MySQLi", "PDO_MySQL");
return false;
}
$this->link = @mysql_connect(
($server != "" ? $server : ini_get("mysql.default_host")),
("$server$username" != "" ? $username : ini_get("mysql.default_user")),
("$server$username$password" != "" ? $password : ini_get("mysql.default_password")),
true,
131072 // CLIENT_MULTI_RESULTS for CALL
);
if ($this->link) {
$this->server_info = mysql_get_server_info($this->link);
} else {
$this->error = mysql_error();
}
return (bool) $this->link;
}
/** Set the client character set */
function set_charset(string $charset): bool {
if (function_exists('mysql_set_charset')) {
if (mysql_set_charset($charset, $this->link)) {
return true;
}
// the client library may not support utf8mb4
mysql_set_charset('utf8', $this->link);
}
return $this->query("SET NAMES $charset");
}
function quote(string $string): string {
return "'" . mysql_real_escape_string($string, $this->link) . "'";
}
function select_db(string $database): bool {
return mysql_select_db($database, $this->link);
}
function query(string $query, bool $unbuffered = false) {
$result = @($unbuffered ? mysql_unbuffered_query($query, $this->link) : mysql_query($query, $this->link)); // @ - mute mysql.trace_mode
$this->error = "";
if (!$result) {
$this->errno = mysql_errno($this->link);
$this->error = mysql_error($this->link);
return false;
}
if ($result === true) {
$this->affected_rows = mysql_affected_rows($this->link);
$this->info = mysql_info($this->link);
return true;
}
return new Result($result);
}
}
class Result {
public int $num_rows; // number of rows in the result
/** @var resource */ private $result;
private int $offset = 0;
/** @param resource $result */
function __construct($result) {
$this->result = $result;
$this->num_rows = mysql_num_rows($result);
}
/** Fetch next row as associative array
* @return array<?string>|false
*/
function fetch_assoc() {
return mysql_fetch_assoc($this->result);
}
/** Fetch next row as numbered array
* @return list<?string>|false
*/
function fetch_row() {
return mysql_fetch_row($this->result);
}
/** Fetch next field
* @return \stdClass properties: name, type (0 number, 15 varchar, 254 char), charsetnr (63 binary); optionally: table, orgtable, orgname
*/
function fetch_field(): \stdClass {
$return = mysql_fetch_field($this->result, $this->offset++); // offset required under certain conditions
$return->orgtable = $return->table;
$return->charsetnr = ($return->blob ? 63 : 0);
return $return;
}
/** Free result set */
function __destruct() {
mysql_free_result($this->result);
}
}
} elseif (extension_loaded("pdo_mysql")) {
class Db extends PdoDb {
public string $extension = "PDO_MySQL";
function connect(string $server, string $username, string $password): bool {
global $adminer;
$options = array(\PDO::MYSQL_ATTR_LOCAL_INFILE => false);
$ssl = $adminer->connectSsl();
if ($ssl) {
if ($ssl['key']) {
$options[\PDO::MYSQL_ATTR_SSL_KEY] = $ssl['key'];
}
if ($ssl['cert']) {
$options[\PDO::MYSQL_ATTR_SSL_CERT] = $ssl['cert'];
}
if ($ssl['ca']) {
$options[\PDO::MYSQL_ATTR_SSL_CA] = $ssl['ca'];
}
if (isset($ssl['verify'])) {
$options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $ssl['verify'];
}
}
$this->dsn(
"mysql:charset=utf8;host=" . str_replace(":", ";unix_socket=", preg_replace('~:(\d)~', ';port=\1', $server)),
$username,
$password,
$options
);
return true;
}
function set_charset($charset) {
return $this->query("SET NAMES $charset"); // charset in DSN is ignored before PHP 5.3.6
}
function select_db(string $database): bool {
// database selection is separated from the connection so dbname in DSN can't be used
return $this->query("USE " . idf_escape($database));
}
function query(string $query, bool $unbuffered = false) {
$this->pdo->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, !$unbuffered);
return parent::query($query, $unbuffered);
}
}
}
class Driver extends SqlDriver {
/** @var list<string> */ static array $possibleDrivers = array("MySQLi", "MySQL", "PDO_MySQL");
static string $jush = "sql"; // JUSH identifier
/** @var list<string> */ public array $unsigned = array("unsigned", "zerofill", "unsigned zerofill");
/** @var list<string> */ public array $operators = array("=", "<", ">", "<=", ">=", "!=", "LIKE", "LIKE %%", "REGEXP", "IN", "FIND_IN_SET", "IS NULL", "NOT LIKE", "NOT REGEXP", "NOT IN", "IS NOT NULL", "SQL");
/** @var list<string> */ public array $functions = array("char_length", "date", "from_unixtime", "lower", "round", "floor", "ceil", "sec_to_time", "time_to_sec", "upper");
/** @var list<string> */ public array $grouping = array("avg", "count", "count distinct", "group_concat", "max", "min", "sum");
function __construct(Db $connection) {
parent::__construct($connection);
$this->types = array(
lang('Numbers') => array("tinyint" => 3, "smallint" => 5, "mediumint" => 8, "int" => 10, "bigint" => 20, "decimal" => 66, "float" => 12, "double" => 21),
lang('Date and time') => array("date" => 10, "datetime" => 19, "timestamp" => 19, "time" => 10, "year" => 4),
lang('Strings') => array("char" => 255, "varchar" => 65535, "tinytext" => 255, "text" => 65535, "mediumtext" => 16777215, "longtext" => 4294967295),
lang('Lists') => array("enum" => 65535, "set" => 64),
lang('Binary') => array("bit" => 20, "binary" => 255, "varbinary" => 65535, "tinyblob" => 255, "blob" => 65535, "mediumblob" => 16777215, "longblob" => 4294967295),
lang('Geometry') => array("geometry" => 0, "point" => 0, "linestring" => 0, "polygon" => 0, "multipoint" => 0, "multilinestring" => 0, "multipolygon" => 0, "geometrycollection" => 0),
);
$this->insertFunctions = array(
"char" => "md5/sha1/password/encrypt/uuid",
"binary" => "md5/sha1",
"date|time" => "now",
);
$this->editFunctions = array(
number_type() => "+/-",
"date" => "+ interval/- interval",
"time" => "addtime/subtime",
"char|text" => "concat",
);
if (min_version('5.7.8', 10.2, $connection)) {
$this->types[lang('Strings')]["json"] = 4294967295;
}
if (min_version('', 10.7, $connection)) {
$this->types[lang('Strings')]["uuid"] = 128;
$this->insertFunctions['uuid'] = 'uuid';
}
if (min_version(9, '', $connection)) {
$this->types[lang('Numbers')]["vector"] = 16383;
$this->insertFunctions['vector'] = 'string_to_vector';
}
if (min_version(5.7, 10.2, $connection)) {
$this->generated = array("STORED", "VIRTUAL");
}
}
function unconvertFunction(array $field) {
return (preg_match("~binary~", $field["type"]) ? "<code class='jush-sql'>UNHEX</code>"
: ($field["type"] == "bit" ? doc_link(array('sql' => 'bit-value-literals.html'), "<code>b''</code>")
: (preg_match("~geometry|point|linestring|polygon~", $field["type"]) ? "<code class='jush-sql'>GeomFromText</code>"
: "")));
}
function insert(string $table, array $set) {
return ($set ? parent::insert($table, $set) : queries("INSERT INTO " . table($table) . " ()\nVALUES ()"));
}
function insertUpdate(string $table, array $rows, array $primary) {
$columns = array_keys(reset($rows));
$prefix = "INSERT INTO " . table($table) . " (" . implode(", ", $columns) . ") VALUES\n";
$values = array();
foreach ($columns as $key) {
$values[$key] = "$key = VALUES($key)";
}
$suffix = "\nON DUPLICATE KEY UPDATE " . implode(", ", $values);
$values = array();
$length = 0;
foreach ($rows as $set) {
$value = "(" . implode(", ", $set) . ")";
if ($values && (strlen($prefix) + $length + strlen($value) + strlen($suffix) > 1e6)) { // 1e6 - default max_allowed_packet
if (!queries($prefix . implode(",\n", $values) . $suffix)) {
return false;
}
$values = array();
$length = 0;
}
$values[] = $value;
$length += strlen($value) + 2; // 2 - strlen(",\n")
}
return queries($prefix . implode(",\n", $values) . $suffix);
}
function slowQuery(string $query, int $timeout) {
if (min_version('5.7.8', '10.1.2')) {
if ($this->conn->flavor == 'maria') {
return "SET STATEMENT max_statement_time=$timeout FOR $query";
} elseif (preg_match('~^(SELECT\b)(.+)~is', $query, $match)) {
return "$match[1] /*+ MAX_EXECUTION_TIME(" . ($timeout * 1000) . ") */ $match[2]";
}
}
}
function convertSearch(string $idf, array $val, array $field): string {
return (preg_match('~char|text|enum|set~', $field["type"]) && !preg_match("~^utf8~", $field["collation"]) && preg_match('~[\x80-\xFF]~', $val['val'])
? "CONVERT($idf USING " . charset($this->conn) . ")"
: $idf
);
}
function warnings() {
$result = $this->conn->query("SHOW WARNINGS");
if ($result && $result->num_rows) {
ob_start();
print_select_result($result); // print_select_result() usually needs to print a big table progressively
return ob_get_clean();
}
}
function tableHelp(string $name, bool $is_view = false) {
$maria = ($this->conn->flavor == 'maria');
if (information_schema(DB)) {
return strtolower("information-schema-" . ($maria ? "$name-table/" : str_replace("_", "-", $name) . "-table.html"));
}
if (DB == "mysql") {
return ($maria ? "mysql$name-table/" : "system-schema.html"); //! more precise link
}
}
function hasCStyleEscapes(): bool {
static $c_style;
if ($c_style === null) {
$sql_mode = get_val("SHOW VARIABLES LIKE 'sql_mode'", 1, $this->conn);
$c_style = (strpos($sql_mode, 'NO_BACKSLASH_ESCAPES') === false);
}
return $c_style;
}
function engines(): array {
$return = array();
foreach (get_rows("SHOW ENGINES") as $row) {
if (preg_match("~YES|DEFAULT~", $row["Support"])) {
$return[] = $row["Engine"];
}
}
return $return;
}
}
/** Escape database identifier */
function idf_escape(string $idf): string {
return "`" . str_replace("`", "``", $idf) . "`";
}
/** Get escaped table name */
function table(string $idf): string {
return idf_escape($idf);
}
/** Connect to the database
* @param array{string, string, string} $credentials [$server, $username, $password]
* @return string|Db string for error
*/
function connect(array $credentials) {
global $drivers;
$connection = new Db;
if ($connection->connect($credentials[0], $credentials[1], $credentials[2])) {
$connection->set_charset(charset($connection));
$connection->query("SET sql_quote_show_create = 1, autocommit = 1");
$connection->flavor = (preg_match('~MariaDB~', $connection->server_info) ? 'maria' : 'mysql');
$drivers[DRIVER] = ($connection->flavor == 'maria' ? "MariaDB" : "MySQL");
return $connection;
}
$return = $connection->error;
if (function_exists('iconv') && !is_utf8($return) && strlen($s = iconv("windows-1250", "utf-8", $return)) > strlen($return)) { // windows-1250 - most common Windows encoding
$return = $s;
}
return $return;
}
/** Get cached list of databases
* @return list<string>
*/
function get_databases(bool $flush): array {
// SHOW DATABASES can take a very long time so it is cached
$return = get_session("dbs");
if ($return === null) {
$query = "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA ORDER BY SCHEMA_NAME"; // SHOW DATABASES can be disabled by skip_show_database
$return = ($flush ? slow_query($query) : get_vals($query));
restart_session();
set_session("dbs", $return);
stop_session();
}
return $return;
}
/** Formulate SQL query with limit
* @param string $query everything after SELECT
* @param string $where including WHERE
*/
function limit(string $query, string $where, int $limit, int $offset = 0, string $separator = " "): string {
return " $query$where" . ($limit !== null ? $separator . "LIMIT $limit" . ($offset ? " OFFSET $offset" : "") : "");
}
/** Formulate SQL modification query with limit 1
* @param string $query everything after UPDATE or DELETE
*/
function limit1(string $table, string $query, string $where, string $separator = "\n"): string {
return limit($query, $where, 1, 0, $separator);
}
/** Get database collation
* @param string[][] $collations result of collations()
*/
function db_collation(string $db, array $collations): ?string {
$return = null;
$create = get_val("SHOW CREATE DATABASE " . idf_escape($db), 1);
if (preg_match('~ COLLATE ([^ ]+)~', $create, $match)) {
$return = $match[1];
} elseif (preg_match('~ CHARACTER SET ([^ ]+)~', $create, $match)) {
// default collation
$return = $collations[$match[1]][-1];
}
return $return;
}
/** Get logged user */
function logged_user(): string {
return get_val("SELECT USER()");
}
/** Get tables list
* @return string[] [$name => $type]
*/
function tables_list(): array {
return get_key_vals("SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME");
}
/** Count tables in all databases
* @param list<string> $databases
* @return int[] [$db => $tables]
*/
function count_tables(array $databases): array {
$return = array();
foreach ($databases as $db) {
$return[$db] = count(get_vals("SHOW TABLES IN " . idf_escape($db)));
}
return $return;
}
/** Get table status
* @param bool $fast return only "Name", "Engine" and "Comment" fields
* @return TableStatus[]
*/
function table_status(string $name = "", bool $fast = false): array {
$return = array();
foreach (
get_rows(
$fast
? "SELECT TABLE_NAME AS Name, ENGINE AS Engine, TABLE_COMMENT AS Comment FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() " . ($name != "" ? "AND TABLE_NAME = " . q($name) : "ORDER BY Name")
: "SHOW TABLE STATUS" . ($name != "" ? " LIKE " . q(addcslashes($name, "%_\\")) : "")
) as $row
) {
if ($row["Engine"] == "InnoDB") {
// ignore internal comment, unnecessary since MySQL 5.1.21
$row["Comment"] = preg_replace('~(?:(.+); )?InnoDB free: .*~', '\1', $row["Comment"]);
}
if (!isset($row["Engine"])) {
$row["Comment"] = "";
}
if ($name != "") {
// MariaDB: Table name is returned as lowercase on macOS, so we fix it here.
$row["Name"] = $name;
}
$return[$row["Name"]] = $row;
}
return $return;
}
/** Find out whether the identifier is view
* @param TableStatus $table_status
*/
function is_view(array $table_status): bool {
return $table_status["Engine"] === null;
}
/** Check if table supports foreign keys
* @param TableStatus $table_status
*/
function fk_support(array $table_status): bool {
return preg_match('~InnoDB|IBMDB2I' . (min_version(5.6) ? '|NDB' : '') . '~i', $table_status["Engine"]);
}
/** Get information about fields
* @return Field[]
*/
function fields(string $table): array {
global $connection;
$maria = ($connection->flavor == 'maria');
$return = array();
foreach (get_rows("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = " . q($table) . " ORDER BY ORDINAL_POSITION") as $row) {
$field = $row["COLUMN_NAME"];
$type = $row["COLUMN_TYPE"];
$generation = $row["GENERATION_EXPRESSION"];
$extra = $row["EXTRA"];
// https://mariadb.com/kb/en/library/show-columns/, https://github.com/vrana/adminer/pull/359#pullrequestreview-276677186
preg_match('~^(VIRTUAL|PERSISTENT|STORED)~', $extra, $generated);
preg_match('~^([^( ]+)(?:\((.+)\))?( unsigned)?( zerofill)?$~', $type, $match_type);
$default = $row["COLUMN_DEFAULT"];
if ($default != "") {
$is_text = preg_match('~text|json~', $match_type[1]);
if (!$maria && $is_text) {
// default value a'b of text column is stored as _utf8mb4\'a\\\'b\' in MySQL
$default = preg_replace("~^(_\w+)?('.*')$~", '\2', stripslashes($default));
}
if ($maria || $is_text) {
$default = ($default == "NULL" ? null : preg_replace_callback("~^'(.*)'$~", function ($match) {
return stripslashes(str_replace("''", "'", $match[1]));
}, $default));
}
if (!$maria && preg_match('~binary~', $match_type[1]) && preg_match('~^0x(\w*)$~', $default, $match)) {
$default = pack("H*", $match[1]);
}
}
$return[$field] = array(
"field" => $field,
"full_type" => $type,
"type" => $match_type[1],
"length" => $match_type[2],
"unsigned" => ltrim($match_type[3] . $match_type[4]),
"default" => ($generated
? ($maria ? $generation : stripslashes($generation))
: $default
),
"null" => ($row["IS_NULLABLE"] == "YES"),
"auto_increment" => ($extra == "auto_increment"),
"on_update" => (preg_match('~\bon update (\w+)~i', $extra, $match) ? $match[1] : ""), //! available since MySQL 5.1.23
"collation" => $row["COLLATION_NAME"],
"privileges" => array_flip(explode(",", "$row[PRIVILEGES],where,order")),
"comment" => $row["COLUMN_COMMENT"],
"primary" => ($row["COLUMN_KEY"] == "PRI"),
"generated" => ($generated[1] == "PERSISTENT" ? "STORED" : $generated[1]),
);
}
return $return;
}
/** Get table indexes
* @return Index[]
*/
function indexes(string $table, ?Db $connection2 = null): array {
$return = array();
foreach (get_rows("SHOW INDEX FROM " . table($table), $connection2) as $row) {
$name = $row["Key_name"];
$return[$name]["type"] = ($name == "PRIMARY" ? "PRIMARY" : ($row["Index_type"] == "FULLTEXT" ? "FULLTEXT" : ($row["Non_unique"] ? ($row["Index_type"] == "SPATIAL" ? "SPATIAL" : "INDEX") : "UNIQUE")));
$return[$name]["columns"][] = $row["Column_name"];
$return[$name]["lengths"][] = ($row["Index_type"] == "SPATIAL" ? null : $row["Sub_part"]);
$return[$name]["descs"][] = null;
}
return $return;
}
/** Get foreign keys in table
* @return ForeignKey[]
*/
function foreign_keys(string $table): array {
global $driver;
static $pattern = '(?:`(?:[^`]|``)+`|"(?:[^"]|"")+")';
$return = array();
$create_table = get_val("SHOW CREATE TABLE " . table($table), 1);
if ($create_table) {
preg_match_all(
"~CONSTRAINT ($pattern) FOREIGN KEY ?\\(((?:$pattern,? ?)+)\\) REFERENCES ($pattern)(?:\\.($pattern))? \\(((?:$pattern,? ?)+)\\)(?: ON DELETE ($driver->onActions))?(?: ON UPDATE ($driver->onActions))?~",
$create_table,
$matches,
PREG_SET_ORDER
);
foreach ($matches as $match) {
preg_match_all("~$pattern~", $match[2], $source);
preg_match_all("~$pattern~", $match[5], $target);
$return[idf_unescape($match[1])] = array(
"db" => idf_unescape($match[4] != "" ? $match[3] : $match[4]),
"table" => idf_unescape($match[4] != "" ? $match[4] : $match[3]),
"source" => array_map('Adminer\idf_unescape', $source[0]),
"target" => array_map('Adminer\idf_unescape', $target[0]),
"on_delete" => ($match[6] ?: "RESTRICT"),
"on_update" => ($match[7] ?: "RESTRICT"),
);
}
}
return $return;
}
/** Get view SELECT
* @return array{select:string}
*/
function view(string $name): array {
return array("select" => preg_replace('~^(?:[^`]|`[^`]*`)*\s+AS\s+~isU', '', get_val("SHOW CREATE VIEW " . table($name), 1)));
}
/** Get sorted grouped list of collations
* @return string[][]
*/
function collations(): array {
$return = array();
foreach (get_rows("SHOW COLLATION") as $row) {
if ($row["Default"]) {
$return[$row["Charset"]][-1] = $row["Collation"];
} else {
$return[$row["Charset"]][] = $row["Collation"];
}
}
ksort($return);
foreach ($return as $key => $val) {
sort($return[$key]);
}
return $return;
}
/** Find out if database is information_schema */
function information_schema(?string $db): bool {
return ($db == "information_schema")
|| (min_version(5.5) && $db == "performance_schema");
}
/** Get escaped error message */
function error(): string {
global $connection;
return h(preg_replace('~^You have an error.*syntax to use~U', "Syntax error", $connection->error));
}
/** Create database
* @return Result
*/
function create_database(string $db, string $collation) {
return queries("CREATE DATABASE " . idf_escape($db) . ($collation ? " COLLATE " . q($collation) : ""));
}
/** Drop databases
* @param list<string> $databases
*/
function drop_databases(array $databases): bool {
$return = apply_queries("DROP DATABASE", $databases, 'Adminer\idf_escape');
restart_session();
set_session("dbs", null);
return $return;
}
/** Rename database from DB
* @param string $name new name
*/
function rename_database(string $name, string $collation): bool {
$return = false;
if (create_database($name, $collation)) {
$tables = array();
$views = array();
foreach (tables_list() as $table => $type) {
if ($type == 'VIEW') {
$views[] = $table;
} else {
$tables[] = $table;
}
}
$return = (!$tables && !$views) || move_tables($tables, $views, $name);
drop_databases($return ? array(DB) : array());
}
return $return;
}
/** Generate modifier for auto increment column */
function auto_increment(): string {
$auto_increment_index = " PRIMARY KEY";
// don't overwrite primary key by auto_increment
if ($_GET["create"] != "" && $_POST["auto_increment_col"]) {
foreach (indexes($_GET["create"]) as $index) {
if (in_array($_POST["fields"][$_POST["auto_increment_col"]]["orig"], $index["columns"], true)) {
$auto_increment_index = "";
break;
}
if ($index["type"] == "PRIMARY") {
$auto_increment_index = " UNIQUE";
}
}
}
return " AUTO_INCREMENT$auto_increment_index";
}
/** Run commands to create or alter table
* @param string $table "" to create
* @param string $name new name
* @param list<array{string, list<string>, string}> $fields of [$orig, $process_field, $after]
* @param string[] $foreign
* @param string $auto_increment number
* @return Result|bool
*/
function alter_table(string $table, string $name, array $fields, array $foreign, ?string $comment, string $engine, string $collation, string $auto_increment, string $partitioning) {
global $connection;
$alter = array();
foreach ($fields as $field) {
if ($field[1]) {
$default = $field[1][3];
if (preg_match('~ GENERATED~', $default)) {
// swap default and null
$field[1][3] = ($connection->flavor == 'maria' ? "" : $field[1][2]); // MariaDB doesn't support NULL on virtual columns
$field[1][2] = $default;
}
$alter[] = ($table != "" ? ($field[0] != "" ? "CHANGE " . idf_escape($field[0]) : "ADD") : " ") . " " . implode($field[1]) . ($table != "" ? $field[2] : "");
} else {
$alter[] = "DROP " . idf_escape($field[0]);
}
}
$alter = array_merge($alter, $foreign);
$status = ($comment !== null ? " COMMENT=" . q($comment) : "")
. ($engine ? " ENGINE=" . q($engine) : "")
. ($collation ? " COLLATE " . q($collation) : "")
. ($auto_increment != "" ? " AUTO_INCREMENT=$auto_increment" : "")
;
if ($table == "") {
return queries("CREATE TABLE " . table($name) . " (\n" . implode(",\n", $alter) . "\n)$status$partitioning");
}
if ($table != $name) {
$alter[] = "RENAME TO " . table($name);
}
if ($status) {
$alter[] = ltrim($status);
}
return ($alter || $partitioning ? queries("ALTER TABLE " . table($table) . "\n" . implode(",\n", $alter) . $partitioning) : true);
}
/** Run commands to alter indexes
* @param string $table escaped table name
* @param list<array{string, string, 'DROP'|list<string>}> $alter of ["index type", "name", ["column definition", ...]] or ["index type", "name", "DROP"]
* @return Result|bool
*/
function alter_indexes(string $table, $alter) {
$changes = array();
foreach ($alter as $val) {
$changes[] = ($val[2] == "DROP"
? "\nDROP INDEX " . idf_escape($val[1])
: "\nADD $val[0] " . ($val[0] == "PRIMARY" ? "KEY " : "") . ($val[1] != "" ? idf_escape($val[1]) . " " : "") . "(" . implode(", ", $val[2]) . ")"
);
}
return queries("ALTER TABLE " . table($table) . implode(",", $changes));
}
/** Run commands to truncate tables
* @param list<string> $tables
*/
function truncate_tables(array $tables): bool {
return apply_queries("TRUNCATE TABLE", $tables);
}
/** Drop views
* @param list<string> $views
* @return Result|bool
*/
function drop_views(array $views) {
return queries("DROP VIEW " . implode(", ", array_map('Adminer\table', $views)));
}
/** Drop tables
* @param list<string> $tables
* @return Result|bool
*/
function drop_tables(array $tables) {
return queries("DROP TABLE " . implode(", ", array_map('Adminer\table', $tables)));
}
/** Move tables to other schema
* @param list<string> $tables
* @param list<string> $views
*/
function move_tables(array $tables, array $views, string $target): bool {
global $connection;
$rename = array();
foreach ($tables as $table) {
$rename[] = table($table) . " TO " . idf_escape($target) . "." . table($table);
}
if (!$rename || queries("RENAME TABLE " . implode(", ", $rename))) {
$definitions = array();
foreach ($views as $table) {
$definitions[table($table)] = view($table);
}
$connection->select_db($target);
$db = idf_escape(DB);
foreach ($definitions as $name => $view) {
if (!queries("CREATE VIEW $name AS " . str_replace(" $db.", " ", $view["select"])) || !queries("DROP VIEW $db.$name")) {
return false;
}
}
return true;
}
//! move triggers
return false;
}
/** Copy tables to other schema
* @param list<string> $tables
* @param list<string> $views
*/
function copy_tables(array $tables, array $views, string $target): bool {
queries("SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'");
foreach ($tables as $table) {
$name = ($target == DB ? table("copy_$table") : idf_escape($target) . "." . table($table));
if (
($_POST["overwrite"] && !queries("\nDROP TABLE IF EXISTS $name"))
|| !queries("CREATE TABLE $name LIKE " . table($table))
|| !queries("INSERT INTO $name SELECT * FROM " . table($table))
) {
return false;
}
foreach (get_rows("SHOW TRIGGERS LIKE " . q(addcslashes($table, "%_\\"))) as $row) {
$trigger = $row["Trigger"];
if (!queries("CREATE TRIGGER " . ($target == DB ? idf_escape("copy_$trigger") : idf_escape($target) . "." . idf_escape($trigger)) . " $row[Timing] $row[Event] ON $name FOR EACH ROW\n$row[Statement];")) {
return false;
}
}
}
foreach ($views as $table) {
$name = ($target == DB ? table("copy_$table") : idf_escape($target) . "." . table($table));
$view = view($table);
if (
($_POST["overwrite"] && !queries("DROP VIEW IF EXISTS $name"))
|| !queries("CREATE VIEW $name AS $view[select]") //! USE to avoid db.table
) {
return false;
}
}
return true;
}
/** Get information about trigger
* @param string $name trigger name
* @return Trigger
*/
function trigger(string $name, string $table): array {
if ($name == "") {
return array();
}
$rows = get_rows("SHOW TRIGGERS WHERE `Trigger` = " . q($name));
return reset($rows);
}
/** Get defined triggers
* @return array{string, string}[]
*/
function triggers(string $table): array {
$return = array();
foreach (get_rows("SHOW TRIGGERS LIKE " . q(addcslashes($table, "%_\\"))) as $row) {
$return[$row["Trigger"]] = array($row["Timing"], $row["Event"]);
}
return $return;
}
/** Get trigger options
* @return array{Timing: list<string>, Event: list<string>, Type: list<string>}
*/
function trigger_options(): array {
return array(
"Timing" => array("BEFORE", "AFTER"),
"Event" => array("INSERT", "UPDATE", "DELETE"),
"Type" => array("FOR EACH ROW"),
);
}
/** Get information about stored routine
* @param 'FUNCTION'|'PROCEDURE' $type
* @return Routine
*/
function routine(string $name, string $type): array {
global $driver;
$aliases = array("bool", "boolean", "integer", "double precision", "real", "dec", "numeric", "fixed", "national char", "national varchar");
$space = "(?:\\s|/\\*[\s\S]*?\\*/|(?:#|-- )[^\n]*\n?|--\r?\n)";
$enum = $driver->enumLength;
$type_pattern = "((" . implode("|", array_merge(array_keys($driver->types()), $aliases)) . ")\\b(?:\\s*\\(((?:[^'\")]|$enum)++)\\))?"
. "\\s*(zerofill\\s*)?(unsigned(?:\\s+zerofill)?)?)(?:\\s*(?:CHARSET|CHARACTER\\s+SET)\\s*['\"]?([^'\"\\s,]+)['\"]?)?";
$pattern = "$space*(" . ($type == "FUNCTION" ? "" : $driver->inout) . ")?\\s*(?:`((?:[^`]|``)*)`\\s*|\\b(\\S+)\\s+)$type_pattern";
$create = get_val("SHOW CREATE $type " . idf_escape($name), 2);
preg_match("~\\(((?:$pattern\\s*,?)*)\\)\\s*" . ($type == "FUNCTION" ? "RETURNS\\s+$type_pattern\\s+" : "") . "(.*)~is", $create, $match);
$fields = array();
preg_match_all("~$pattern\\s*,?~is", $match[1], $matches, PREG_SET_ORDER);
foreach ($matches as $param) {
$fields[] = array(
"field" => str_replace("``", "`", $param[2]) . $param[3],
"type" => strtolower($param[5]),
"length" => preg_replace_callback("~$enum~s", 'Adminer\normalize_enum', $param[6]),
"unsigned" => strtolower(preg_replace('~\s+~', ' ', trim("$param[8] $param[7]"))),
"null" => true,
"full_type" => $param[4],
"inout" => strtoupper($param[1]),
"collation" => strtolower($param[9]),
);
}
return array(
"fields" => $fields,
"comment" => get_val("SELECT ROUTINE_COMMENT FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = DATABASE() AND ROUTINE_NAME = " . q($name)),
) + ($type != "FUNCTION" ? array("definition" => $match[11]) : array(
"returns" => array("type" => $match[12], "length" => $match[13], "unsigned" => $match[15], "collation" => $match[16]),
"definition" => $match[17],
"language" => "SQL", // available in information_schema.ROUTINES.BODY_STYLE
));
}
/** Get list of routines
* @return list<string[]> ["SPECIFIC_NAME" => , "ROUTINE_NAME" => , "ROUTINE_TYPE" => , "DTD_IDENTIFIER" => ]
*/
function routines(): array {
return get_rows("SELECT ROUTINE_NAME AS SPECIFIC_NAME, ROUTINE_NAME, ROUTINE_TYPE, DTD_IDENTIFIER FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = DATABASE()");
}
/** Get list of available routine languages
* @return list<string>
*/
function routine_languages(): array {
return array(); // "SQL" not required
}
/** Get routine signature
* @param Routine $row
*/
function routine_id(string $name, array $row): string {
return idf_escape($name);
}
/** Get last auto increment ID
* @param Result|bool $result
*/
function last_id($result): string {
return get_val("SELECT LAST_INSERT_ID()"); // mysql_insert_id() truncates bigint
}
/** Explain select
* @return Result
*/
function explain(Db $connection, string $query) {
return $connection->query("EXPLAIN " . (min_version(5.1) && !min_version(5.7) ? "PARTITIONS " : "") . $query);
}
/** Get approximate number of rows
* @param TableStatus $table_status
* @param list<string> $where
* @return numeric-string|null null if approximate number can't be retrieved
*/
function found_rows(array $table_status, array $where) {
return ($where || $table_status["Engine"] != "InnoDB" ? null : $table_status["Rows"]);
}
/** Get SQL command to create table */
function create_sql(string $table, ?bool $auto_increment, string $style): string {
$return = get_val("SHOW CREATE TABLE " . table($table), 1);
if (!$auto_increment) {
$return = preg_replace('~ AUTO_INCREMENT=\d+~', '', $return); //! skip comments
}
return $return;
}
/** Get SQL command to truncate table */
function truncate_sql(string $table): string {
return "TRUNCATE " . table($table);
}
/** Get SQL command to change database */
function use_sql(string $database): string {
return "USE " . idf_escape($database);
}
/** Get SQL commands to create triggers */
function trigger_sql(string $table): string {
$return = "";
foreach (get_rows("SHOW TRIGGERS LIKE " . q(addcslashes($table, "%_\\")), null, "-- ") as $row) {
$return .= "\nCREATE TRIGGER " . idf_escape($row["Trigger"]) . " $row[Timing] $row[Event] ON " . table($row["Table"]) . " FOR EACH ROW\n$row[Statement];;\n";
}
return $return;
}
/** Get server variables
* @return list<string[]> [[$name, $value]]
*/
function show_variables(): array {
return get_rows("SHOW VARIABLES");
}
/** Get status variables
* @return list<string[]> [[$name, $value]]
*/
function show_status(): array {
return get_rows("SHOW STATUS");
}
/** Get process list
* @return list<string[]> [$row]
*/
function process_list(): array {
return get_rows("SHOW FULL PROCESSLIST");
}
/** Convert field in select and edit
* @param Field $field
* @return string|void
*/
function convert_field(array $field) {
if (preg_match("~binary~", $field["type"])) {
return "HEX(" . idf_escape($field["field"]) . ")";
}
if ($field["type"] == "bit") {
return "BIN(" . idf_escape($field["field"]) . " + 0)"; // + 0 is required outside MySQLnd
}
if (preg_match("~geometry|point|linestring|polygon~", $field["type"])) {
return (min_version(8) ? "ST_" : "") . "AsWKT(" . idf_escape($field["field"]) . ")";
}
}
/** Convert value in edit after applying functions back
* @param Field $field
* @param string $return SQL expression
*/
function unconvert_field(array $field, string $return): string {
if (preg_match("~binary~", $field["type"])) {
$return = "UNHEX($return)";
}
if ($field["type"] == "bit") {
$return = "CONVERT(b$return, UNSIGNED)";
}
if (preg_match("~geometry|point|linestring|polygon~", $field["type"])) {
$prefix = (min_version(8) ? "ST_" : "");
$return = $prefix . "GeomFromText($return, $prefix" . "SRID($field[field]))";
}
return $return;
}
/** Check whether a feature is supported
* @param literal-string $feature "check|comment|copy|database|descidx|drop_col|dump|event|indexes|kill|materializedview|partitioning|privileges|procedure|processlist|routine|scheme|sequence|status|table|trigger|type|variables|view|view_trigger"
*/
function support(string $feature): bool {
return !preg_match("~scheme|sequence|type|view_trigger|materializedview" . (min_version(8) ? "" : "|descidx" . (min_version(5.1) ? "" : "|event|partitioning")) . (min_version('8.0.16', '10.2.1') ? "" : "|check") . "~", $feature);
}
/** Kill a process
* @param numeric-string $val
* @return Result|bool
*/
function kill_process(string $val) {
return queries("KILL " . number($val));
}
/** Return query to get connection ID */
function connection_id(): string {
return "SELECT CONNECTION_ID()";
}
/** Get maximum number of connections
* @return numeric-string
*/
function max_connections(): string {
return get_val("SELECT @@max_connections");
}
// Not used is MySQL but checked in compile.php:
/** Get user defined types
* @return string[] [$id => $name]
*/
function types(): array {
return array();
}
/** Get values of user defined type */
function type_values(int $id): string {
return "";
}
/** Get existing schemas
* @return list<string>
*/
function schemas(): array {
return array();
}
/** Get current schema */
function get_schema(): string {
return "";
}
/** Set current schema */
function set_schema(string $schema, Db $connection2 = null): bool {
return true;
}
}