Index: src/wp-includes/wp-db-api.php
===================================================================
--- src/wp-includes/wp-db-api.php	(revision 0)
+++ src/wp-includes/wp-db-api.php	(working copy)
@@ -0,0 +1,428 @@
+<?php
+/**
+ * WordPress DB API Class
+ *
+ * @since X4.2
+ * @package WordPress
+ * @subpackage Database
+ */
+
+/**
+ * WordPress Database API
+ *
+ * This class is a collection of low level methods for accessing the database without taking WordPress into account.
+ * All code that requires a different approach according to the currently used API should be put here too.
+ *
+ * api_*    methods provide a  generic interface to API functions that        have corresponding functionality.
+ * db_*     methods provide a specific interface to API functions that do not have corresponding functionality.
+ * mysqli_* methods provide a doctored access to mysqli_* functions.
+ * mysql_*  methods provide a doctored access to mysql_*  functions.
+ *
+ * @since X4.2
+ * @package WordPress
+ * @subpackage Database
+ */
+class wpdb_API {
+
+	/**
+	 * API Name
+	 *
+	 * One of these two values:
+	 * - 'mysqli'
+	 * - 'mysql'
+	 *
+	 * @since X4.2
+	 * @var string
+	 */
+	protected $api = '';
+
+	/**
+	 * Fallback State
+	 *
+	 * One of these four values:
+	 *
+	 * - 'impossible' (a fallback is not an option)
+	 * - 'possible'   (can attempt a fallback)
+	 * - 'success'    (a fallback allowed to connect)
+	 * - 'failure'    (out of luck for connecting)
+	 *
+	 * @since X4.2
+	 * @var string
+	 */
+	protected $fallback;
+
+	/**
+	 * Database Username
+	 *
+	 * @since 2.9.0
+	 * @var string
+	 */
+	protected $dbuser;
+
+	/**
+	 * Database Password
+	 *
+	 * @since 3.1.0
+	 * @var string
+	 */
+	protected $dbpassword;
+
+	/**
+	 * Database Name
+	 *
+	 * @since 3.1.0
+	 * @var string
+	 */
+	protected $dbname;
+
+	/**
+	 * Database Host
+	 *
+	 * @since 3.1.0
+	 * @var string
+	 */
+	protected $dbhost;
+
+	/**
+	 * Database Handle
+	 *
+	 * @since 0.71
+	 * @var string
+	 */
+	protected $dbh = null;
+
+	/**
+	 * Store connection data.
+	 *
+	 * @param string $dbuser     MySQL database user
+	 * @param string $dbpassword MySQL database password
+	 * @param string $dbname     MySQL database name
+	 * @param string $dbhost     MySQL database host
+	 */
+	protected function __construct( $dbuser, $dbpassword, $dbname, $dbhost ) {
+		$this->dbuser     = $dbuser;
+		$this->dbpassword = $dbpassword;
+		$this->dbname     = $dbname;
+		$this->dbhost     = $dbhost;
+
+		$this->init_correspondence();
+		$this->init_api_to_use();
+	}
+
+	/**
+	 * Detect the API to use
+	 *
+	 * @since X4.2
+	 */
+	protected function init_api_to_use() {
+		$got_mysqli  = function_exists( 'mysqli_connect' );
+		$got_mysql   = function_exists( 'mysql_connect' );
+		$not_mysql   = defined( 'WP_USE_EXT_MYSQL' ) && ! WP_USE_EXT_MYSQL;
+		$PHP_5_5_min = version_compare( phpversion(), '5.5', '>=' );
+		$core_dev    = false !== strpos( $GLOBALS['wp_version'], '-' );
+
+		$this->api = $got_mysqli && ( $not_mysql || $PHP_5_5_min || ! $got_mysql || $core_dev )
+			? 'mysqli'
+			: 'mysql';
+
+		$this->fallback = ( $this->api == 'mysqli' ) && ! $not_mysql && $got_mysql
+			? 'possible'
+			: 'impossible';
+	}
+
+	/**
+	 * These mysql_* functions and mysqli_* counterparts have the same parameters.
+	 * The value has no meaning, we only use the key.
+	 *
+	 * @since X4.2
+	 * @var array
+	 */
+	private $same_arguments;
+
+	/**
+	 * These mysql_* functions have parameters in a certain order, mysqli_* counterparts have them in another order.
+	 * The value has no meaning, we only use the key.
+	 *
+	 * @since X4.2
+	 * @var array
+	 */
+	private $swapped_arguments;
+
+	/**
+	 * These mysql_* functions have some parameters, mysqli_* counterparts have some other.
+	 * The value is the number of same initial parameters.
+	 *
+	 * @since X4.2
+	 * @var array
+	 */
+	private $different_arguments;
+
+	/**
+	 * These mysql_* functions exist, mysqli_* counterparts do not.
+	 * The value has no meaning, we only use the key.
+	 *
+	 * @since X4.2
+	 * @var array
+	 */
+	private $defunct_functions;
+
+	/**
+	 * Categorize mysql_* and mysqli_* functions.
+	 *
+	 * @since X4.2
+	 */
+	protected function init_correspondence() {
+		// these arrays use keys for speed (isset VS in_array)
+
+		$this->same_arguments = array_flip( explode( ' ', 'affected_rows client_encoding close data_seek errno error'
+			. ' fetch_array fetch_assoc fetch_field fetch_lengths fetch_object fetch_row field_seek free_result'
+			. ' get_client_info get_host_info get_proto_info get_server_info info insert_id num_rows ping stat'
+			. ' thread_id' ) );
+			// mysql_fetch_field accepts one additional parameter, but mostly useless
+			// mysqli_get_client_info accepts one additional parameter
+
+		$this->swapped_arguments = array_flip( explode( ' ', 'query real_escape_string select_db set_charset' ) );
+			// mysqli_query accepts an additional parameter
+
+		// use db_connect instead
+		$this->different_arguments = array( 'connect' => 3 );
+			// mysql additionally accepts $new_link, $client_flags; mysqli additionally accepts $dbname, $port, $socket
+
+		// if needed, these functions should be implemented along the lines of what done for connect in db_connect
+		$this->defunct_functions = array_flip( explode( ' ', 'create_db db_name db_query drop_db escape_string'
+			. ' field_flags field_len field_name field_table field_type list_dbs list_fields list_processes list_tables'
+			. ' num_fields pconnect result tablename unbuffered_query' ) );
+			// mysql_escape_string highly discouraged, mysqli_escape_string alias of mysqli_real_escape_string
+			// mysql_result is a function, mysqli_result is a class
+	}
+
+	/**
+	 * Extract the term from $name.
+	 *
+	 * Example:
+	 *
+	 *    api_term('mysqli_close') == 'close';  // true
+	 *    api_term('mysql_close')  == 'close';  // true
+	 *    api_term('close')        == 'close';  // true
+	 *
+	 * @since X4.2
+	 * @param  string $name The name of an API function
+	 * @return string       The term contained in $name
+	 */
+	protected function api_term($name) {
+		$result = $name;
+		if (strpos($name, $this->api . '_') === 0) {
+			$result = substr($name, strlen($this->api . '_'));
+		}
+		return $result;
+	}
+
+	/**
+	 * Reorder arguments of mysqli_* functions when needed for calling mysql_* counterparts.
+	 *
+	 * @since X4.2
+	 * @param  string $name The mysqli_* function
+	 * @param  array  $args The arguments of the mysqli_* function
+	 * @return mixed        The arguments of the corresponding mysql_* function
+	 * @throws Exception
+	 */
+	protected function api_args($name, $args) {
+		// when using mysqli we can short-circuit right here
+		if ( 'mysqli' == $this->api ) {
+			return $args;
+		}
+
+		$term = $this->api_term( $name );
+
+		if ( isset( $this->same_arguments[$term] ) ) {
+			return $args;
+		}
+		if ( isset( $this->swapped_arguments[$term] ) ) {
+			array_splice( $args, 0, 2, array( $args[1], $args[0] ));
+			return $args;
+		}
+		if ( isset( $this->different_arguments[$term] ) ) {
+			if ( count($args) <= $this->different_arguments[$term] ) {
+				return $args;
+			}
+		}
+		if ( isset( $this->defunct_functions[$term] ) ) {
+			// we should never get here, except due to developer's misunderstanding
+			throw new Exception('Expected a mysqli compatible function call.');
+		}
+		// we should never get here, except due to developer's misunderstanding
+		throw new Exception('Expected a mysql function call.');
+	}
+
+	/**
+	 * Make an array with mysqli_* keys and either mysqli_* or mysql_* values, according to the current API.
+	 *
+	 * Use extract() to define API function variables with same name and arguments (in the same order).
+	 *
+	 * Example:
+	 *
+	 *    extract( $this->api_vars( 'close' ) );
+	 *    // or extract( $this->api_vars( 'mysqli_close' ) );
+	 *    // or extract( $this->api_vars( 'mysql_close' ) );
+	 *    $mysqli_close( $this->dbh );
+	 *
+	 * Prefer mysqli_* names for making it easier to find where a given API is called.
+	 *
+	 * @since X4.2
+	 * @var    array ... Names of API functions
+	 * @return array     Array of variable names to API names
+	 */
+	protected function api_vars() {
+		$names = func_get_args();
+		$result = array();
+		foreach ($names as $name) {
+			$term = $this->api_term($name);
+			$result["mysqli_$term"] = $this->api . '_' . $term;
+		}
+		return $result;
+	}
+
+	/**
+	 * Call the current API function with the given name and additional arguments.
+	 *
+	 * This method allows to call API functions with the same name and arguments (possibly in a different order).
+	 *
+	 * Example:
+	 *
+	 *    $this->api_call( 'close' );
+	 *    // or $this->api_call( 'mysqli_close' );
+	 *    // or $this->api_call( 'mysql_close' );
+	 *
+	 * Prefer mysqli_* names for making it easier to find where a given API is called.
+	 *
+	 * @since X4.2
+	 * @param  $name The name of the API function to call
+	 * @param  ...   All the arguments for calling the API function
+	 * @return mixed The value returned by the API call
+	 */
+	public function api_call($name) {
+		$term = $this->api_term($name);
+		$args = func_get_args();
+		array_shift($args); // discard $name
+		$args = $this->api_args($term, $args);
+		$name = $this->api . '_' . $term;
+		return call_user_func_array($name, $args);
+	}
+
+	/**
+	 * Connect to the database using mysql
+	 *
+	 * @since X4.2
+	 */
+	private function mysql_connect() {
+		/*
+		 * Deprecated in 3.9+ when using MySQLi. No equivalent
+		 * $new_link parameter exists for mysqli_* functions.
+		 */
+		$new_link = defined( 'MYSQL_NEW_LINK' ) ? MYSQL_NEW_LINK : true;
+		$client_flags = defined( 'MYSQL_CLIENT_FLAGS' ) ? MYSQL_CLIENT_FLAGS : 0;
+
+		if ( WP_DEBUG ) {
+			$this->dbh = mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags );
+		} else {
+			$this->dbh = @mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags );
+		}
+	}
+
+	/**
+	 * Connect to the database using mysqli
+	 *
+	 * @since X4.2
+	 */
+	private function mysqli_connect() {
+		$this->dbh = mysqli_init();
+
+		// mysqli_real_connect doesn't support the host param including a port or socket
+		// like mysql_connect does. This duplicates how mysql_connect detects a port and/or socket file.
+		$port = null;
+		$socket = null;
+		$host = $this->dbhost;
+		$port_or_socket = strstr( $host, ':' );
+		if ( ! empty( $port_or_socket ) ) {
+			$host = substr( $host, 0, strpos( $host, ':' ) );
+			$port_or_socket = substr( $port_or_socket, 1 );
+			if ( 0 !== strpos( $port_or_socket, '/' ) ) {
+				$port = intval( $port_or_socket );
+				$maybe_socket = strstr( $port_or_socket, ':' );
+				if ( ! empty( $maybe_socket ) ) {
+					$socket = substr( $maybe_socket, 1 );
+				}
+			} else {
+				$socket = $port_or_socket;
+			}
+		}
+
+		$client_flags = defined( 'MYSQL_CLIENT_FLAGS' ) ? MYSQL_CLIENT_FLAGS : 0;
+
+		if ( WP_DEBUG ) {
+			mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
+		} else {
+			@mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
+		}
+	}
+
+	/**
+	 * Connect to the database using the current API.
+	 * This method COULD change the current API from mysqli to mysql in case of a fallback.
+	 *
+	 * mysqli_connect works quite differently from mysql_connect: we need a specific method for each and a this one for
+	 * orchestrating. Notice that (public) wpdb::db_connect() overrides (protected) wpdb_API::db_connect().
+	 *
+	 * @since X4.2
+	 * @return boolean TRUE if connected
+	 */
+	protected function db_connect() {
+		if ( 'mysqli' == $this->api ) {
+			$this->mysqli_connect();
+
+			if ( $this->dbh->connect_errno ) {
+				$this->dbh = null;
+				if ( 'possible' == $this->fallback ) {
+					$this->api = 'mysql';
+					$this->mysql_connect();
+					$this->fallback = $this->dbh ? 'success' : 'failure';
+				}
+			}
+		} else {
+			$this->mysql_connect();
+		}
+
+		return (bool) $this->dbh;
+	}
+
+	/**
+	 * List SQL modes.
+	 *
+	 * @since X4.2
+	 * @return array The set of SQL modes
+	 */
+	protected function db_modes() {
+		$result = array();
+
+		$res = $this->api_call( 'query', $this->dbh, 'SELECT @@SESSION.sql_mode' );
+		if ( empty( $res ) ) {
+			return $result;
+		}
+
+		if ( 'mysqli' == $this->api ) {
+			$modes_array = mysqli_fetch_array( $res );
+			if ( empty( $modes_array[0] ) ) {
+				return $result;
+			}
+			$modes_str = $modes_array[0];
+		} else {
+			$modes_str = mysql_result( $res, 0 );
+		}
+		if ( empty( $modes_str ) ) {
+			return $result;
+		}
+
+		$result = explode( ',', $modes_str );
+		return $result;
+	}
+}
Index: src/wp-includes/wp-db.php
===================================================================
--- src/wp-includes/wp-db.php	(revision 29898)
+++ src/wp-includes/wp-db.php	(working copy)
@@ -1,4 +1,7 @@
 <?php
+
+require_once( ABSPATH . WPINC . '/wp-db-api.php' );
+
 /**
  * WordPress DB Class
  *
@@ -49,7 +52,7 @@
  * @subpackage Database
  * @since 0.71
  */
-class wpdb {
+class wpdb extends wpdb_API {
 
 	/**
 	 * Whether to show SQL/DB errors.
@@ -465,51 +468,6 @@
 	public $collate;
 
 	/**
-	 * Database Username
-	 *
-	 * @since 2.9.0
-	 * @access protected
-	 * @var string
-	 */
-	protected $dbuser;
-
-	/**
-	 * Database Password
-	 *
-	 * @since 3.1.0
-	 * @access protected
-	 * @var string
-	 */
-	protected $dbpassword;
-
-	/**
-	 * Database Name
-	 *
-	 * @since 3.1.0
-	 * @access protected
-	 * @var string
-	 */
-	protected $dbname;
-
-	/**
-	 * Database Host
-	 *
-	 * @since 3.1.0
-	 * @access protected
-	 * @var string
-	 */
-	protected $dbhost;
-
-	/**
-	 * Database Handle
-	 *
-	 * @since 0.71
-	 * @access protected
-	 * @var string
-	 */
-	protected $dbh;
-
-	/**
 	 * A textual description of the last query/get_row/get_var call
 	 *
 	 * @since 3.0.0
@@ -545,6 +503,8 @@
 	/**
 	 * Whether to use mysqli over mysql.
 	 *
+	 * @deprecated
+	 * @property-read
 	 * @since 3.9.0
 	 * @access private
 	 * @var bool
@@ -554,6 +514,8 @@
 	/**
 	 * Whether we've managed to successfully connect at some point
 	 *
+	 * @deprecated
+	 * @property-read
 	 * @since 3.9.0
 	 * @access private
 	 * @var bool
@@ -581,28 +543,9 @@
 		if ( WP_DEBUG && WP_DEBUG_DISPLAY )
 			$this->show_errors();
 
-		/* Use ext/mysqli if it exists and:
-		 *  - WP_USE_EXT_MYSQL is defined as false, or
-		 *  - We are a development version of WordPress, or
-		 *  - We are running PHP 5.5 or greater, or
-		 *  - ext/mysql is not loaded.
-		 */
-		if ( function_exists( 'mysqli_connect' ) ) {
-			if ( defined( 'WP_USE_EXT_MYSQL' ) ) {
-				$this->use_mysqli = ! WP_USE_EXT_MYSQL;
-			} elseif ( version_compare( phpversion(), '5.5', '>=' ) || ! function_exists( 'mysql_connect' ) ) {
-				$this->use_mysqli = true;
-			} elseif ( false !== strpos( $GLOBALS['wp_version'], '-' ) ) {
-				$this->use_mysqli = true;
-			}
-		}
-
 		$this->init_charset();
 
-		$this->dbuser = $dbuser;
-		$this->dbpassword = $dbpassword;
-		$this->dbname = $dbname;
-		$this->dbhost = $dbhost;
+		parent::__construct( $dbuser, $dbpassword, $dbname, $dbhost );
 
 		// wp-config.php creation will manually connect when ready.
 		if ( defined( 'WP_SETUP_CONFIG' ) ) {
@@ -632,8 +575,15 @@
 	 * @return mixed The private member
 	 */
 	public function __get( $name ) {
-		if ( 'col_info' == $name )
+		if ( 'col_info' == $name ) {
 			$this->load_col_info();
+		} elseif ( 'use_mysqli' == $name ) {
+			_deprecated_property( __CLASS__ . '::$' . $name, 'X4.2', "Try wpdb_API::api_call('mysqli_*')." );
+			return 'mysqli' == $this->api;
+		} elseif ( 'has_connected' == $name ) {
+			_deprecated_property( __CLASS__ . '::$' . $name, 'X4.2' );
+			return (bool) $this->dbh;
+		}
 
 		return $this->$name;
 	}
@@ -647,6 +597,13 @@
 	 * @param mixed  $value The value to set
 	 */
 	public function __set( $name, $value ) {
+		if ( 'use_mysqli' == $name ) {
+			_deprecated_property( __CLASS__ . '::$' . $name, 'X4.2', "Try wpdb_API::api_call('mysqli_*')." );
+			return;
+		} elseif ( 'has_connected' == $name ) {
+			_deprecated_property( __CLASS__ . '::$' . $name, 'X4.2' );
+			return;
+		}
 		$this->$name = $value;
 	}
 
@@ -660,6 +617,14 @@
 	 * @return bool If the member is set or not
 	 */
 	public function __isset( $name ) {
+		if ( 'use_mysqli' == $name ) {
+			_deprecated_property( __CLASS__ . '::$' . $name, 'X4.2', "Try wpdb_API::api_call('mysqli_*')." );
+			return true;
+		} elseif ( 'has_connected' == $name ) {
+			_deprecated_property( __CLASS__ . '::$' . $name, 'X4.2' );
+			return true;
+		}
+
 		return isset( $this->$name );
 	}
 
@@ -671,6 +636,14 @@
 	 * @param string $name  The private member to unset
 	 */
 	public function __unset( $name ) {
+		if ( 'use_mysqli' == $name ) {
+			_deprecated_property( __CLASS__ . '::$' . $name, 'X4.2', "Try wpdb_API::api_call('mysqli_*')." );
+			return;
+		} elseif ( 'has_connected' == $name ) {
+			_deprecated_property( __CLASS__ . '::$' . $name, 'X4.2' );
+			return;
+		}
+
 		unset( $this->$name );
 	}
 
@@ -709,24 +682,15 @@
 		if ( ! isset( $collate ) )
 			$collate = $this->collate;
 		if ( $this->has_cap( 'collation' ) && ! empty( $charset ) ) {
-			if ( $this->use_mysqli ) {
-				if ( function_exists( 'mysqli_set_charset' ) && $this->has_cap( 'set_charset' ) ) {
-					mysqli_set_charset( $dbh, $charset );
-				} else {
-					$query = $this->prepare( 'SET NAMES %s', $charset );
-					if ( ! empty( $collate ) )
-						$query .= $this->prepare( ' COLLATE %s', $collate );
-					mysqli_query( $query, $dbh );
-				}
+			extract( $this->api_vars( 'set_charset' ) ); /** @var $mysqli_set_charset */
+
+			if ( function_exists( $mysqli_set_charset ) && $this->has_cap( 'set_charset' ) ) {
+				$this->api_call( 'mysqli_set_charset', $dbh, $charset );
 			} else {
-				if ( function_exists( 'mysql_set_charset' ) && $this->has_cap( 'set_charset' ) ) {
-					mysql_set_charset( $charset, $dbh );
-				} else {
-					$query = $this->prepare( 'SET NAMES %s', $charset );
-					if ( ! empty( $collate ) )
-						$query .= $this->prepare( ' COLLATE %s', $collate );
-					mysql_query( $query, $dbh );
-				}
+				$query = $this->prepare( 'SET NAMES %s', $charset );
+				if ( ! empty( $collate ) )
+					$query .= $this->prepare( ' COLLATE %s', $collate );
+				$this->api_call( 'mysqli_query', $dbh, $query );
 			}
 		}
 	}
@@ -743,33 +707,8 @@
 	 */
 	public function set_sql_mode( $modes = array() ) {
 		if ( empty( $modes ) ) {
-			if ( $this->use_mysqli ) {
-				$res = mysqli_query( $this->dbh, 'SELECT @@SESSION.sql_mode' );
-			} else {
-				$res = mysql_query( 'SELECT @@SESSION.sql_mode', $this->dbh );
-			}
-
-			if ( empty( $res ) ) {
-				return;
-			}
-
-			if ( $this->use_mysqli ) {
-				$modes_array = mysqli_fetch_array( $res );
-				if ( empty( $modes_array[0] ) ) {
-					return;
-				}
-				$modes_str = $modes_array[0];
-			} else {
-				$modes_str = mysql_result( $res, 0 );
-			}
-
-			if ( empty( $modes_str ) ) {
-				return;
-			}
-
-			$modes = explode( ',', $modes_str );
+			$modes = $this->db_modes();
 		}
-
 		$modes = array_change_key_case( $modes, CASE_UPPER );
 
 		/**
@@ -782,20 +721,10 @@
 		 * @param array $incompatible_modes An array of incompatible modes.
 		 */
 		$incompatible_modes = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes );
-
-		foreach( $modes as $i => $mode ) {
-			if ( in_array( $mode, $incompatible_modes ) ) {
-				unset( $modes[ $i ] );
-			}
-		}
+		$modes = array_diff($modes, $incompatible_modes);
 
 		$modes_str = implode( ',', $modes );
-
-		if ( $this->use_mysqli ) {
-			mysqli_query( $this->dbh, "SET SESSION sql_mode='$modes_str'" );
-		} else {
-			mysql_query( "SET SESSION sql_mode='$modes_str'", $this->dbh );
-		}
+		$this->api_call( 'mysqli_query', $this->dbh, "SET SESSION sql_mode='$modes_str'" );
 	}
 
 	/**
@@ -979,11 +908,7 @@
 		if ( is_null($dbh) )
 			$dbh = $this->dbh;
 
-		if ( $this->use_mysqli ) {
-			$success = @mysqli_select_db( $dbh, $db );
-		} else {
-			$success = @mysql_select_db( $db, $dbh );
-		}
+		$success = @$this->api_call('mysqli_select_db', $dbh, $db );
 		if ( ! $success ) {
 			$this->ready = false;
 			if ( ! did_action( 'template_redirect' ) ) {
@@ -1034,11 +959,7 @@
 	 */
 	function _real_escape( $string ) {
 		if ( $this->dbh ) {
-			if ( $this->use_mysqli ) {
-				return mysqli_real_escape_string( $this->dbh, $string );
-			} else {
-				return mysql_real_escape_string( $string, $this->dbh );
-			}
+			return $this->api_call( 'mysqli_real_escape_string', $this->dbh, $string );
 		}
 
 		$class = get_class( $this );
@@ -1216,11 +1137,7 @@
 		global $EZSQL_ERROR;
 
 		if ( !$str ) {
-			if ( $this->use_mysqli ) {
-				$str = mysqli_error( $this->dbh );
-			} else {
-				$str = mysql_error( $this->dbh );
-			}
+			$str = $this->api_call( 'mysqli_error', $this->dbh );
 		}
 		$EZSQL_ERROR[] = array( 'query' => $this->last_query, 'error_str' => $str );
 
@@ -1325,11 +1242,7 @@
 		$this->last_error  = '';
 
 		if ( is_resource( $this->result ) ) {
-			if ( $this->use_mysqli ) {
-				mysqli_free_result( $this->result );
-			} else {
-				mysql_free_result( $this->result );
-			}
+			$this->api_call( 'mysqli_free_result', $this->result );
 		}
 	}
 
@@ -1349,72 +1262,7 @@
 
 		$this->is_mysql = true;
 
-		/*
-		 * Deprecated in 3.9+ when using MySQLi. No equivalent
-		 * $new_link parameter exists for mysqli_* functions.
-		 */
-		$new_link = defined( 'MYSQL_NEW_LINK' ) ? MYSQL_NEW_LINK : true;
-		$client_flags = defined( 'MYSQL_CLIENT_FLAGS' ) ? MYSQL_CLIENT_FLAGS : 0;
-
-		if ( $this->use_mysqli ) {
-			$this->dbh = mysqli_init();
-
-			// mysqli_real_connect doesn't support the host param including a port or socket
-			// like mysql_connect does. This duplicates how mysql_connect detects a port and/or socket file.
-			$port = null;
-			$socket = null;
-			$host = $this->dbhost;
-			$port_or_socket = strstr( $host, ':' );
-			if ( ! empty( $port_or_socket ) ) {
-				$host = substr( $host, 0, strpos( $host, ':' ) );
-				$port_or_socket = substr( $port_or_socket, 1 );
-				if ( 0 !== strpos( $port_or_socket, '/' ) ) {
-					$port = intval( $port_or_socket );
-					$maybe_socket = strstr( $port_or_socket, ':' );
-					if ( ! empty( $maybe_socket ) ) {
-						$socket = substr( $maybe_socket, 1 );
-					}
-				} else {
-					$socket = $port_or_socket;
-				}
-			}
-
-			if ( WP_DEBUG ) {
-				mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
-			} else {
-				@mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags );
-			}
-
-			if ( $this->dbh->connect_errno ) {
-				$this->dbh = null;
-
-				/* It's possible ext/mysqli is misconfigured. Fall back to ext/mysql if:
-		 		 *  - We haven't previously connected, and
-		 		 *  - WP_USE_EXT_MYSQL isn't set to false, and
-		 		 *  - ext/mysql is loaded.
-		 		 */
-				$attempt_fallback = true;
-
-				if ( $this->has_connected ) {
-					$attempt_fallback = false;
-				} else if ( defined( 'WP_USE_EXT_MYSQL' ) && ! WP_USE_EXT_MYSQL ) {
-					$attempt_fallback = false;
-				} else if ( ! function_exists( 'mysql_connect' ) ) {
-					$attempt_fallback = false;
-				}
-
-				if ( $attempt_fallback ) {
-					$this->use_mysqli = false;
-					$this->db_connect();
-				}
-			}
-		} else {
-			if ( WP_DEBUG ) {
-				$this->dbh = mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags );
-			} else {
-				$this->dbh = @mysql_connect( $this->dbhost, $this->dbuser, $this->dbpassword, $new_link, $client_flags );
-			}
-		}
+		$result = parent::db_connect();
 
 		if ( ! $this->dbh && $allow_bail ) {
 			wp_load_translations_early();
@@ -1436,18 +1284,16 @@
 <p>If you're unsure what these terms mean you should probably contact your host. If you still need help you can always visit the <a href='https://wordpress.org/support/'>WordPress Support Forums</a>.</p>
 " ), htmlspecialchars( $this->dbhost, ENT_QUOTES ) ), 'db_connect_fail' );
 
-			return false;
 		} else if ( $this->dbh ) {
-			$this->has_connected = true;
+
 			$this->set_charset( $this->dbh );
 			$this->set_sql_mode();
 			$this->ready = true;
 			$this->select( $this->dbname, $this->dbh );
 
-			return true;
 		}
 
-		return false;
+		return $result;
 	}
 
 	/**
@@ -1465,14 +1311,8 @@
 	 * @return bool True if the connection is up.
 	 */
 	public function check_connection( $allow_bail = true ) {
-		if ( $this->use_mysqli ) {
-			if ( @mysqli_ping( $this->dbh ) ) {
-				return true;
-			}
-		} else {
-			if ( @mysql_ping( $this->dbh ) ) {
-				return true;
-			}
+		if ( @$this->api_call( 'mysqli_ping', $this->dbh ) ) {
+			return true;
 		}
 
 		$error_reporting = false;
@@ -1527,6 +1367,28 @@
 	}
 
 	/**
+	 * Called right before issuing the query, can be used when apply_filters( 'query', $query ) does not work.
+	 *
+	 * @since X4.2
+	 * @param string $query
+	 * @return string mixed
+	 */
+	protected function filter_query($query) {
+		return $query;
+	}
+
+	/**
+	 * Called on each results row right after fetching it.
+	 *
+	 * @since X4.2
+	 * @param object $row
+	 * @return object mixed
+	 */
+	protected function filter_row($row) {
+		return $row;
+	}
+
+	/**
 	 * Perform a MySQL database query, using current database connection.
 	 *
 	 * More information can be found on the codex page.
@@ -1540,6 +1402,15 @@
 		if ( ! $this->ready )
 			return false;
 
+		extract( $this->api_vars( 'errno', 'error', 'affected_rows', 'insert_id', 'fetch_object' ) );
+		/**
+		 * @var $mysqli_errno
+		 * @var $mysqli_error
+		 * @var $mysqli_affected_rows
+		 * @var $mysqli_insert_id
+		 * @var $mysqli_fetch_object'
+		 */
+
 		/**
 		 * Filter the database query.
 		 *
@@ -1551,6 +1422,7 @@
 		 * @param string $query Database query.
 		 */
 		$query = apply_filters( 'query', $query );
+		$query = $this->filter_query($query);
 
 		$this->flush();
 
@@ -1563,16 +1435,12 @@
 		$this->_do_query( $query );
 
 		// MySQL server has gone away, try to reconnect
-		$mysql_errno = 0;
+		$errno = 0;
 		if ( ! empty( $this->dbh ) ) {
-			if ( $this->use_mysqli ) {
-				$mysql_errno = mysqli_errno( $this->dbh );
-			} else {
-				$mysql_errno = mysql_errno( $this->dbh );
-			}
+			$errno = $mysqli_errno( $this->dbh );
 		}
 
-		if ( empty( $this->dbh ) || 2006 == $mysql_errno ) {
+		if ( empty( $this->dbh ) || 2006 == $errno ) {
 			if ( $this->check_connection() ) {
 				$this->_do_query( $query );
 			} else {
@@ -1582,11 +1450,7 @@
 		}
 
 		// If there is an error then take note of it..
-		if ( $this->use_mysqli ) {
-			$this->last_error = mysqli_error( $this->dbh );
-		} else {
-			$this->last_error = mysql_error( $this->dbh );
-		}
+		$this->last_error = $mysqli_error( $this->dbh );
 
 		if ( $this->last_error ) {
 			// Clear insert_id on a subsequent failed insert.
@@ -1600,33 +1464,19 @@
 		if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
 			$return_val = $this->result;
 		} elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
-			if ( $this->use_mysqli ) {
-				$this->rows_affected = mysqli_affected_rows( $this->dbh );
-			} else {
-				$this->rows_affected = mysql_affected_rows( $this->dbh );
-			}
+			$this->rows_affected = $mysqli_affected_rows( $this->dbh );
 			// Take note of the insert_id
 			if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
-				if ( $this->use_mysqli ) {
-					$this->insert_id = mysqli_insert_id( $this->dbh );
-				} else {
-					$this->insert_id = mysql_insert_id( $this->dbh );
-				}
+				$this->insert_id = $mysqli_insert_id( $this->dbh );
 			}
 			// Return number of rows affected
 			$return_val = $this->rows_affected;
 		} else {
 			$num_rows = 0;
-			if ( $this->use_mysqli ) {
-				while ( $row = @mysqli_fetch_object( $this->result ) ) {
-					$this->last_result[$num_rows] = $row;
-					$num_rows++;
-				}
-			} else {
-				while ( $row = @mysql_fetch_object( $this->result ) ) {
-					$this->last_result[$num_rows] = $row;
-					$num_rows++;
-				}
+			while ( $row = @$mysqli_fetch_object( $this->result ) ) {
+				$row = $this->filter_row($row);
+				$this->last_result[$num_rows] = $row;
+				$num_rows++;
 			}
 
 			// Log number of rows the query returned
@@ -1653,11 +1503,7 @@
 			$this->timer_start();
 		}
 
-		if ( $this->use_mysqli ) {
-			$this->result = @mysqli_query( $this->dbh, $query );
-		} else {
-			$this->result = @mysql_query( $query, $this->dbh );
-		}
+		$this->result = @$this->api_call( 'mysqli_query', $this->dbh, $query );
 		$this->num_queries++;
 
 		if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
@@ -2001,14 +1847,10 @@
 		if ( $this->col_info )
 			return;
 
-		if ( $this->use_mysqli ) {
-			for ( $i = 0; $i < @mysqli_num_fields( $this->result ); $i++ ) {
-				$this->col_info[ $i ] = @mysqli_fetch_field( $this->result );
-			}
-		} else {
-			for ( $i = 0; $i < @mysql_num_fields( $this->result ); $i++ ) {
-				$this->col_info[ $i ] = @mysql_fetch_field( $this->result, $i );
-			}
+		extract( $this->api_vars( 'num_fields', 'fetch_field' ) ); /** @var $mysqli_num_fields *//** @var $mysqli_fetch_field */
+
+		for ( $i = 0; $i < @$mysqli_num_fields( $this->result ); $i++ ) {
+			$this->col_info[ $i ] = @$mysqli_fetch_field( $this->result ); // no need to use field_offset here!
 		}
 	}
 
@@ -2180,11 +2022,7 @@
 	 * @return false|string false on failure, version number on success
 	 */
 	public function db_version() {
-		if ( $this->use_mysqli ) {
-			$server_info = mysqli_get_server_info( $this->dbh );
-		} else {
-			$server_info = mysql_get_server_info( $this->dbh );
-		}
+		$server_info = $this->api_call( 'mysqli_get_server_info', $this->dbh );
 		return preg_replace( '/[^0-9.].*/', '', $server_info );
 	}
 }
Index: tests/phpunit/tests/db.php
===================================================================
--- tests/phpunit/tests/db.php	(revision 29898)
+++ tests/phpunit/tests/db.php	(working copy)
@@ -50,11 +50,7 @@
 		$var = $wpdb->get_var( "SELECT ID FROM $wpdb->users LIMIT 1" );
 		$this->assertGreaterThan( 0, $var );
 
-		if ( $wpdb->use_mysqli ) {
-			mysqli_close( $wpdb->dbh );
-		} else {
-			mysql_close( $wpdb->dbh );
-		}
+		$wpdb->api_call( 'close', $wpdb->dbh );
 		unset( $wpdb->dbh );
 
 		$var = $wpdb->get_var( "SELECT ID FROM $wpdb->users LIMIT 1" );
@@ -145,7 +141,7 @@
 	 * @dataProvider data_like_query
 	 * @param $data string The haystack, raw.
 	 * @param $like string The like phrase, raw.
-         * @param $result string The expected comparison result; '1' = true, '0' = false
+	 * @param $result string The expected comparison result; '1' = true, '0' = false
 	 */
 	function test_like_query( $data, $like, $result ) {
 		global $wpdb;
@@ -458,4 +454,24 @@
 		$this->assertEquals( $expected2, $wpdb->last_query );
 		$wpdb->suppress_errors( $suppress );
 	}
+
+	/**
+	 * @expectedDeprecated wpdb::$use_mysqli
+	 * @expectedDeprecated wpdb::$has_connected
+	 *
+	 * @ticket
+	 */
+	function test_deprecated_properties() {
+		global $wpdb;
+
+		$before = $wpdb->use_mysqli;
+		$wpdb->use_mysqli = ! $before;
+		$after = $wpdb->use_mysqli;
+		$this->assertEquals( $before, $after );  // readonly
+
+		$before = $wpdb->has_connected;
+		$wpdb->has_connected = ! $before;
+		$after = $wpdb->has_connected;
+		$this->assertEquals( $before, $after );  // readonly
+	}
 }
