if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed');
class UpdraftPlus_Database_Utility {
private $table_prefix_raw;
* @param String $whichdb - which database is being backed up
* @param String $table_prefix_raw - the base table prefix
* @param Object $dbhandle - WPDB object
public function __construct($whichdb, $table_prefix_raw, $dbhandle) {
$this->whichdb = $whichdb;
$this->table_prefix_raw = $table_prefix_raw;
$this->dbhandle = $dbhandle;
* The purpose of this function is to make sure that the options table is put in the database first, then the users table, then the site + blogs tables (if present - multisite), then the usermeta table; and after that the core WP tables - so that when restoring we restore the core tables first
* @param Array $a_arr the first array
* @param Array $b_arr the second array
* @return Integer - the sort result, according to the rules of PHP custom sorting functions
public function backup_db_sorttables($a_arr, $b_arr) {
$a_table_type = $a_arr['type'];
$b_table_type = $b_arr['type'];
// Views must always go after tables (since they can depend upon them)
if ('VIEW' == $a_table_type && 'VIEW' != $b_table_type) return 1;
if ('VIEW' == $b_table_type && 'VIEW' != $a_table_type) return -1;
if ('wp' != $this->whichdb) return strcmp($a, $b);
$our_table_prefix = $this->table_prefix_raw;
if ($a == $our_table_prefix.'options') return -1;
if ($b == $our_table_prefix.'options') return 1;
if ($a == $our_table_prefix.'site') return -1;
if ($b == $our_table_prefix.'site') return 1;
if ($a == $our_table_prefix.'blogs') return -1;
if ($b == $our_table_prefix.'blogs') return 1;
if ($a == $our_table_prefix.'users') return -1;
if ($b == $our_table_prefix.'users') return 1;
if ($a == $our_table_prefix.'usermeta') return -1;
if ($b == $our_table_prefix.'usermeta') return 1;
if (empty($our_table_prefix)) return strcmp($a, $b);
$core_tables = array_merge($this->dbhandle->tables, $this->dbhandle->global_tables, $this->dbhandle->ms_global_tables);
$updraftplus->log($e->getMessage());
if (empty($core_tables)) $core_tables = array('terms', 'term_taxonomy', 'termmeta', 'term_relationships', 'commentmeta', 'comments', 'links', 'postmeta', 'posts', 'site', 'sitemeta', 'blogs', 'blogversions', 'blogmeta');
$na = UpdraftPlus_Manipulation_Functions::str_replace_once($our_table_prefix, '', $a);
$nb = UpdraftPlus_Manipulation_Functions::str_replace_once($our_table_prefix, '', $b);
if (in_array($na, $core_tables) && !in_array($nb, $core_tables)) return -1;
if (!in_array($na, $core_tables) && in_array($nb, $core_tables)) return 1;
* Detect if the table has a composite primary key (composed from multiple columns)
* @param String $table - table to examine
* @param Object|Null $wpdb_obj - WPDB-like object (requires the get_results() method), or null to use the global default
public static function table_has_composite_private_key($table, $wpdb_obj = null) {
$wpdb = (null === $wpdb_obj) ? $GLOBALS['wpdb'] : $wpdb_obj;
$table_structure = $wpdb->get_results("DESCRIBE ".UpdraftPlus_Manipulation_Functions::backquote($table));
if (!$table_structure) return false;
$primary_key_columns_found = 0;
foreach ($table_structure as $struct) {
if (isset($struct->Key) && 'PRI' == $struct->Key) {
$primary_key_columns_found++;
if ($primary_key_columns_found > 1) return true;
* Set MySQL server system variable
* @param String $variable The name of the system variable
* @param String $value The variable value
* @param Resource|Object $db_handle The database link identifier(resource) given by mysqli_init or mysql_connect
* @return Boolean Returns true on success, false otherwise
public static function set_system_variable($variable, $value, $db_handle) {
$is_mysqli = is_a($db_handle, 'mysqli');
if (!is_resource($db_handle) && !$is_mysqli) return false;
$sql = "SET SESSION %s='%s'";
// @codingStandardsIgnoreLine
$res = @mysqli_query($db_handle, sprintf($sql, mysqli_real_escape_string($db_handle, $variable), mysqli_real_escape_string($db_handle, $value)));
// @codingStandardsIgnoreLine
$res = @mysql_query(sprintf($sql, mysql_real_escape_string($variable, $db_handle), mysql_real_escape_string($value, $db_handle)), $db_handle);
* Get MySQL server system variable.
* @param String $variable The name of the system variable
* @param Resource|Object $db_handle The database link identifier(resource) given by mysqli_init or mysql_connect
* @return String|Boolean|Null Returns value of the system variable, false on query failure or null if there is no result for the corresponding variable
public static function get_system_variable($variable, $db_handle) {
$is_mysqli = is_a($db_handle, 'mysqli');
if (!is_resource($db_handle) && !$is_mysqli) return false;
$sql = 'SELECT @@SESSION.%s';
// @codingStandardsIgnoreLine
$res = @mysqli_query($db_handle, sprintf($sql, mysqli_real_escape_string($db_handle, $variable)));
// @codingStandardsIgnoreLine
$res = @mysql_query(sprintf($sql, mysql_real_escape_string($variable, $db_handle)), $db_handle);
// @codingStandardsIgnoreLine
$res = mysqli_fetch_array($res);
return isset($res[0]) ? $res[0] : null;
// @codingStandardsIgnoreLine
$res = mysql_result($res, 0);
return false === $res ? null : $res;
* This function is adapted from the set_sql_mode() method in WordPress wpdb class but with few modifications applied, this can be used to switch between different sets of SQL modes.
* @see https://developer.wordpress.org/reference/classes/wpdb/set_sql_mode/
* @see https://dev.mysql.com/doc/refman/5.6/en/sql-mode.html
* @see https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html
* @see https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html
* @see https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_sql_mode
* @see https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_sql_mode
* @see https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_sql_mode
* @see https://mariadb.com/kb/en/library/sql-mode/#strict-mode
* @see https://mariadb.com/kb/en/library/sql-mode/#setting-sql_mode
* @param Array $modes - Optional. A list of SQL modes to set.
* @param Array $remove_modes - modes to remove if they are currently active
* @param Resource|Object|NULL $db_handle - Optional. If specified, it should either the valid database link identifier(resource) given by mysql(i) or null to instead use the global WPDB object, or a WPDB-compatible object.
public static function set_sql_mode($modes = array(), $remove_modes = array(), $db_handle = null) {
global $updraftplus, $wpdb;
$wpdb_handle_if_used = (null !== $db_handle && is_a($db_handle, 'WPDB')) ? $db_handle : $wpdb;
// If any of these are set, they will be unset
// according to mariadb and mysql docs, strict mode can be one of these or both
$incompatible_modes = array_unique(array_merge(array(
if (is_null($db_handle) || is_a($db_handle, 'WPDB')) {
$initial_modes_str = $wpdb_handle_if_used->get_var('SELECT @@SESSION.sql_mode');
$initial_modes_str = call_user_func_array(array($class, 'get_system_variable'), array('sql_mode', $db_handle));
if (is_scalar($initial_modes_str) && !is_bool($initial_modes_str)) {
$modes = array_unique(array_merge($modes, array_change_key_case(explode(',', $initial_modes_str), CASE_UPPER)));
$updraftplus->log("Couldn't get the sql_mode value (".serialize($initial_modes_str)."); will not attempt any adjustment");
$modes = array_change_key_case($modes, CASE_UPPER);
$unwanted_modes = array_merge($incompatible_modes, $remove_modes);
foreach ($modes as $i => $mode) {
if (in_array($mode, $unwanted_modes)) {
$modes_str = implode(',', $modes);
if (is_null($db_handle) || is_a($db_handle, 'WPDB')) {
$res = $wpdb_handle_if_used->query($wpdb_handle_if_used->prepare("SET SESSION sql_mode = %s", $modes_str));
$res = call_user_func_array(array($class, 'set_system_variable'), array('sql_mode', $modes_str, $db_handle));
if (isset($initial_modes_str) && false == array_diff(explode(',', $initial_modes_str), $modes)) {
$updraftplus->log("SQL compatibility mode is: $modes_str");
$updraftplus->log("SQL compatibility mode".((false === $res) ? " not" : "")." successfully changed".(isset($initial_modes_str) ? " from $initial_modes_str" : "")." to $modes_str");
* Parse the SQL "create table" column definition (non validating) and check whether it's a generated column and retrieve its column options
* @see https://dev.mysql.com/doc/refman/8.0/en/create-table.html
* @see https://mariadb.com/kb/en/create-table/
* @param String $table_column_definition the column definition statement in which the generated column needs to be identified
* @param Integer $starting_offset the string position of the column definition in a "create table" statement
* @return Array|False an array of generated column fragment (column definition, column name, generated column type, etc); false otherwise
* $column_definition = "fullname varchar(101) GENERATED ALWAYS AS (CONCAT(first_name,' ',last_name)) VIRTUAL NOT NULL COMMENT 'this is the comment',"
* "column_definition" => "fullname varchar(101) GENERATED ALWAYS AS (CONCAT(first_name,' ',last_name)) VIRTUAL NOT NULL COMMENT 'this is the comment',",
* "column_name" => "fullname",
* "column_data_type_definition" => [
* "GENERATED ALWAYS AS (CONCAT(first_name,' ',last_name))",
* "COMMENT 'this is the comment'",
public static function get_generated_column_info($table_column_definition, $starting_offset) {
// check whether or not the column definition ($table_column_definition) is a generated column, if so then get all the column definitions
// https://regex101.com/r/Fy2Bkd/12
if (preg_match_all('/^\s*\`((?:[^`]|``)+)\`([^,\'"]+?)(?:((?:GENERATED\s*ALWAYS\s*)?AS\s*\(.+\))([\w\s]*)(COMMENT\s*(?:\'(?:[^\']|\'\')*\'|\"(?:[^"]|"")*\"))([\w\s]*)|((?:GENERATED\s*ALWAYS\s*)?AS\s*\(.+\)([\w\s]*)))/i', $table_column_definition, $column_definitions, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
if (empty($column_definitions)) return false;
* If the above preg_match_all function succeed, it returns an array with the following format:
* [0]=> // 1st set of the matched/captured string
* [0]=> // 1st index represents a full column definition
* [0]=> string(131) ", `full_name` char(41) GENERATED ALWAYS AS (concat(`firstname`,'()`)(()',`lastname`)) VIRTUAL NOT NULL COMMENT 'fu(ll)'_name'' COLUMN_FORMAT DEFAULT"
* [1]=> // 2nd index represents a column name
* [0]=> string(9) "full_name"
* [2]=> // 3rd index represents data type option that is captured before "generated always as"
* [0]=> string(18) " char(41) "
* [3]=> // 4rd index represents data type option which is specific for "generated always as"
* [0]=> string(18) "GENERATED ALWAYS AS (concat(`firstname`,'()`)(()',`lastname`))"
* [1]=> int(629) // this is the position or starting offset of the captured data type's option, this can later be used to help with the unsupported keyword replacement stuff among db server
* [4]=> // 5th index represents data type option that is captured before COMMENT keyword and after "generated alwasy as"
* [0]=> string(13) " VIRTUAL NOT NULL " // this is the comment string that could be filled with any word even the reserved keyword (e.g. not null, virtual, stored, etc..)
* [1]=> int(656) // this is the position or starting offset of the captured data type's option, this can later be used to help with the unsupported keyword replacement stuff among db server
* [5]=> // 6th index represents the comment
* [0]=> string(2) "COMMENT 'fu(ll)'_name''"
* [1]=> int(670) // this is the position or starting offset of the captured comment's string
* [6]=> // 7th index represents data type option that is captured after the COMMENT keyword
* [0]=> string(2) "COLUMN_FORMAT DEFAULT"
* [0]=> string(95) ", `full_name6` char(41) GENERATED ALWAYS AS (concat(`firstname`,' ',`lastname2`))STORED NULL"
* [0]=> string(10) "full_name6"
* [0]=> string(0) " char(41) "
* [0]=> string(0) "" // an empty string of this captured token indicates that the column definition doesn't have COMMENT keyword
* [7]=> // 8th index will appear if there's no COMMENT keyword found in the column definition and it represents data type option that is specific for "generated always as"
* [0]=> string(11) "GENERATED ALWAYS AS (concat(`firstname`,' ',`lastname2`))"
* [8]=> // 9th index will appear if there's no COMMENT keyword found in the column definition and it represents the captured data type options
* [0]=> string(11) "STORED NULL"
foreach ($column_definitions as $column_definition) {
$data_type_definition = (!empty($column_definition[4][0]) ? $column_definition[4][0] : '').(!empty($column_definition[6][0]) ? $column_definition[6][0] : '').(!empty($column_definition[8][0]) ? $column_definition[8][0] : '');
// if no virtual, stored or persistent option is specified then it's virtual by default. It's not possible having two generated columns type in the column definition e.g fullname varchar(101) GENERATED ALWAYS AS (CONCAT(first_name,' ',last_name)) VIRTUAL STORED NOT NULL COMMENT 'comment text', both MySQL and MariaDB will produces an error
$is_virtual = preg_match('/\bvirtual\b/i', $data_type_definition) || (!preg_match('/\bstored\b/i', $data_type_definition) && !preg_match('/\bpersistent\b/i', $data_type_definition));
// full syntax of the column definition
"column_definition" => $column_definition[0][0],
// the extracted column name
"column_name" => $column_definition[1][0],
'column_data_type_definition' => array(),
"is_virtual" => $is_virtual,
if (!empty($column_definition[2])) {
$fragment['column_data_type_definition']['DATA_TYPE_TOKEN'] = $column_definition[2];
$fragment['column_data_type_definition']['DATA_TYPE_TOKEN'][1] = (int) $starting_offset + (int) $fragment['column_data_type_definition']['DATA_TYPE_TOKEN'][1];
if (!empty($column_definition[3])) {
$fragment['column_data_type_definition']['GENERATED_ALWAYS_TOKEN'] = $column_definition[3];
if (empty($fragment['column_data_type_definition'][1]) && !empty($column_definition[7][0])) $fragment['column_data_type_definition']['GENERATED_ALWAYS_TOKEN'] = $column_definition[7];
$fragment['column_data_type_definition']['GENERATED_ALWAYS_TOKEN'][1] = (int) $starting_offset + (int) $fragment['column_data_type_definition']['GENERATED_ALWAYS_TOKEN'][1];
if (!empty($column_definition[4])) {
$fragment['column_data_type_definition'][2] = $column_definition[4];
$fragment['column_data_type_definition'][2][1] = (int) $starting_offset + (int) $fragment['column_data_type_definition'][2][1];
if (!empty($column_definition[5])) {
$fragment['column_data_type_definition']['COMMENT_TOKEN'] = $column_definition[5];
$fragment['column_data_type_definition']['COMMENT_TOKEN'][1] = (int) $starting_offset + (int) $fragment['column_data_type_definition']['COMMENT_TOKEN'][1];
if (!empty($column_definition[6])) {
$fragment['column_data_type_definition'][4] = $column_definition[6];
$fragment['column_data_type_definition'][4][1] = (int) $starting_offset + (int) $fragment['column_data_type_definition'][4][1];
if (!empty($column_definition[8])) {
$fragment['column_data_type_definition'][5] = $column_definition[8];
$fragment['column_data_type_definition'][5][1] = (int) $starting_offset + (int) $fragment['column_data_type_definition'][5][1];
return isset($fragment) ? $fragment : false;
* Retrieve information concerning whether the currently running database server supports generated columns (VIRTUAL, STORED, PERSISTENT)
* @param String $engine Optional. If specified, it should either a well-known database engine like InnoDB, MyISAM, etc or an empty string to instead use database default storage engine; e.g. 'MyISAM'
* @return Array|Boolean an array of supported generated column syntax options (whether or not persistent type, not null, virtual index are supported) or false if generated column isn't supported
* The return value is structured thus:
* // InnoDB supports PERSISTENT generated columns type, whereas MyISAM does not
* "is_persistent_supported" => false,
* // InnoDB supports NOT NULL constraint, whereas MyISAM does not
* "is_not_null_supported" => true,
* // if it's on MariaDB, you can use insert ignore statement to prevent generated columns errors but not on MySQL
* "can_insert_ignore_to_generated_column" => true,
* // No matter what the database engine you use, MySQL doesn't yet support indexing on generated columns
* "is_virtual_index_supported" => false
public static function is_generated_column_supported($engine = '') {
global $table_prefix, $wpdb;
$random_table_name = $table_prefix.'updraft_tmp_'.rand(0, 9999999).md5(microtime(true));
$drop_statement = "DROP TABLE IF EXISTS `$random_table_name`;";
// both mysql and mariadb support generated column, virtual is the default type and the other option type is called stored, mariadb has an alias for stored type which is called persistent, whereas mysql doesn't have such thing.
// MySQL supports NULL and NOT NULL constraints. On the other hand, MariaDB doesn't support it.
"CREATE TABLE `$random_table_name` (`virtual_column` varchar(17) GENERATED ALWAYS AS ('virtual_column') VIRTUAL COMMENT 'virtual_column')".(!empty($engine) ? " ENGINE=$engine" : "").";",
"ALTER TABLE `$random_table_name` ADD `persistent_column` VARCHAR(17) AS ('persistent_column') PERSISTENT COMMENT 'generated_column';",
"ALTER TABLE `$random_table_name` ADD `virtual_column_not_null` VARCHAR(17) AS ('virtual_column_not_null') VIRTUAL NOT NULL COMMENT 'virtual_column_not_null';",
// check if we can get through this: Error Code: 3105. The value specified for generated column 'generated_column' in table 'wp_generated_column_test' is not allowed.
// DEFAULT is the only allowed value for virtual and stored type (i.e INSERT IGNORE INTO `wp_generated_column_test` (`virtual_column`) VALUES(DEFAULT)), other than that will produce an error, luckily insert ignore works fine on MariaDB but not on MySQL
"INSERT IGNORE INTO `$random_table_name` (`virtual_column`) VALUES('virtual_column');",
// MySQL does not support the create option 'Index on virtual generated column' on MyISAM storage engine
"CREATE INDEX `idx_wp_udp_generated_column_test_generated_column` ON `$random_table_name` (virtual_column) COMMENT 'virtual_column' ALGORITHM DEFAULT LOCK DEFAULT;",
$old_val = $wpdb->suppress_errors();
$wpdb->query($drop_statement);
$is_generated_column_supported = $wpdb->query($sql[0]);
if ($is_generated_column_supported) {
$is_generated_column_supported = array(
'is_persistent_supported' => $wpdb->query($sql[1]),
'is_not_null_supported' => $wpdb->query($sql[2]),
'can_insert_ignore_to_generated_column' => (bool) $wpdb->query($sql[3]),
'is_virtual_index_supported' => $wpdb->query($sql[4])
$is_generated_column_supported = false;
$wpdb->query($drop_statement);
$wpdb->suppress_errors($old_val);
return $is_generated_column_supported;
* Parse the "insert into" statement, capture the column names (if any) and check whether one of the captured columns matches the given list of the "$generated_columns"
* @see https://regex101.com/r/JZiJqH/2
* @param String $insert_statement the insert statement in which the generated columns will be checked
* @param Array $generated_columns the list of the available "generated columns"
* @return Boolean|Null True if "generated columns" exist in the "insert into" statement, false otherwise, null on empty or unmatched insert statement
public static function generated_columns_exist_in_the_statement($insert_statement, $generated_columns) {
if (preg_match('/\s*insert.+?into(?:\s*`(?:[^`]|`)+?`|[^\(]+)(?:\s*\((.+?)\))?\s*values.+/i', $insert_statement, $matches)) {
* the reqex above will search for matches of either the insert statement gives data based on the specified column names (i.e INSERT INTO `table_name`(`col1`,'col2`,`virtual_column`,`stored_column`,`col5`) values('1','2','3','4','5')) or not (i.e INSERT INTO `table_name` values('1',',2','3','4','5')), and if the above preg_match function succeed, it returns an array with the following format:
* [0]=> "INSERT INTO `table_name`(`col1`,'col2`,`virtual_column`,`stored_column`,`col5`) values('1','2','3','4','5')"
* [1]=> "`col1`,`col2`,`virtual_column`,`col4`,`stored_column`"