2013-05-03 01:39:16 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* This is the MySQL database abstraction layer.
|
|
|
|
|
*
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*
|
|
|
|
|
* @file
|
|
|
|
|
* @ingroup Database
|
|
|
|
|
*/
|
2017-02-07 04:49:57 +00:00
|
|
|
namespace Wikimedia\Rdbms;
|
|
|
|
|
|
|
|
|
|
use DateTime;
|
|
|
|
|
use DateTimeZone;
|
|
|
|
|
use MediaWiki;
|
|
|
|
|
use InvalidArgumentException;
|
|
|
|
|
use Exception;
|
|
|
|
|
use stdClass;
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Database abstraction object for MySQL.
|
|
|
|
|
* Defines methods independent on used MySQL extension.
|
|
|
|
|
*
|
|
|
|
|
* @ingroup Database
|
|
|
|
|
* @since 1.22
|
|
|
|
|
* @see Database
|
|
|
|
|
*/
|
2016-09-28 23:08:15 +00:00
|
|
|
abstract class DatabaseMysqlBase extends Database {
|
2013-05-23 00:23:21 +00:00
|
|
|
/** @var MysqlMasterPos */
|
2016-09-03 22:06:59 +00:00
|
|
|
protected $lastKnownReplicaPos;
|
2016-09-03 14:13:47 +00:00
|
|
|
/** @var string Method to detect replica DB lag */
|
2015-09-25 19:53:04 +00:00
|
|
|
protected $lagDetectionMethod;
|
2016-09-03 14:13:47 +00:00
|
|
|
/** @var array Method to detect replica DB lag */
|
2016-03-08 20:36:03 +00:00
|
|
|
protected $lagDetectionOptions = [];
|
2016-05-21 00:26:08 +00:00
|
|
|
/** @var bool bool Whether to use GTID methods */
|
|
|
|
|
protected $useGTIDs = false;
|
2016-08-22 17:37:31 +00:00
|
|
|
/** @var string|null */
|
|
|
|
|
protected $sslKeyPath;
|
|
|
|
|
/** @var string|null */
|
|
|
|
|
protected $sslCertPath;
|
|
|
|
|
/** @var string|null */
|
|
|
|
|
protected $sslCAPath;
|
|
|
|
|
/** @var string[]|null */
|
|
|
|
|
protected $sslCiphers;
|
2016-09-16 03:33:25 +00:00
|
|
|
/** @var string sql_mode value to send on connection */
|
|
|
|
|
protected $sqlMode;
|
|
|
|
|
/** @var bool Use experimental UTF-8 transmission encoding */
|
|
|
|
|
protected $utf8Mode;
|
|
|
|
|
|
2014-08-31 10:43:50 +00:00
|
|
|
/** @var string|null */
|
|
|
|
|
private $serverVersion = null;
|
|
|
|
|
|
2015-09-25 19:53:04 +00:00
|
|
|
/**
|
|
|
|
|
* Additional $params include:
|
|
|
|
|
* - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
|
2016-05-21 00:26:08 +00:00
|
|
|
* pt-heartbeat assumes the table is at heartbeat.heartbeat
|
|
|
|
|
* and uses UTC timestamps in the heartbeat.ts column.
|
|
|
|
|
* (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
|
2016-03-08 20:36:03 +00:00
|
|
|
* - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change
|
2016-05-21 00:26:08 +00:00
|
|
|
* the default behavior. Normally, the heartbeat row with the server
|
|
|
|
|
* ID of this server's master will be used. Set the "conds" field to
|
|
|
|
|
* override the query conditions, e.g. ['shard' => 's1'].
|
|
|
|
|
* - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
|
2016-08-22 17:37:31 +00:00
|
|
|
* - sslKeyPath : path to key file [default: null]
|
|
|
|
|
* - sslCertPath : path to certificate file [default: null]
|
|
|
|
|
* - sslCAPath : parth to certificate authority PEM files [default: null]
|
|
|
|
|
* - sslCiphers : array list of allowable ciphers [default: null]
|
2015-09-25 19:53:04 +00:00
|
|
|
* @param array $params
|
|
|
|
|
*/
|
|
|
|
|
function __construct( array $params ) {
|
|
|
|
|
$this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
|
|
|
|
|
? $params['lagDetectionMethod']
|
|
|
|
|
: 'Seconds_Behind_Master';
|
2016-03-08 20:36:03 +00:00
|
|
|
$this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
|
|
|
|
|
? $params['lagDetectionOptions']
|
|
|
|
|
: [];
|
2016-05-21 00:26:08 +00:00
|
|
|
$this->useGTIDs = !empty( $params['useGTIDs' ] );
|
2016-08-22 17:37:31 +00:00
|
|
|
foreach ( [ 'KeyPath', 'CertPath', 'CAPath', 'Ciphers' ] as $name ) {
|
|
|
|
|
$var = "ssl{$name}";
|
|
|
|
|
if ( isset( $params[$var] ) ) {
|
|
|
|
|
$this->$var = $params[$var];
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-16 03:33:25 +00:00
|
|
|
$this->sqlMode = isset( $params['sqlMode'] ) ? $params['sqlMode'] : '';
|
|
|
|
|
$this->utf8Mode = !empty( $params['utf8Mode'] );
|
2016-10-11 21:29:04 +00:00
|
|
|
|
|
|
|
|
parent::__construct( $params );
|
2015-09-25 19:53:04 +00:00
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function getType() {
|
2013-05-03 01:39:16 +00:00
|
|
|
return 'mysql';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $server
|
|
|
|
|
* @param string $user
|
|
|
|
|
* @param string $password
|
|
|
|
|
* @param string $dbName
|
|
|
|
|
* @throws Exception|DBConnectionError
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function open( $server, $user, $password, $dbName ) {
|
2015-06-25 00:24:00 +00:00
|
|
|
# Close/unset connection handle
|
2013-05-03 01:39:16 +00:00
|
|
|
$this->close();
|
2015-06-25 00:24:00 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
$this->mServer = $server;
|
|
|
|
|
$this->mUser = $user;
|
|
|
|
|
$this->mPassword = $password;
|
|
|
|
|
$this->mDBname = $dbName;
|
|
|
|
|
|
|
|
|
|
$this->installErrorHandler();
|
|
|
|
|
try {
|
2016-09-16 03:33:25 +00:00
|
|
|
$this->mConn = $this->mysqlConnect( $this->mServer );
|
2013-08-24 15:06:25 +00:00
|
|
|
} catch ( Exception $ex ) {
|
2013-11-15 15:13:19 +00:00
|
|
|
$this->restoreErrorHandler();
|
2013-05-03 01:39:16 +00:00
|
|
|
throw $ex;
|
|
|
|
|
}
|
|
|
|
|
$error = $this->restoreErrorHandler();
|
|
|
|
|
|
|
|
|
|
# Always log connection errors
|
|
|
|
|
if ( !$this->mConn ) {
|
|
|
|
|
if ( !$error ) {
|
|
|
|
|
$error = $this->lastError();
|
|
|
|
|
}
|
2016-10-04 20:51:03 +00:00
|
|
|
$this->connLogger->error(
|
2014-06-23 22:25:55 +00:00
|
|
|
"Error connecting to {db_server}: {error}",
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->getLogContext( [
|
2014-06-23 22:25:55 +00:00
|
|
|
'method' => __METHOD__,
|
|
|
|
|
'error' => $error,
|
2016-02-17 09:09:32 +00:00
|
|
|
] )
|
2014-06-23 22:25:55 +00:00
|
|
|
);
|
2016-10-04 20:51:03 +00:00
|
|
|
$this->connLogger->debug( "DB connection error\n" .
|
2013-05-03 01:39:16 +00:00
|
|
|
"Server: $server, User: $user, Password: " .
|
|
|
|
|
substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
|
|
|
|
|
|
2014-01-02 16:21:59 +00:00
|
|
|
$this->reportConnectionError( $error );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $dbName != '' ) {
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\suppressWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
$success = $this->selectDB( $dbName );
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\restoreWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( !$success ) {
|
2016-09-16 03:33:25 +00:00
|
|
|
$this->queryLogger->error(
|
2014-06-23 22:25:55 +00:00
|
|
|
"Error selecting database {db_name} on server {db_server}",
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->getLogContext( [
|
2014-06-23 22:25:55 +00:00
|
|
|
'method' => __METHOD__,
|
2016-02-17 09:09:32 +00:00
|
|
|
] )
|
2014-06-23 22:25:55 +00:00
|
|
|
);
|
2016-09-16 03:33:25 +00:00
|
|
|
$this->queryLogger->debug(
|
|
|
|
|
"Error selecting database $dbName on server {$this->mServer}" );
|
2013-05-03 01:39:16 +00:00
|
|
|
|
2014-01-02 16:21:59 +00:00
|
|
|
$this->reportConnectionError( "Error selecting database $dbName" );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-27 22:40:15 +00:00
|
|
|
// Tell the server what we're communicating with
|
|
|
|
|
if ( !$this->connectInitCharset() ) {
|
2014-01-02 16:21:59 +00:00
|
|
|
$this->reportConnectionError( "Error setting character set" );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2013-12-27 22:40:15 +00:00
|
|
|
|
2015-01-14 00:57:52 +00:00
|
|
|
// Abstract over any insane MySQL defaults
|
2016-02-17 09:09:32 +00:00
|
|
|
$set = [ 'group_concat_max_len = 262144' ];
|
2013-05-03 01:39:16 +00:00
|
|
|
// Set SQL mode, default is turning them all off, can be overridden or skipped with null
|
2016-09-16 03:33:25 +00:00
|
|
|
if ( is_string( $this->sqlMode ) ) {
|
|
|
|
|
$set[] = 'sql_mode = ' . $this->addQuotes( $this->sqlMode );
|
2015-01-14 00:57:52 +00:00
|
|
|
}
|
2015-04-24 18:07:02 +00:00
|
|
|
// Set any custom settings defined by site config
|
|
|
|
|
// (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html)
|
|
|
|
|
foreach ( $this->mSessionVars as $var => $val ) {
|
|
|
|
|
// Escape strings but not numbers to avoid MySQL complaining
|
|
|
|
|
if ( !is_int( $val ) && !is_float( $val ) ) {
|
|
|
|
|
$val = $this->addQuotes( $val );
|
|
|
|
|
}
|
|
|
|
|
$set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val;
|
|
|
|
|
}
|
2015-01-14 00:57:52 +00:00
|
|
|
|
|
|
|
|
if ( $set ) {
|
2013-11-21 23:54:02 +00:00
|
|
|
// Use doQuery() to avoid opening implicit transactions (DBO_TRX)
|
2015-09-24 17:10:45 +00:00
|
|
|
$success = $this->doQuery( 'SET ' . implode( ', ', $set ) );
|
2013-11-21 23:54:02 +00:00
|
|
|
if ( !$success ) {
|
2016-09-16 03:33:25 +00:00
|
|
|
$this->queryLogger->error(
|
2015-01-14 00:57:52 +00:00
|
|
|
'Error setting MySQL variables on server {db_server} (check $wgSQLMode)',
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->getLogContext( [
|
2014-06-23 22:25:55 +00:00
|
|
|
'method' => __METHOD__,
|
2016-02-17 09:09:32 +00:00
|
|
|
] )
|
2014-06-23 22:25:55 +00:00
|
|
|
);
|
2015-01-14 00:57:52 +00:00
|
|
|
$this->reportConnectionError(
|
|
|
|
|
'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' );
|
2013-11-21 23:54:02 +00:00
|
|
|
}
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->mOpened = true;
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-27 22:40:15 +00:00
|
|
|
/**
|
|
|
|
|
* Set the character set information right after connection
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
protected function connectInitCharset() {
|
2016-09-16 03:33:25 +00:00
|
|
|
if ( $this->utf8Mode ) {
|
2013-12-27 22:40:15 +00:00
|
|
|
// Tell the server we're communicating with it in UTF-8.
|
|
|
|
|
// This may engage various charset conversions.
|
|
|
|
|
return $this->mysqlSetCharset( 'utf8' );
|
|
|
|
|
} else {
|
|
|
|
|
return $this->mysqlSetCharset( 'binary' );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
|
|
|
|
* Open a connection to a MySQL server
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $realServer
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return mixed Raw connection
|
|
|
|
|
* @throws DBConnectionError
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlConnect( $realServer );
|
|
|
|
|
|
2013-11-15 22:21:40 +00:00
|
|
|
/**
|
|
|
|
|
* Set the character set of the MySQL link
|
|
|
|
|
*
|
|
|
|
|
* @param string $charset
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlSetCharset( $charset );
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
2013-05-03 01:39:16 +00:00
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function freeResult( $res ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\suppressWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
$ok = $this->mysqlFreeResult( $res );
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\restoreWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( !$ok ) {
|
|
|
|
|
throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Free result memory
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param resource $res Raw result
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlFreeResult( $res );
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
|
|
|
|
* @return stdClass|bool
|
2013-05-03 01:39:16 +00:00
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function fetchObject( $res ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\suppressWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
$row = $this->mysqlFetchObject( $res );
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\restoreWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
$errno = $this->lastErrno();
|
|
|
|
|
// Unfortunately, mysql_fetch_object does not reset the last errno.
|
|
|
|
|
// Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
|
|
|
|
|
// these are the only errors mysql_fetch_object can cause.
|
2016-10-13 05:34:26 +00:00
|
|
|
// See https://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( $errno == 2000 || $errno == 2013 ) {
|
2013-11-20 10:13:51 +00:00
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() )
|
|
|
|
|
);
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return $row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch a result row as an object
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param resource $res Raw result
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return stdClass
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlFetchObject( $res );
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return array|bool
|
|
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function fetchRow( $res ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\suppressWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
$row = $this->mysqlFetchArray( $res );
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\restoreWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
$errno = $this->lastErrno();
|
|
|
|
|
// Unfortunately, mysql_fetch_array does not reset the last errno.
|
|
|
|
|
// Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
|
|
|
|
|
// these are the only errors mysql_fetch_array can cause.
|
2016-10-13 05:34:26 +00:00
|
|
|
// See https://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( $errno == 2000 || $errno == 2013 ) {
|
2013-11-20 10:13:51 +00:00
|
|
|
throw new DBUnexpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() )
|
|
|
|
|
);
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return $row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch a result row as an associative and numeric array
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param resource $res Raw result
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlFetchArray( $res );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @throws DBUnexpectedError
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
function numRows( $res ) {
|
|
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\suppressWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
$n = $this->mysqlNumRows( $res );
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\restoreWarnings();
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
// Unfortunately, mysql_num_rows does not reset the last errno.
|
|
|
|
|
// We are not checking for any errors here, since
|
|
|
|
|
// these are no errors mysql_num_rows can cause.
|
2016-10-13 05:34:26 +00:00
|
|
|
// See https://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
|
2015-09-12 13:54:13 +00:00
|
|
|
// See https://phabricator.wikimedia.org/T44430
|
2013-05-03 01:39:16 +00:00
|
|
|
return $n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get number of rows in result
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param resource $res Raw result
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlNumRows( $res );
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return int
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function numFields( $res ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return $this->mysqlNumFields( $res );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get number of fields in result
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param resource $res Raw result
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlNumFields( $res );
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
2014-04-19 11:55:27 +00:00
|
|
|
* @param int $n
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function fieldName( $res, $n ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return $this->mysqlFieldName( $res, $n );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the name of the specified field in a result
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
2014-04-19 11:55:27 +00:00
|
|
|
* @param int $n
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlFieldName( $res, $n );
|
|
|
|
|
|
2013-12-07 20:08:47 +00:00
|
|
|
/**
|
|
|
|
|
* mysql_field_type() wrapper
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
2014-04-19 11:55:27 +00:00
|
|
|
* @param int $n
|
2013-12-07 20:08:47 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function fieldType( $res, $n ) {
|
|
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->mysqlFieldType( $res, $n );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the type of the specified field in a result
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
|
|
|
|
* @param int $n
|
2013-12-07 20:08:47 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlFieldType( $res, $n );
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
|
|
|
|
* @param int $row
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function dataSeek( $res, $row ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( $res instanceof ResultWrapper ) {
|
|
|
|
|
$res = $res->result;
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return $this->mysqlDataSeek( $res, $row );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Move internal result pointer
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param ResultWrapper|resource $res
|
|
|
|
|
* @param int $row
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlDataSeek( $res, $row );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function lastError() {
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( $this->mConn ) {
|
|
|
|
|
# Even if it's non-zero, it can still be invalid
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\suppressWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
$error = $this->mysqlError( $this->mConn );
|
|
|
|
|
if ( !$error ) {
|
|
|
|
|
$error = $this->mysqlError();
|
|
|
|
|
}
|
2015-06-10 18:29:05 +00:00
|
|
|
MediaWiki\restoreWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
} else {
|
|
|
|
|
$error = $this->mysqlError();
|
|
|
|
|
}
|
|
|
|
|
if ( $error ) {
|
|
|
|
|
$error .= ' (' . $this->mServer . ')';
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return $error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the text of the error message from previous MySQL operation
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param resource $conn Raw connection
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlError( $conn = null );
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $table
|
|
|
|
|
* @param array $uniqueIndexes
|
|
|
|
|
* @param array $rows
|
|
|
|
|
* @param string $fname
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return ResultWrapper
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
return $this->nativeReplace( $table, $rows, $fname );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Estimate rows in dataset
|
|
|
|
|
* Returns estimated count, based on EXPLAIN output
|
|
|
|
|
* Takes same arguments as Database::select()
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string|array $table
|
|
|
|
|
* @param string|array $vars
|
|
|
|
|
* @param string|array $conds
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @param string|array $options
|
|
|
|
|
* @return bool|int
|
2013-05-03 01:39:16 +00:00
|
|
|
*/
|
2013-11-20 10:13:51 +00:00
|
|
|
public function estimateRowCount( $table, $vars = '*', $conds = '',
|
2016-02-17 09:09:32 +00:00
|
|
|
$fname = __METHOD__, $options = []
|
2013-11-20 10:13:51 +00:00
|
|
|
) {
|
2013-05-03 01:39:16 +00:00
|
|
|
$options['EXPLAIN'] = true;
|
|
|
|
|
$res = $this->select( $table, $vars, $conds, $fname, $options );
|
|
|
|
|
if ( $res === false ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if ( !$this->numRows( $res ) ) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$rows = 1;
|
|
|
|
|
foreach ( $res as $plan ) {
|
|
|
|
|
$rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2015-01-22 15:36:18 +00:00
|
|
|
return (int)$rows;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2016-11-03 05:24:51 +00:00
|
|
|
public function tableExists( $table, $fname = __METHOD__ ) {
|
2017-06-19 17:20:43 +00:00
|
|
|
// Split database and table into proper variables as Database::tableName() returns
|
|
|
|
|
// shared tables prefixed with their database, which do not work in SHOW TABLES statements
|
2017-06-27 18:31:35 +00:00
|
|
|
list( $database, , $prefix, $table ) = $this->qualifiedTableComponents( $table );
|
|
|
|
|
$tableName = "{$prefix}{$table}";
|
2017-06-19 17:20:43 +00:00
|
|
|
|
2017-06-27 18:31:35 +00:00
|
|
|
if ( isset( $this->mSessionTempTables[$tableName] ) ) {
|
|
|
|
|
return true; // already known to exist and won't show in SHOW TABLES anyway
|
|
|
|
|
}
|
2017-06-19 17:20:43 +00:00
|
|
|
|
New maintenance script to clean up rows with invalid DB keys
The TitleValue constructor, used by the link cache among other things,
throws an exception for DB keys which do not satisfy a simple sanity test
(starting or ending with _, or containing a space, tab, CR or LF
character). This has broken certain special pages on a number of WMF sites;
see T99736, T146778 and T155091.
The new cleanupInvalidDbKeys.php script allows these bogus entries to be
removed from the DB, making sure these exceptions won't be thrown in the
future. It cleans the title columns of the page, archive, redirect,
logging, category, protected_titles, recentchanges, watchlist, pagelinks,
templatelinks, and categorylinks tables.
The script doesn't support batching; most wikis should have fewer than 500
broken entries in each table. If need be, the script can be run several
times.
To make the LIKE queries work properly I had to fix the broken escaping
behaviour of Database::buildLike() -- previously it had a habit of double-
escaping things. Now an ESCAPE clause is added to change the escape
character from the problematic default backslash, and tests are added to
cover the changes.
Bug: T155091
Change-Id: I908e795e884e35be91852c0eaf056d6acfda31d8
2017-03-10 13:27:27 +00:00
|
|
|
// We can't use buildLike() here, because it specifies an escape character
|
|
|
|
|
// other than the backslash, which is the only one supported by SHOW TABLES
|
2017-06-27 18:31:35 +00:00
|
|
|
$encLike = $this->escapeLikeInternal( $tableName, '\\' );
|
2016-09-20 17:30:00 +00:00
|
|
|
|
2017-06-27 18:31:35 +00:00
|
|
|
// If the database has been specified (such as for shared tables), use "FROM"
|
2017-06-19 17:20:43 +00:00
|
|
|
if ( $database !== '' ) {
|
2017-06-27 18:31:35 +00:00
|
|
|
$encDatabase = $this->addIdentifierQuotes( $database );
|
|
|
|
|
$query = "SHOW TABLES FROM $encDatabase LIKE '$encLike'";
|
2017-06-19 17:20:43 +00:00
|
|
|
} else {
|
|
|
|
|
$query = "SHOW TABLES LIKE '$encLike'";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->query( $query, $fname )->numRows() > 0;
|
2016-09-20 17:30:00 +00:00
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $table
|
|
|
|
|
* @param string $field
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool|MySQLField
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function fieldInfo( $table, $field ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
$res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
|
|
|
|
|
if ( !$res ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
$n = $this->mysqlNumFields( $res->result );
|
|
|
|
|
for ( $i = 0; $i < $n; $i++ ) {
|
|
|
|
|
$meta = $this->mysqlFetchField( $res->result, $i );
|
|
|
|
|
if ( $field == $meta->name ) {
|
|
|
|
|
return new MySQLField( $meta );
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get column information from a result
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param resource $res Raw result
|
|
|
|
|
* @param int $n
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return stdClass
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlFetchField( $res, $n );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get information about an index into an object
|
|
|
|
|
* Returns false if the index does not exist
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $table
|
|
|
|
|
* @param string $index
|
|
|
|
|
* @param string $fname
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool|array|null False or null on failure
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function indexInfo( $table, $index, $fname = __METHOD__ ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
# SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
|
|
|
|
|
# SHOW INDEX should work for 3.x and up:
|
2016-10-13 05:34:26 +00:00
|
|
|
# https://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
|
2013-05-03 01:39:16 +00:00
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
$index = $this->indexName( $index );
|
|
|
|
|
|
|
|
|
|
$sql = 'SHOW INDEX FROM ' . $table;
|
|
|
|
|
$res = $this->query( $sql, $fname );
|
|
|
|
|
|
|
|
|
|
if ( !$res ) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$result = [];
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
if ( $row->Key_name == $index ) {
|
|
|
|
|
$result[] = $row;
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return empty( $result ) ? false : $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $s
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function strencode( $s ) {
|
2016-08-10 02:15:05 +00:00
|
|
|
return $this->mysqlRealEscapeString( $s );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2015-10-03 23:36:55 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $s
|
|
|
|
|
* @return mixed
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlRealEscapeString( $s );
|
|
|
|
|
|
2016-10-06 17:39:08 +00:00
|
|
|
public function addQuotes( $s ) {
|
|
|
|
|
if ( is_bool( $s ) ) {
|
|
|
|
|
// Parent would transform to int, which does not play nice with MySQL type juggling.
|
|
|
|
|
// When searching for an int in a string column, the strings are cast to int, which
|
|
|
|
|
// means false would match any string not starting with a number.
|
|
|
|
|
$s = (string)(int)$s;
|
|
|
|
|
}
|
|
|
|
|
return parent::addQuotes( $s );
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
|
|
|
|
* MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $s
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function addIdentifierQuotes( $s ) {
|
2013-10-08 18:14:52 +00:00
|
|
|
// Characters in the range \u0001-\uFFFF are valid in a quoted identifier
|
|
|
|
|
// Remove NUL bytes and escape backticks by doubling
|
2016-02-17 09:09:32 +00:00
|
|
|
return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`';
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $name
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function isQuotedIdentifier( $name ) {
|
|
|
|
|
return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-03 05:24:51 +00:00
|
|
|
public function getLag() {
|
2015-12-04 00:37:56 +00:00
|
|
|
if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
|
2015-09-25 19:53:04 +00:00
|
|
|
return $this->getLagFromPtHeartbeat();
|
|
|
|
|
} else {
|
|
|
|
|
return $this->getLagFromSlaveStatus();
|
|
|
|
|
}
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2015-12-04 00:37:56 +00:00
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function getLagDetectionMethod() {
|
|
|
|
|
return $this->lagDetectionMethod;
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
|
|
|
|
* @return bool|int
|
|
|
|
|
*/
|
2015-09-25 19:53:04 +00:00
|
|
|
protected function getLagFromSlaveStatus() {
|
2013-05-03 01:39:16 +00:00
|
|
|
$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
|
2015-09-25 19:53:04 +00:00
|
|
|
$row = $res ? $res->fetchObject() : false;
|
|
|
|
|
if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
|
|
|
|
|
return intval( $row->Seconds_Behind_Master );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2015-09-25 19:53:04 +00:00
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return bool|float
|
|
|
|
|
*/
|
|
|
|
|
protected function getLagFromPtHeartbeat() {
|
2016-03-08 20:36:03 +00:00
|
|
|
$options = $this->lagDetectionOptions;
|
|
|
|
|
|
|
|
|
|
if ( isset( $options['conds'] ) ) {
|
|
|
|
|
// Best method for multi-DC setups: use logical channel names
|
|
|
|
|
$data = $this->getHeartbeatData( $options['conds'] );
|
|
|
|
|
} else {
|
|
|
|
|
// Standard method: use master server ID (works with stock pt-heartbeat)
|
|
|
|
|
$masterInfo = $this->getMasterServerInfo();
|
|
|
|
|
if ( !$masterInfo ) {
|
2016-09-16 03:33:25 +00:00
|
|
|
$this->queryLogger->error(
|
2016-03-08 20:36:03 +00:00
|
|
|
"Unable to query master of {db_server} for server ID",
|
|
|
|
|
$this->getLogContext( [
|
|
|
|
|
'method' => __METHOD__
|
|
|
|
|
] )
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return false; // could not get master server ID
|
|
|
|
|
}
|
2016-01-13 22:33:38 +00:00
|
|
|
|
2016-03-08 20:36:03 +00:00
|
|
|
$conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
|
|
|
|
|
$data = $this->getHeartbeatData( $conds );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2015-09-25 19:53:04 +00:00
|
|
|
|
2016-03-08 20:36:03 +00:00
|
|
|
list( $time, $nowUnix ) = $data;
|
2015-12-04 00:37:56 +00:00
|
|
|
if ( $time !== null ) {
|
|
|
|
|
// @time is in ISO format like "2015-09-25T16:48:10.000510"
|
|
|
|
|
$dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) );
|
|
|
|
|
$timeUnix = (int)$dateTime->format( 'U' ) + $dateTime->format( 'u' ) / 1e6;
|
|
|
|
|
|
|
|
|
|
return max( $nowUnix - $timeUnix, 0.0 );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2015-09-25 19:53:04 +00:00
|
|
|
|
2016-09-16 03:33:25 +00:00
|
|
|
$this->queryLogger->error(
|
2016-01-13 22:33:38 +00:00
|
|
|
"Unable to find pt-heartbeat row for {db_server}",
|
2016-02-17 09:09:32 +00:00
|
|
|
$this->getLogContext( [
|
2016-01-13 22:33:38 +00:00
|
|
|
'method' => __METHOD__
|
2016-02-17 09:09:32 +00:00
|
|
|
] )
|
2016-01-13 22:33:38 +00:00
|
|
|
);
|
|
|
|
|
|
2015-12-04 00:37:56 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function getMasterServerInfo() {
|
|
|
|
|
$cache = $this->srvCache;
|
|
|
|
|
$key = $cache->makeGlobalKey(
|
|
|
|
|
'mysql',
|
|
|
|
|
'master-info',
|
2016-09-03 14:13:47 +00:00
|
|
|
// Using one key for all cluster replica DBs is preferable
|
2015-12-04 00:37:56 +00:00
|
|
|
$this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $cache->getWithSetCallback(
|
|
|
|
|
$key,
|
|
|
|
|
$cache::TTL_INDEFINITE,
|
2016-02-10 17:07:30 +00:00
|
|
|
function () use ( $cache, $key ) {
|
2015-12-04 00:37:56 +00:00
|
|
|
// Get and leave a lock key in place for a short period
|
|
|
|
|
if ( !$cache->lock( $key, 0, 10 ) ) {
|
|
|
|
|
return false; // avoid master connection spike slams
|
|
|
|
|
}
|
|
|
|
|
|
2016-02-10 17:07:30 +00:00
|
|
|
$conn = $this->getLazyMasterHandle();
|
2015-12-04 00:37:56 +00:00
|
|
|
if ( !$conn ) {
|
|
|
|
|
return false; // something is misconfigured
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Connect to and query the master; catch errors to avoid outages
|
|
|
|
|
try {
|
|
|
|
|
$res = $conn->query( 'SELECT @@server_id AS id', __METHOD__ );
|
|
|
|
|
$row = $res ? $res->fetchObject() : false;
|
|
|
|
|
$id = $row ? (int)$row->id : 0;
|
|
|
|
|
} catch ( DBError $e ) {
|
|
|
|
|
$id = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cache the ID if it was retrieved
|
2016-02-17 09:09:32 +00:00
|
|
|
return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
|
2015-12-04 00:37:56 +00:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-03-08 20:36:03 +00:00
|
|
|
* @param array $conds WHERE clause conditions to find a row
|
|
|
|
|
* @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat
|
2015-12-04 00:37:56 +00:00
|
|
|
* @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html
|
|
|
|
|
*/
|
2016-03-08 20:36:03 +00:00
|
|
|
protected function getHeartbeatData( array $conds ) {
|
2016-12-22 23:16:07 +00:00
|
|
|
// Do not bother starting implicit transactions here
|
|
|
|
|
$this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
|
|
|
|
|
try {
|
|
|
|
|
$whereSQL = $this->makeList( $conds, self::LIST_AND );
|
|
|
|
|
// Use ORDER BY for channel based queries since that field might not be UNIQUE.
|
|
|
|
|
// Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
|
|
|
|
|
// percision field is not supported in MySQL <= 5.5.
|
|
|
|
|
$res = $this->query(
|
|
|
|
|
"SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1"
|
|
|
|
|
);
|
|
|
|
|
$row = $res ? $res->fetchObject() : false;
|
|
|
|
|
} finally {
|
|
|
|
|
$this->restoreFlags();
|
|
|
|
|
}
|
2015-09-25 19:53:04 +00:00
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
return [ $row ? $row->ts : null, microtime( true ) ];
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2016-11-03 05:24:51 +00:00
|
|
|
protected function getApproximateLagStatus() {
|
2015-12-04 00:37:56 +00:00
|
|
|
if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
|
2015-10-01 02:40:09 +00:00
|
|
|
// Disable caching since this is fast enough and we don't wan't
|
|
|
|
|
// to be *too* pessimistic by having both the cache TTL and the
|
|
|
|
|
// pt-heartbeat interval count as lag in getSessionLagStatus()
|
|
|
|
|
return parent::getApproximateLagStatus();
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-18 00:21:02 +00:00
|
|
|
$key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServer() );
|
2015-10-01 02:40:09 +00:00
|
|
|
$approxLag = $this->srvCache->get( $key );
|
|
|
|
|
if ( !$approxLag ) {
|
|
|
|
|
$approxLag = parent::getApproximateLagStatus();
|
|
|
|
|
$this->srvCache->set( $key, $approxLag, 1 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $approxLag;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-03 05:24:51 +00:00
|
|
|
public function masterPosWait( DBMasterPos $pos, $timeout ) {
|
2016-02-17 22:31:31 +00:00
|
|
|
if ( !( $pos instanceof MySQLMasterPos ) ) {
|
|
|
|
|
throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-18 23:03:27 +00:00
|
|
|
if ( $this->getLBInfo( 'is static' ) === true ) {
|
|
|
|
|
return 0; // this is a copy of a read-only dataset with no master DB
|
2016-09-03 22:06:59 +00:00
|
|
|
} elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
|
2016-05-18 23:03:27 +00:00
|
|
|
return 0; // already reached this point for sure
|
2013-05-23 00:23:21 +00:00
|
|
|
}
|
2013-05-03 01:39:16 +00:00
|
|
|
|
2016-05-21 00:26:08 +00:00
|
|
|
// Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
|
|
|
|
|
if ( $this->useGTIDs && $pos->gtids ) {
|
|
|
|
|
// Wait on the GTID set (MariaDB only)
|
2016-08-08 19:36:18 +00:00
|
|
|
$gtidArg = $this->addQuotes( implode( ',', $pos->gtids ) );
|
2016-05-21 00:26:08 +00:00
|
|
|
$res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
|
|
|
|
|
} else {
|
|
|
|
|
// Wait on the binlog coordinates
|
|
|
|
|
$encFile = $this->addQuotes( $pos->file );
|
|
|
|
|
$encPos = intval( $pos->pos );
|
|
|
|
|
$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
|
|
|
|
|
}
|
2016-02-17 22:31:31 +00:00
|
|
|
|
|
|
|
|
$row = $res ? $this->fetchRow( $res ) : false;
|
|
|
|
|
if ( !$row ) {
|
2017-03-09 05:16:00 +00:00
|
|
|
throw new DBExpectedError( $this,
|
|
|
|
|
"MASTER_POS_WAIT() or MASTER_GTID_WAIT() failed: {$this->lastError()}" );
|
2016-02-17 22:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
|
|
|
|
|
$status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
|
|
|
|
|
if ( $status === null ) {
|
|
|
|
|
// T126436: jobs programmed to wait on master positions might be referencing binlogs
|
|
|
|
|
// with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try
|
2016-09-03 14:13:47 +00:00
|
|
|
// to detect this and treat the replica DB as having reached the position; a proper master
|
2016-02-17 22:31:31 +00:00
|
|
|
// switchover already requires that the new master be caught up before the switch.
|
2016-09-24 04:17:32 +00:00
|
|
|
$replicationPos = $this->getReplicaPos();
|
2016-09-03 22:06:59 +00:00
|
|
|
if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) {
|
|
|
|
|
$this->lastKnownReplicaPos = $replicationPos;
|
2016-02-17 22:31:31 +00:00
|
|
|
$status = 0;
|
2013-05-23 00:23:21 +00:00
|
|
|
}
|
2016-02-17 22:31:31 +00:00
|
|
|
} elseif ( $status >= 0 ) {
|
|
|
|
|
// Remember that this position was reached to save queries next time
|
2016-09-03 22:06:59 +00:00
|
|
|
$this->lastKnownReplicaPos = $pos;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2013-05-23 00:23:21 +00:00
|
|
|
|
|
|
|
|
return $status;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the position of the master from SHOW SLAVE STATUS
|
|
|
|
|
*
|
|
|
|
|
* @return MySQLMasterPos|bool
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function getReplicaPos() {
|
2016-05-21 00:26:08 +00:00
|
|
|
$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
|
2013-05-03 01:39:16 +00:00
|
|
|
$row = $this->fetchObject( $res );
|
|
|
|
|
|
|
|
|
|
if ( $row ) {
|
2013-11-20 10:13:51 +00:00
|
|
|
$pos = isset( $row->Exec_master_log_pos )
|
|
|
|
|
? $row->Exec_master_log_pos
|
|
|
|
|
: $row->Exec_Master_Log_Pos;
|
2016-05-21 00:26:08 +00:00
|
|
|
// Also fetch the last-applied GTID set (MariaDB)
|
|
|
|
|
if ( $this->useGTIDs ) {
|
|
|
|
|
$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ );
|
|
|
|
|
$gtidRow = $this->fetchObject( $res );
|
|
|
|
|
$gtidSet = $gtidRow ? $gtidRow->Value : '';
|
|
|
|
|
} else {
|
|
|
|
|
$gtidSet = '';
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2016-05-21 00:26:08 +00:00
|
|
|
return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet );
|
2013-05-03 01:39:16 +00:00
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the position of the master from SHOW MASTER STATUS
|
|
|
|
|
*
|
|
|
|
|
* @return MySQLMasterPos|bool
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function getMasterPos() {
|
2016-05-21 00:26:08 +00:00
|
|
|
$res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
|
2013-05-03 01:39:16 +00:00
|
|
|
$row = $this->fetchObject( $res );
|
|
|
|
|
|
|
|
|
|
if ( $row ) {
|
2016-05-21 00:26:08 +00:00
|
|
|
// Also fetch the last-written GTID set (MariaDB)
|
|
|
|
|
if ( $this->useGTIDs ) {
|
|
|
|
|
$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ );
|
|
|
|
|
$gtidRow = $this->fetchObject( $res );
|
|
|
|
|
$gtidSet = $gtidRow ? $gtidRow->Value : '';
|
|
|
|
|
} else {
|
|
|
|
|
$gtidSet = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new MySQLMasterPos( $row->File, $row->Position, $gtidSet );
|
2013-05-03 01:39:16 +00:00
|
|
|
} else {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-22 05:15:30 +00:00
|
|
|
public function serverIsReadOnly() {
|
|
|
|
|
$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__ );
|
|
|
|
|
$row = $this->fetchObject( $res );
|
|
|
|
|
|
|
|
|
|
return $row ? ( strtolower( $row->Value ) === 'on' ) : false;
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $index
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
function useIndexClause( $index ) {
|
|
|
|
|
return "FORCE INDEX (" . $this->indexName( $index ) . ")";
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-04 10:56:40 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $index
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
function ignoreIndexClause( $index ) {
|
|
|
|
|
return "IGNORE INDEX (" . $this->indexName( $index ) . ")";
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
function lowPriorityOption() {
|
|
|
|
|
return 'LOW_PRIORITY';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function getSoftwareLink() {
|
2014-08-31 10:43:50 +00:00
|
|
|
// MariaDB includes its name in its version string; this is how MariaDB's version of
|
|
|
|
|
// the mysql command-line client identifies MariaDB servers (see mariadb_connection()
|
|
|
|
|
// in libmysql/libmysql.c).
|
2014-01-09 01:50:21 +00:00
|
|
|
$version = $this->getServerVersion();
|
2014-01-16 07:18:36 +00:00
|
|
|
if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) {
|
2014-01-09 01:50:21 +00:00
|
|
|
return '[{{int:version-db-mariadb-url}} MariaDB]';
|
|
|
|
|
}
|
2014-01-16 07:18:36 +00:00
|
|
|
|
|
|
|
|
// Percona Server's version suffix is not very distinctive, and @@version_comment
|
|
|
|
|
// doesn't give the necessary info for source builds, so assume the server is MySQL.
|
|
|
|
|
// (Even Percona's version of mysql doesn't try to make the distinction.)
|
|
|
|
|
return '[{{int:version-db-mysql-url}} MySQL]';
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2014-08-31 10:43:50 +00:00
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function getServerVersion() {
|
|
|
|
|
// Not using mysql_get_server_info() or similar for consistency: in the handshake,
|
|
|
|
|
// MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
|
|
|
|
|
// it off (see RPL_VERSION_HACK in include/mysql_com.h).
|
|
|
|
|
if ( $this->serverVersion === null ) {
|
|
|
|
|
$this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ );
|
|
|
|
|
}
|
|
|
|
|
return $this->serverVersion;
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param array $options
|
2013-05-03 01:39:16 +00:00
|
|
|
*/
|
|
|
|
|
public function setSessionOptions( array $options ) {
|
|
|
|
|
if ( isset( $options['connTimeout'] ) ) {
|
|
|
|
|
$timeout = (int)$options['connTimeout'];
|
|
|
|
|
$this->query( "SET net_read_timeout=$timeout" );
|
|
|
|
|
$this->query( "SET net_write_timeout=$timeout" );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $sql
|
|
|
|
|
* @param string $newLine
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2013-05-03 01:39:16 +00:00
|
|
|
public function streamStatementEnd( &$sql, &$newLine ) {
|
|
|
|
|
if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
|
|
|
|
|
preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
|
|
|
|
|
$this->delimiter = $m[1];
|
|
|
|
|
$newLine = '';
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return parent::streamStatementEnd( $sql, $newLine );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check to see if a named lock is available. This is non-blocking.
|
|
|
|
|
*
|
2014-07-24 17:42:45 +00:00
|
|
|
* @param string $lockName Name of lock to poll
|
|
|
|
|
* @param string $method Name of method calling us
|
2013-12-27 01:54:51 +00:00
|
|
|
* @return bool
|
2013-05-03 01:39:16 +00:00
|
|
|
* @since 1.20
|
|
|
|
|
*/
|
|
|
|
|
public function lockIsFree( $lockName, $method ) {
|
2016-09-21 00:25:58 +00:00
|
|
|
$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
|
|
|
|
|
$result = $this->query( "SELECT IS_FREE_LOCK($encName) AS lockstatus", $method );
|
2013-05-03 01:39:16 +00:00
|
|
|
$row = $this->fetchObject( $result );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return ( $row->lockstatus == 1 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $lockName
|
|
|
|
|
* @param string $method
|
|
|
|
|
* @param int $timeout
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function lock( $lockName, $method, $timeout = 5 ) {
|
2016-09-21 00:25:58 +00:00
|
|
|
$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
|
|
|
|
|
$result = $this->query( "SELECT GET_LOCK($encName, $timeout) AS lockstatus", $method );
|
2013-05-03 01:39:16 +00:00
|
|
|
$row = $this->fetchObject( $result );
|
|
|
|
|
|
|
|
|
|
if ( $row->lockstatus == 1 ) {
|
2016-01-30 17:17:17 +00:00
|
|
|
parent::lock( $lockName, $method, $timeout ); // record
|
2013-05-03 01:39:16 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
2016-01-30 17:17:17 +00:00
|
|
|
|
2016-09-20 23:34:09 +00:00
|
|
|
$this->queryLogger->warning( __METHOD__ . " failed to acquire lock '$lockName'\n" );
|
2016-01-30 17:17:17 +00:00
|
|
|
|
|
|
|
|
return false;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2013-11-20 10:13:51 +00:00
|
|
|
* FROM MYSQL DOCS:
|
2016-10-13 05:34:26 +00:00
|
|
|
* https://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $lockName
|
|
|
|
|
* @param string $method
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function unlock( $lockName, $method ) {
|
2016-09-21 00:25:58 +00:00
|
|
|
$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
|
|
|
|
|
$result = $this->query( "SELECT RELEASE_LOCK($encName) as lockstatus", $method );
|
2013-05-03 01:39:16 +00:00
|
|
|
$row = $this->fetchObject( $result );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2016-01-30 17:17:17 +00:00
|
|
|
if ( $row->lockstatus == 1 ) {
|
|
|
|
|
parent::unlock( $lockName, $method ); // record
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-20 23:34:09 +00:00
|
|
|
$this->queryLogger->warning( __METHOD__ . " failed to release lock '$lockName'\n" );
|
2016-01-30 17:17:17 +00:00
|
|
|
|
|
|
|
|
return false;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2015-12-16 05:34:52 +00:00
|
|
|
private function makeLockName( $lockName ) {
|
2016-10-13 05:34:26 +00:00
|
|
|
// https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
|
2015-12-16 05:34:52 +00:00
|
|
|
// Newer version enforce a 64 char length limit.
|
|
|
|
|
return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-22 06:13:31 +00:00
|
|
|
public function namedLocksEnqueue() {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-30 21:56:22 +00:00
|
|
|
public function tableLocksHaveTransactionScope() {
|
|
|
|
|
return false; // tied to TCP connection
|
|
|
|
|
}
|
2013-05-03 01:39:16 +00:00
|
|
|
|
2017-03-30 21:56:22 +00:00
|
|
|
protected function doLockTables( array $read, array $write, $method ) {
|
|
|
|
|
$items = [];
|
2013-05-03 01:39:16 +00:00
|
|
|
foreach ( $write as $table ) {
|
2017-03-30 21:56:22 +00:00
|
|
|
$items[] = $this->tableName( $table ) . ' WRITE';
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
foreach ( $read as $table ) {
|
|
|
|
|
$items[] = $this->tableName( $table ) . ' READ';
|
|
|
|
|
}
|
2017-03-30 21:56:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
$sql = "LOCK TABLES " . implode( ',', $items );
|
|
|
|
|
$this->query( $sql, $method );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-30 21:56:22 +00:00
|
|
|
protected function doUnlockTables( $method ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
$this->query( "UNLOCK TABLES", $method );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param bool $value
|
|
|
|
|
*/
|
|
|
|
|
public function setBigSelects( $value = true ) {
|
|
|
|
|
if ( $value === 'default' ) {
|
|
|
|
|
if ( $this->mDefaultBigSelects === null ) {
|
|
|
|
|
# Function hasn't been called before so it must already be set to the default
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
$value = $this->mDefaultBigSelects;
|
|
|
|
|
}
|
|
|
|
|
} elseif ( $this->mDefaultBigSelects === null ) {
|
2015-09-27 08:15:12 +00:00
|
|
|
$this->mDefaultBigSelects =
|
|
|
|
|
(bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
$encValue = $value ? '1' : '0';
|
|
|
|
|
$this->query( "SET sql_big_selects=$encValue", __METHOD__ );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* DELETE where the condition is a join. MySql uses multi-table deletes.
|
2014-04-19 11:55:27 +00:00
|
|
|
* @param string $delTable
|
|
|
|
|
* @param string $joinTable
|
|
|
|
|
* @param string $delVar
|
|
|
|
|
* @param string $joinVar
|
|
|
|
|
* @param array|string $conds
|
|
|
|
|
* @param bool|string $fname
|
2013-05-03 01:39:16 +00:00
|
|
|
* @throws DBUnexpectedError
|
|
|
|
|
* @return bool|ResultWrapper
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function deleteJoin(
|
|
|
|
|
$delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
|
|
|
|
|
) {
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( !$conds ) {
|
2016-09-15 02:04:21 +00:00
|
|
|
throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$delTable = $this->tableName( $delTable );
|
|
|
|
|
$joinTable = $this->tableName( $joinTable );
|
|
|
|
|
$sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
|
|
|
|
|
|
|
|
|
|
if ( $conds != '*' ) {
|
2016-09-19 07:28:17 +00:00
|
|
|
$sql .= ' AND ' . $this->makeList( $conds, self::LIST_AND );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->query( $sql, $fname );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $table
|
|
|
|
|
* @param array $rows
|
|
|
|
|
* @param array $uniqueIndexes
|
|
|
|
|
* @param array $set
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2014-01-06 18:41:54 +00:00
|
|
|
public function upsert( $table, array $rows, array $uniqueIndexes,
|
|
|
|
|
array $set, $fname = __METHOD__
|
2013-05-03 01:39:16 +00:00
|
|
|
) {
|
|
|
|
|
if ( !count( $rows ) ) {
|
|
|
|
|
return true; // nothing to do
|
|
|
|
|
}
|
2014-02-03 20:42:04 +00:00
|
|
|
|
|
|
|
|
if ( !is_array( reset( $rows ) ) ) {
|
2016-02-17 09:09:32 +00:00
|
|
|
$rows = [ $rows ];
|
2014-02-03 20:42:04 +00:00
|
|
|
}
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
$table = $this->tableName( $table );
|
|
|
|
|
$columns = array_keys( $rows[0] );
|
|
|
|
|
|
|
|
|
|
$sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
|
2016-02-17 09:09:32 +00:00
|
|
|
$rowTuples = [];
|
2013-05-03 01:39:16 +00:00
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
|
$rowTuples[] = '(' . $this->makeList( $row ) . ')';
|
|
|
|
|
}
|
|
|
|
|
$sql .= implode( ',', $rowTuples );
|
2016-09-19 07:28:17 +00:00
|
|
|
$sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, self::LIST_SET );
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
return (bool)$this->query( $sql, $fname );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines how long the server has been up
|
|
|
|
|
*
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function getServerUptime() {
|
2013-05-03 01:39:16 +00:00
|
|
|
$vars = $this->getMysqlStatus( 'Uptime' );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return (int)$vars['Uptime'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines if the last failure was due to a deadlock
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function wasDeadlock() {
|
2013-05-03 01:39:16 +00:00
|
|
|
return $this->lastErrno() == 1213;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines if the last failure was due to a lock timeout
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function wasLockTimeout() {
|
2013-05-03 01:39:16 +00:00
|
|
|
return $this->lastErrno() == 1205;
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-03 05:24:51 +00:00
|
|
|
public function wasErrorReissuable() {
|
2013-05-03 01:39:16 +00:00
|
|
|
return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines if the last failure was due to the database being read-only.
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function wasReadOnlyError() {
|
2013-05-03 01:39:16 +00:00
|
|
|
return $this->lastErrno() == 1223 ||
|
|
|
|
|
( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-03 05:24:51 +00:00
|
|
|
public function wasConnectionError( $errno ) {
|
2015-08-06 22:09:54 +00:00
|
|
|
return $errno == 2013 || $errno == 2006;
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $oldName
|
|
|
|
|
* @param string $newName
|
|
|
|
|
* @param bool $temporary
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @return bool
|
2013-05-03 01:39:16 +00:00
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function duplicateTableStructure(
|
|
|
|
|
$oldName, $newName, $temporary = false, $fname = __METHOD__
|
|
|
|
|
) {
|
2013-05-03 01:39:16 +00:00
|
|
|
$tmp = $temporary ? 'TEMPORARY ' : '';
|
|
|
|
|
$newName = $this->addIdentifierQuotes( $newName );
|
|
|
|
|
$oldName = $this->addIdentifierQuotes( $oldName );
|
|
|
|
|
$query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
|
2014-01-06 18:32:50 +00:00
|
|
|
|
|
|
|
|
return $this->query( $query, $fname );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* List all tables on the database
|
|
|
|
|
*
|
|
|
|
|
* @param string $prefix Only show tables with this prefix, e.g. mw_
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $fname Calling function name
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return array
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function listTables( $prefix = null, $fname = __METHOD__ ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
$result = $this->query( "SHOW TABLES", $fname );
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$endArray = [];
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
foreach ( $result as $table ) {
|
|
|
|
|
$vars = get_object_vars( $table );
|
|
|
|
|
$table = array_pop( $vars );
|
|
|
|
|
|
|
|
|
|
if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
|
|
|
|
|
$endArray[] = $table;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $endArray;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2014-04-19 11:55:27 +00:00
|
|
|
* @param string $tableName
|
|
|
|
|
* @param string $fName
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return bool|ResultWrapper
|
|
|
|
|
*/
|
|
|
|
|
public function dropTable( $tableName, $fName = __METHOD__ ) {
|
|
|
|
|
if ( !$this->tableExists( $tableName, $fName ) ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get status information from SHOW STATUS in an associative array
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $which
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return array
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
private function getMysqlStatus( $which = "%" ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
$res = $this->query( "SHOW STATUS LIKE '{$which}'" );
|
2016-02-17 09:09:32 +00:00
|
|
|
$status = [];
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
$status[$row->Variable_name] = $row->Value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-24 15:14:40 +00:00
|
|
|
/**
|
|
|
|
|
* Lists VIEWs in the database
|
|
|
|
|
*
|
2013-11-20 06:58:22 +00:00
|
|
|
* @param string $prefix Only show VIEWs with this prefix, eg.
|
2013-05-24 15:14:40 +00:00
|
|
|
* unit_test_, or $wgDBprefix. Default: null, would return all views.
|
2013-11-20 06:58:22 +00:00
|
|
|
* @param string $fname Name of calling function
|
2013-05-24 15:14:40 +00:00
|
|
|
* @return array
|
|
|
|
|
* @since 1.22
|
|
|
|
|
*/
|
|
|
|
|
public function listViews( $prefix = null, $fname = __METHOD__ ) {
|
2016-09-23 03:11:18 +00:00
|
|
|
// The name of the column containing the name of the VIEW
|
|
|
|
|
$propertyName = 'Tables_in_' . $this->mDBname;
|
2013-05-24 15:14:40 +00:00
|
|
|
|
2016-09-23 03:11:18 +00:00
|
|
|
// Query for the VIEWS
|
|
|
|
|
$res = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
|
|
|
|
|
$allViews = [];
|
|
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
array_push( $allViews, $row->$propertyName );
|
2013-05-24 15:14:40 +00:00
|
|
|
}
|
|
|
|
|
|
2013-11-19 18:03:54 +00:00
|
|
|
if ( is_null( $prefix ) || $prefix === '' ) {
|
2016-09-23 03:11:18 +00:00
|
|
|
return $allViews;
|
2013-05-24 15:14:40 +00:00
|
|
|
}
|
|
|
|
|
|
2016-02-17 09:09:32 +00:00
|
|
|
$filteredViews = [];
|
2016-09-23 03:11:18 +00:00
|
|
|
foreach ( $allViews as $viewName ) {
|
2013-05-24 15:14:40 +00:00
|
|
|
// Does the name of this VIEW start with the table-prefix?
|
|
|
|
|
if ( strpos( $viewName, $prefix ) === 0 ) {
|
|
|
|
|
array_push( $filteredViews, $viewName );
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2013-05-24 15:14:40 +00:00
|
|
|
return $filteredViews;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Differentiates between a TABLE and a VIEW.
|
|
|
|
|
*
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string $name Name of the TABLE/VIEW to test
|
|
|
|
|
* @param string $prefix
|
2013-05-24 15:14:40 +00:00
|
|
|
* @return bool
|
|
|
|
|
* @since 1.22
|
|
|
|
|
*/
|
|
|
|
|
public function isView( $name, $prefix = null ) {
|
|
|
|
|
return in_array( $name, $this->listViews( $prefix ) );
|
|
|
|
|
}
|
2017-03-30 11:29:35 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Allows for index remapping in queries where this is not consistent across DBMS
|
|
|
|
|
*
|
|
|
|
|
* @param string $index
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function indexName( $index ) {
|
2017-03-30 04:17:47 +00:00
|
|
|
/**
|
|
|
|
|
* When SQLite indexes were introduced in r45764, it was noted that
|
|
|
|
|
* SQLite requires index names to be unique within the whole database,
|
|
|
|
|
* not just within a schema. As discussed in CR r45819, to avoid the
|
|
|
|
|
* need for a schema change on existing installations, the indexes
|
|
|
|
|
* were implicitly mapped from the new names to the old names.
|
|
|
|
|
*
|
|
|
|
|
* This mapping can be removed if DB patches are introduced to alter
|
|
|
|
|
* the relevant tables in existing installations. Note that because
|
|
|
|
|
* this index mapping applies to table creation, even new installations
|
|
|
|
|
* of MySQL have the old names (except for installations created during
|
|
|
|
|
* a period where this mapping was inappropriately removed, see
|
|
|
|
|
* T154872).
|
|
|
|
|
*/
|
2017-03-30 11:29:35 +00:00
|
|
|
$renamed = [
|
|
|
|
|
'ar_usertext_timestamp' => 'usertext_timestamp',
|
|
|
|
|
'un_user_id' => 'user_id',
|
|
|
|
|
'un_user_ip' => 'user_ip',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ( isset( $renamed[$index] ) ) {
|
|
|
|
|
return $renamed[$index];
|
|
|
|
|
} else {
|
|
|
|
|
return $index;
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2017-02-07 04:49:57 +00:00
|
|
|
|
|
|
|
|
class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' );
|