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;
|
|
|
|
|
|
2020-01-10 00:00:51 +00:00
|
|
|
use InvalidArgumentException;
|
2018-12-21 18:20:58 +00:00
|
|
|
use RuntimeException;
|
2017-02-07 04:49:57 +00:00
|
|
|
use stdClass;
|
2020-01-10 00:00:51 +00:00
|
|
|
use Wikimedia\AtEase\AtEase;
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Database abstraction object for MySQL.
|
|
|
|
|
* Defines methods independent on used MySQL extension.
|
|
|
|
|
*
|
2021-07-15 00:46:59 +00:00
|
|
|
* TODO: This could probably be merged with DatabaseMysqli.
|
|
|
|
|
* The split was created to support a transition from the old "mysql" extension
|
|
|
|
|
* to mysqli, and there may be an argument for retaining it in order to support
|
|
|
|
|
* some future transition to something else, but it's complexity and YAGNI.
|
|
|
|
|
*
|
2013-05-03 01:39:16 +00:00
|
|
|
* @ingroup Database
|
|
|
|
|
* @since 1.22
|
|
|
|
|
* @see Database
|
|
|
|
|
*/
|
2016-09-28 23:08:15 +00:00
|
|
|
abstract class DatabaseMysqlBase extends Database {
|
2021-05-14 19:48:07 +00:00
|
|
|
/** @var MySQLPrimaryPos */
|
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 */
|
2017-08-24 23:54:19 +00:00
|
|
|
protected $sslCAFile;
|
|
|
|
|
/** @var string|null */
|
2016-08-22 17:37:31 +00:00
|
|
|
protected $sslCAPath;
|
2021-03-10 01:38:23 +00:00
|
|
|
/**
|
|
|
|
|
* Open SSL cipher list string
|
|
|
|
|
* @see https://www.openssl.org/docs/man1.0.2/man1/ciphers.html
|
|
|
|
|
* @var string|null
|
|
|
|
|
*/
|
2016-08-22 17:37:31 +00:00
|
|
|
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;
|
2018-02-13 06:58:57 +00:00
|
|
|
/** @var bool|null */
|
2021-03-10 14:07:38 +00:00
|
|
|
protected $defaultBigSelects;
|
2016-09-16 03:33:25 +00:00
|
|
|
|
2018-01-26 06:02:22 +00:00
|
|
|
/** @var bool|null */
|
2021-03-10 14:07:38 +00:00
|
|
|
private $insertSelectIsSafe;
|
2018-02-28 23:33:03 +00:00
|
|
|
/** @var stdClass|null */
|
2021-03-10 14:07:38 +00:00
|
|
|
private $replicationInfoRow;
|
2014-08-31 10:43:50 +00:00
|
|
|
|
2018-02-07 10:15:54 +00:00
|
|
|
// Cache getServerId() for 24 hours
|
2020-05-16 01:32:53 +00:00
|
|
|
private const SERVER_ID_CACHE_TTL = 86400;
|
2018-02-07 10:15:54 +00:00
|
|
|
|
2018-04-02 19:56:11 +00:00
|
|
|
/** @var float Warn if lag estimates are made for transactions older than this many seconds */
|
2020-05-16 01:32:53 +00:00
|
|
|
private const LAG_STALE_WARN_THRESHOLD = 0.100;
|
2018-04-02 19:56:11 +00:00
|
|
|
|
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
|
2021-09-01 21:04:40 +00:00
|
|
|
* ID of this server's primary DB will be used. Set the "conds" field to
|
2016-05-21 00:26:08 +00:00
|
|
|
* override the query conditions, e.g. ['shard' => 's1'].
|
|
|
|
|
* - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
|
2018-01-26 06:02:22 +00:00
|
|
|
* - insertSelectIsSafe : force that native INSERT SELECT is or is not safe [default: null]
|
2016-08-22 17:37:31 +00:00
|
|
|
* - sslKeyPath : path to key file [default: null]
|
|
|
|
|
* - sslCertPath : path to certificate file [default: null]
|
2017-08-24 23:54:19 +00:00
|
|
|
* - sslCAFile: path to a single certificate authority PEM file [default: null]
|
|
|
|
|
* - sslCAPath : parth to certificate authority PEM directory [default: null]
|
2016-08-22 17:37:31 +00:00
|
|
|
* - sslCiphers : array list of allowable ciphers [default: null]
|
2015-09-25 19:53:04 +00:00
|
|
|
* @param array $params
|
|
|
|
|
*/
|
2019-07-06 19:36:54 +00:00
|
|
|
public function __construct( array $params ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
$this->lagDetectionMethod = $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master';
|
|
|
|
|
$this->lagDetectionOptions = $params['lagDetectionOptions'] ?? [];
|
2016-05-21 00:26:08 +00:00
|
|
|
$this->useGTIDs = !empty( $params['useGTIDs' ] );
|
2017-08-24 23:54:19 +00:00
|
|
|
foreach ( [ 'KeyPath', 'CertPath', 'CAFile', 'CAPath', 'Ciphers' ] as $name ) {
|
2016-08-22 17:37:31 +00:00
|
|
|
$var = "ssl{$name}";
|
|
|
|
|
if ( isset( $params[$var] ) ) {
|
|
|
|
|
$this->$var = $params[$var];
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-04-01 21:39:36 +00:00
|
|
|
$this->sqlMode = $params['sqlMode'] ?? null;
|
2016-09-16 03:33:25 +00:00
|
|
|
$this->utf8Mode = !empty( $params['utf8Mode'] );
|
2018-01-26 06:02:22 +00:00
|
|
|
$this->insertSelectIsSafe = isset( $params['insertSelectIsSafe'] )
|
|
|
|
|
? (bool)$params['insertSelectIsSafe'] : null;
|
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';
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-10 01:38:23 +00:00
|
|
|
protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
|
2020-06-07 19:06:40 +00:00
|
|
|
$this->close( __METHOD__ );
|
2015-06-25 00:24:00 +00:00
|
|
|
|
2019-06-13 12:46:03 +00:00
|
|
|
if ( $schema !== null ) {
|
2019-07-11 02:35:46 +00:00
|
|
|
throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
|
2019-06-13 12:46:03 +00:00
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
$this->installErrorHandler();
|
|
|
|
|
try {
|
2021-03-10 01:38:23 +00:00
|
|
|
$this->conn = $this->mysqlConnect( $server, $user, $password, $db );
|
2020-03-31 01:11:44 +00:00
|
|
|
} catch ( RuntimeException $e ) {
|
2013-11-15 15:13:19 +00:00
|
|
|
$this->restoreErrorHandler();
|
2019-07-11 02:35:46 +00:00
|
|
|
throw $this->newExceptionAfterConnectError( $e->getMessage() );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
$error = $this->restoreErrorHandler();
|
|
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( !$this->conn ) {
|
2019-07-11 02:35:46 +00:00
|
|
|
throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2019-06-13 12:46:03 +00:00
|
|
|
try {
|
|
|
|
|
$this->currentDomain = new DatabaseDomain(
|
2021-03-10 01:38:23 +00:00
|
|
|
strlen( $db ) ? $db : null,
|
2019-06-13 12:46:03 +00:00
|
|
|
null,
|
|
|
|
|
$tablePrefix
|
2018-04-19 23:21:51 +00:00
|
|
|
);
|
2021-11-22 15:41:36 +00:00
|
|
|
// Abstract over any excessive MySQL defaults
|
2019-06-13 12:46:03 +00:00
|
|
|
$set = [ 'group_concat_max_len = 262144' ];
|
|
|
|
|
// Set SQL mode, default is turning them all off, can be overridden or skipped with null
|
|
|
|
|
if ( is_string( $this->sqlMode ) ) {
|
|
|
|
|
$set[] = 'sql_mode = ' . $this->addQuotes( $this->sqlMode );
|
|
|
|
|
}
|
|
|
|
|
// Set any custom settings defined by site config
|
2021-01-30 22:06:09 +00:00
|
|
|
// https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html
|
2019-06-13 12:46:03 +00:00
|
|
|
foreach ( $this->connectionVariables 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-04-24 18:07:02 +00:00
|
|
|
}
|
2015-01-14 00:57:52 +00:00
|
|
|
|
2020-06-15 10:46:22 +00:00
|
|
|
// @phan-suppress-next-next-line PhanRedundantCondition
|
|
|
|
|
// If kept for safety and to avoid broken query
|
2019-06-13 12:46:03 +00:00
|
|
|
if ( $set ) {
|
|
|
|
|
$this->query(
|
|
|
|
|
'SET ' . implode( ', ', $set ),
|
|
|
|
|
__METHOD__,
|
2021-12-14 01:00:28 +00:00
|
|
|
self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX
|
2014-06-23 22:25:55 +00:00
|
|
|
);
|
2013-11-21 23:54:02 +00:00
|
|
|
}
|
2020-03-31 01:11:44 +00:00
|
|
|
} catch ( RuntimeException $e ) {
|
2019-07-11 02:35:46 +00:00
|
|
|
throw $this->newExceptionAfterConnectError( $e->getMessage() );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-22 01:12:44 +00:00
|
|
|
protected function doSelectDomain( DatabaseDomain $domain ) {
|
|
|
|
|
if ( $domain->getSchema() !== null ) {
|
2019-07-06 19:36:54 +00:00
|
|
|
throw new DBExpectedError(
|
|
|
|
|
$this,
|
|
|
|
|
__CLASS__ . ": domain '{$domain->getId()}' has a schema component"
|
|
|
|
|
);
|
2019-03-22 01:12:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$database = $domain->getDatabase();
|
|
|
|
|
// A null database means "don't care" so leave it as is and update the table prefix
|
|
|
|
|
if ( $database === null ) {
|
|
|
|
|
$this->currentDomain = new DatabaseDomain(
|
|
|
|
|
$this->currentDomain->getDatabase(),
|
|
|
|
|
null,
|
|
|
|
|
$domain->getTablePrefix()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $database !== $this->getDBname() ) {
|
|
|
|
|
$sql = 'USE ' . $this->addIdentifierQuotes( $database );
|
2019-05-22 20:31:59 +00:00
|
|
|
list( $res, $err, $errno ) =
|
|
|
|
|
$this->executeQuery( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
|
|
|
|
|
|
|
|
|
|
if ( $res === false ) {
|
|
|
|
|
$this->reportQueryError( $err, $errno, $sql, __METHOD__ );
|
|
|
|
|
return false; // unreachable
|
2019-03-22 01:12:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update that domain fields on success (no exception thrown)
|
|
|
|
|
$this->currentDomain = $domain;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
|
|
|
|
* Open a connection to a MySQL server
|
|
|
|
|
*
|
2021-03-10 01:38:23 +00:00
|
|
|
* @param string|null $server
|
|
|
|
|
* @param string|null $user
|
|
|
|
|
* @param string|null $password
|
|
|
|
|
* @param string|null $db
|
2019-07-11 02:35:46 +00:00
|
|
|
* @return mixed|null Driver connection handle
|
2013-05-03 01:39:16 +00:00
|
|
|
* @throws DBConnectionError
|
|
|
|
|
*/
|
2021-03-10 01:38:23 +00:00
|
|
|
abstract protected function mysqlConnect( $server, $user, $password, $db );
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function lastError() {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->conn ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
# Even if it's non-zero, it can still be invalid
|
2019-07-11 09:32:27 +00:00
|
|
|
AtEase::suppressWarnings();
|
2018-02-13 06:58:57 +00:00
|
|
|
$error = $this->mysqlError( $this->conn );
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( !$error ) {
|
|
|
|
|
$error = $this->mysqlError();
|
|
|
|
|
}
|
2019-07-11 09:32:27 +00:00
|
|
|
AtEase::restoreWarnings();
|
2013-05-03 01:39:16 +00:00
|
|
|
} else {
|
|
|
|
|
$error = $this->mysqlError();
|
|
|
|
|
}
|
|
|
|
|
if ( $error ) {
|
2021-03-10 01:38:23 +00:00
|
|
|
$error .= ' (' . $this->getServerName() . ')';
|
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 $error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the text of the error message from previous MySQL operation
|
|
|
|
|
*
|
2018-06-26 21:14:43 +00:00
|
|
|
* @param resource|null $conn Raw connection
|
2013-05-03 01:39:16 +00:00
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function mysqlError( $conn = null );
|
|
|
|
|
|
2017-09-18 11:38:59 +00:00
|
|
|
protected function wasQueryTimeout( $error, $errno ) {
|
2019-03-21 21:22:34 +00:00
|
|
|
// https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html
|
|
|
|
|
// https://phabricator.wikimedia.org/T170638
|
|
|
|
|
return in_array( $errno, [ 2062, 3024 ] );
|
2017-09-18 11:38:59 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-28 23:33:03 +00:00
|
|
|
protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
|
|
|
|
|
$row = $this->getReplicationSafetyInfo();
|
|
|
|
|
// For row-based-replication, the resulting changes will be relayed, not the query
|
|
|
|
|
if ( $row->binlog_format === 'ROW' ) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// LIMIT requires ORDER BY on a unique key or it is non-deterministic
|
|
|
|
|
if ( isset( $selectOptions['LIMIT'] ) ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// In MySQL, an INSERT SELECT is only replication safe with row-based
|
|
|
|
|
// replication or if innodb_autoinc_lock_mode is 0. When those
|
|
|
|
|
// conditions aren't met, use non-native mode.
|
|
|
|
|
// While we could try to determine if the insert is safe anyway by
|
|
|
|
|
// checking if the target table has an auto-increment column that
|
|
|
|
|
// isn't set in $varMap, that seems unlikely to be worth the extra
|
|
|
|
|
// complexity.
|
2018-03-02 04:41:32 +00:00
|
|
|
return (
|
|
|
|
|
in_array( 'NO_AUTO_COLUMNS', $insertOptions ) ||
|
|
|
|
|
(int)$row->innodb_autoinc_lock_mode === 0
|
|
|
|
|
);
|
2018-02-28 23:33:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return stdClass Process cached row
|
|
|
|
|
*/
|
2018-03-02 04:41:32 +00:00
|
|
|
protected function getReplicationSafetyInfo() {
|
2018-02-28 23:33:03 +00:00
|
|
|
if ( $this->replicationInfoRow === null ) {
|
|
|
|
|
$this->replicationInfoRow = $this->selectRow(
|
2018-01-26 06:02:22 +00:00
|
|
|
false,
|
|
|
|
|
[
|
|
|
|
|
'innodb_autoinc_lock_mode' => '@@innodb_autoinc_lock_mode',
|
|
|
|
|
'binlog_format' => '@@binlog_format',
|
|
|
|
|
],
|
|
|
|
|
[],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-28 23:33:03 +00:00
|
|
|
return $this->replicationInfoRow;
|
2018-01-26 06:02:22 +00:00
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
/**
|
|
|
|
|
* Estimate rows in dataset
|
|
|
|
|
* Returns estimated count, based on EXPLAIN output
|
|
|
|
|
* Takes same arguments as Database::select()
|
|
|
|
|
*
|
2021-03-10 14:07:38 +00:00
|
|
|
* @param string|array $tables
|
2018-02-15 03:46:04 +00:00
|
|
|
* @param string|array $var
|
2013-12-27 01:54:51 +00:00
|
|
|
* @param string|array $conds
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @param string|array $options
|
2018-03-12 16:15:14 +00:00
|
|
|
* @param array $join_conds
|
2013-12-27 01:54:51 +00:00
|
|
|
* @return bool|int
|
2013-05-03 01:39:16 +00:00
|
|
|
*/
|
2021-03-10 14:07:38 +00:00
|
|
|
public function estimateRowCount(
|
|
|
|
|
$tables,
|
|
|
|
|
$var = '*',
|
|
|
|
|
$conds = '',
|
|
|
|
|
$fname = __METHOD__,
|
|
|
|
|
$options = [],
|
|
|
|
|
$join_conds = []
|
2013-11-20 10:13:51 +00:00
|
|
|
) {
|
2018-02-15 03:46:04 +00:00
|
|
|
$conds = $this->normalizeConditions( $conds, $fname );
|
|
|
|
|
$column = $this->extractSingleFieldFromList( $var );
|
|
|
|
|
if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
|
|
|
|
|
$conds[] = "$column IS NOT NULL";
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-03 01:39:16 +00:00
|
|
|
$options['EXPLAIN'] = true;
|
2021-03-10 14:07:38 +00:00
|
|
|
$res = $this->select( $tables, $var, $conds, $fname, $options, $join_conds );
|
2013-05-03 01:39:16 +00:00
|
|
|
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
|
|
|
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( isset( $this->sessionTempTables[$tableName] ) ) {
|
2017-06-27 18:31:35 +00:00
|
|
|
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 );
|
2020-03-20 13:16:46 +00:00
|
|
|
$sql = "SHOW TABLES FROM $encDatabase LIKE '$encLike'";
|
2017-06-19 17:20:43 +00:00
|
|
|
} else {
|
2020-03-20 13:16:46 +00:00
|
|
|
$sql = "SHOW TABLES LIKE '$encLike'";
|
2017-06-19 17:20:43 +00:00
|
|
|
}
|
|
|
|
|
|
2020-03-20 13:16:46 +00:00
|
|
|
$res = $this->query(
|
|
|
|
|
$sql,
|
|
|
|
|
$fname,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $res->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 ) {
|
2020-03-20 13:16:46 +00:00
|
|
|
$res = $this->query(
|
|
|
|
|
"SELECT * FROM " . $this->tableName( $table ) . " LIMIT 1",
|
|
|
|
|
__METHOD__,
|
|
|
|
|
self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
|
|
|
|
);
|
2013-05-03 01:39:16 +00:00
|
|
|
if ( !$res ) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2021-07-15 00:46:59 +00:00
|
|
|
/** @var MysqliResultWrapper $res */
|
|
|
|
|
'@phan-var MysqliResultWrapper $res';
|
|
|
|
|
return $res->getInternalFieldInfo( $field );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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__ ) {
|
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
|
|
|
$index = $this->indexName( $index );
|
|
|
|
|
|
2020-03-20 13:16:46 +00:00
|
|
|
$res = $this->query(
|
|
|
|
|
'SHOW INDEX FROM ' . $this->tableName( $table ),
|
|
|
|
|
$fname,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
|
|
|
|
);
|
2013-05-03 01:39:16 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
2018-03-02 04:30:07 +00:00
|
|
|
return $result ?: false;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
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 );
|
|
|
|
|
|
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 ) == '`';
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-19 16:05:16 +00:00
|
|
|
protected function doGetLag() {
|
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
|
|
|
/**
|
2021-01-21 07:17:50 +00:00
|
|
|
* @return int|false Second of lag
|
2013-05-03 01:39:16 +00:00
|
|
|
*/
|
2015-09-25 19:53:04 +00:00
|
|
|
protected function getLagFromSlaveStatus() {
|
2020-03-20 13:16:46 +00:00
|
|
|
$res = $this->query(
|
|
|
|
|
'SHOW SLAVE STATUS',
|
|
|
|
|
__METHOD__,
|
|
|
|
|
self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
|
|
|
|
);
|
2015-09-25 19:53:04 +00:00
|
|
|
$row = $res ? $res->fetchObject() : false;
|
2018-03-29 23:14:37 +00:00
|
|
|
// If the server is not replicating, there will be no row
|
2015-09-25 19:53:04 +00:00
|
|
|
if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
|
2021-05-12 04:39:46 +00:00
|
|
|
// https://mariadb.com/kb/en/delayed-replication/
|
|
|
|
|
// https://dev.mysql.com/doc/refman/5.6/en/replication-delayed.html
|
|
|
|
|
return intval( $row->Seconds_Behind_Master + ( $row->SQL_Remaining_Delay ?? 0 ) );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2015-09-25 19:53:04 +00:00
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2021-01-21 07:17:50 +00:00
|
|
|
* @return float|false Seconds of lag
|
2015-09-25 19:53:04 +00:00
|
|
|
*/
|
|
|
|
|
protected function getLagFromPtHeartbeat() {
|
2016-03-08 20:36:03 +00:00
|
|
|
$options = $this->lagDetectionOptions;
|
|
|
|
|
|
2018-04-02 21:39:33 +00:00
|
|
|
$currentTrxInfo = $this->getRecordedTransactionLagStatus();
|
|
|
|
|
if ( $currentTrxInfo ) {
|
|
|
|
|
// There is an active transaction and the initial lag was already queried
|
|
|
|
|
$staleness = microtime( true ) - $currentTrxInfo['since'];
|
|
|
|
|
if ( $staleness > self::LAG_STALE_WARN_THRESHOLD ) {
|
|
|
|
|
// Avoid returning higher and higher lag value due to snapshot age
|
|
|
|
|
// given that the isolation level will typically be REPEATABLE-READ
|
|
|
|
|
$this->queryLogger->warning(
|
|
|
|
|
"Using cached lag value for {db_server} due to active transaction",
|
2018-12-21 18:20:58 +00:00
|
|
|
$this->getLogContext( [
|
|
|
|
|
'method' => __METHOD__,
|
|
|
|
|
'age' => $staleness,
|
2019-12-04 20:19:52 +00:00
|
|
|
'exception' => new RuntimeException()
|
2018-12-21 18:20:58 +00:00
|
|
|
] )
|
2018-04-02 21:39:33 +00:00
|
|
|
);
|
|
|
|
|
}
|
2018-03-29 23:30:38 +00:00
|
|
|
|
2018-04-02 21:39:33 +00:00
|
|
|
return $currentTrxInfo['lag'];
|
2018-03-29 23:30:38 +00:00
|
|
|
}
|
|
|
|
|
|
2016-03-08 20:36:03 +00:00
|
|
|
if ( isset( $options['conds'] ) ) {
|
|
|
|
|
// Best method for multi-DC setups: use logical channel names
|
2021-01-21 07:17:50 +00:00
|
|
|
$ago = $this->fetchSecondsSinceHeartbeat( $options['conds'] );
|
2016-03-08 20:36:03 +00:00
|
|
|
} else {
|
2021-09-01 21:04:40 +00:00
|
|
|
// Standard method: use primary server ID (works with stock pt-heartbeat)
|
2021-09-02 23:23:41 +00:00
|
|
|
$masterInfo = $this->getPrimaryServerInfo();
|
2016-03-08 20:36:03 +00:00
|
|
|
if ( !$masterInfo ) {
|
2016-09-16 03:33:25 +00:00
|
|
|
$this->queryLogger->error(
|
2021-09-01 21:04:40 +00:00
|
|
|
"Unable to query primary of {db_server} for server ID",
|
2016-03-08 20:36:03 +00:00
|
|
|
$this->getLogContext( [
|
|
|
|
|
'method' => __METHOD__
|
|
|
|
|
] )
|
|
|
|
|
);
|
|
|
|
|
|
2021-09-01 21:04:40 +00:00
|
|
|
return false; // could not get primary server ID
|
2016-03-08 20:36:03 +00:00
|
|
|
}
|
2016-01-13 22:33:38 +00:00
|
|
|
|
2016-03-08 20:36:03 +00:00
|
|
|
$conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
|
2021-01-21 07:17:50 +00:00
|
|
|
$ago = $this->fetchSecondsSinceHeartbeat( $conds );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2015-09-25 19:53:04 +00:00
|
|
|
|
2021-01-21 07:17:50 +00:00
|
|
|
if ( $ago !== null ) {
|
|
|
|
|
return max( $ago, 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;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-02 23:23:41 +00:00
|
|
|
protected function getPrimaryServerInfo() {
|
2015-12-04 00:37:56 +00:00
|
|
|
$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
|
2021-03-10 01:38:23 +00:00
|
|
|
$this->topologyRootMaster ?? $this->getServerName()
|
2015-12-04 00:37:56 +00:00
|
|
|
);
|
2018-09-30 15:09:58 +00:00
|
|
|
$fname = __METHOD__;
|
2015-12-04 00:37:56 +00:00
|
|
|
|
|
|
|
|
return $cache->getWithSetCallback(
|
|
|
|
|
$key,
|
|
|
|
|
$cache::TTL_INDEFINITE,
|
2018-09-30 15:09:58 +00:00
|
|
|
function () use ( $cache, $key, $fname ) {
|
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 ) ) {
|
2021-09-01 21:04:40 +00:00
|
|
|
return false; // avoid primary DB connection spike slams
|
2015-12-04 00:37:56 +00:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-20 13:16:46 +00:00
|
|
|
$flags = self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
|
2021-09-01 21:04:40 +00:00
|
|
|
// Connect to and query the primary DB; catch errors to avoid outages
|
2015-12-04 00:37:56 +00:00
|
|
|
try {
|
2019-06-29 00:58:56 +00:00
|
|
|
$res = $conn->query( 'SELECT @@server_id AS id', $fname, $flags );
|
2015-12-04 00:37:56 +00:00
|
|
|
$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
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-02 23:23:41 +00:00
|
|
|
protected function getMasterServerInfo() {
|
|
|
|
|
wfDeprecated( __METHOD__, '1.37' );
|
|
|
|
|
return $this->getPrimaryServerInfo();
|
|
|
|
|
}
|
|
|
|
|
|
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
|
2021-01-21 07:17:50 +00:00
|
|
|
* @return float|null Elapsed seconds since the newest beat or null if none was found
|
2015-12-04 00:37:56 +00:00
|
|
|
* @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html
|
|
|
|
|
*/
|
2021-01-21 07:17:50 +00:00
|
|
|
protected function fetchSecondsSinceHeartbeat( array $conds ) {
|
2019-08-07 12:37:11 +00:00
|
|
|
$whereSQL = $this->makeList( $conds, self::LIST_AND );
|
2021-01-21 07:17:50 +00:00
|
|
|
// User mysql server time so that query time and trip time are not counted.
|
2019-08-07 12:37:11 +00:00
|
|
|
// Use ORDER BY for channel based queries since that field might not be UNIQUE.
|
|
|
|
|
$res = $this->query(
|
2021-01-21 07:17:50 +00:00
|
|
|
"SELECT TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6)) AS us_ago " .
|
|
|
|
|
"FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
|
2019-08-07 12:37:11 +00:00
|
|
|
__METHOD__,
|
2020-03-20 13:16:46 +00:00
|
|
|
self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
2019-08-07 12:37:11 +00:00
|
|
|
);
|
|
|
|
|
$row = $res ? $res->fetchObject() : false;
|
2015-09-25 19:53:04 +00:00
|
|
|
|
2021-01-21 07:17:50 +00:00
|
|
|
return $row ? ( $row->us_ago / 1e6 ) : null;
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-10 01:38:23 +00:00
|
|
|
$key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServerName() );
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-02 18:57:19 +00:00
|
|
|
public function primaryPosWait( DBPrimaryPos $pos, $timeout ) {
|
2021-05-14 19:48:07 +00:00
|
|
|
if ( !( $pos instanceof MySQLPrimaryPos ) ) {
|
|
|
|
|
throw new InvalidArgumentException( "Position not an instance of MySQLPrimaryPos" );
|
2016-02-17 22:31:31 +00:00
|
|
|
}
|
|
|
|
|
|
2019-07-14 22:43:26 +00:00
|
|
|
if ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
|
2019-06-19 08:10:15 +00:00
|
|
|
$this->queryLogger->debug(
|
|
|
|
|
"Bypassed replication wait; database has a static dataset",
|
2019-10-10 23:15:43 +00:00
|
|
|
$this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )
|
2019-06-19 08:10:15 +00:00
|
|
|
);
|
|
|
|
|
|
2021-05-14 20:04:02 +00:00
|
|
|
return 0; // this is a copy of a read-only dataset with no primary DB
|
2016-09-03 22:06:59 +00:00
|
|
|
} elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
|
2019-06-19 08:10:15 +00:00
|
|
|
$this->queryLogger->debug(
|
2019-10-10 23:15:43 +00:00
|
|
|
"Bypassed replication wait; replication known to have reached {raw_pos}",
|
|
|
|
|
$this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )
|
2019-06-19 08:10:15 +00:00
|
|
|
);
|
|
|
|
|
|
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
|
2018-02-09 23:01:40 +00:00
|
|
|
if ( $pos->getGTIDs() ) {
|
2019-06-19 08:10:15 +00:00
|
|
|
// Get the GTIDs from this replica server too see the domains (channels)
|
|
|
|
|
$refPos = $this->getReplicaPos();
|
|
|
|
|
if ( !$refPos ) {
|
|
|
|
|
$this->queryLogger->error(
|
2019-10-10 23:15:43 +00:00
|
|
|
"Could not get replication position on replica DB to compare to {raw_pos}",
|
|
|
|
|
$this->getLogContext( [ 'method' => __METHOD__, 'raw_pos' => $pos ] )
|
2019-06-19 08:10:15 +00:00
|
|
|
);
|
|
|
|
|
|
2021-09-01 21:04:40 +00:00
|
|
|
return -1; // this is the primary DB itself?
|
2019-06-19 08:10:15 +00:00
|
|
|
}
|
|
|
|
|
// GTIDs with domains (channels) that are active and are present on the replica
|
|
|
|
|
$gtidsWait = $pos::getRelevantActiveGTIDs( $pos, $refPos );
|
2018-02-09 23:01:40 +00:00
|
|
|
if ( !$gtidsWait ) {
|
2018-03-30 11:39:05 +00:00
|
|
|
$this->queryLogger->error(
|
2019-10-10 23:15:43 +00:00
|
|
|
"No active GTIDs in {raw_pos} share a domain with those in {current_pos}",
|
|
|
|
|
$this->getLogContext( [
|
|
|
|
|
'method' => __METHOD__,
|
|
|
|
|
'raw_pos' => $pos,
|
|
|
|
|
'current_pos' => $refPos
|
|
|
|
|
] )
|
2018-03-30 11:39:05 +00:00
|
|
|
);
|
|
|
|
|
|
2018-02-09 23:01:40 +00:00
|
|
|
return -1; // $pos is from the wrong cluster?
|
|
|
|
|
}
|
2019-06-19 08:10:15 +00:00
|
|
|
// Wait on the GTID set
|
2018-02-09 23:01:40 +00:00
|
|
|
$gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
|
2018-04-03 00:36:22 +00:00
|
|
|
if ( strpos( $gtidArg, ':' ) !== false ) {
|
|
|
|
|
// MySQL GTIDs, e.g "source_id:transaction_id"
|
2019-05-22 20:31:59 +00:00
|
|
|
$sql = "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)";
|
2018-04-03 00:36:22 +00:00
|
|
|
} else {
|
|
|
|
|
// MariaDB GTIDs, e.g."domain:server:sequence"
|
2019-05-22 20:31:59 +00:00
|
|
|
$sql = "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)";
|
2018-04-03 00:36:22 +00:00
|
|
|
}
|
2019-10-10 23:15:43 +00:00
|
|
|
$waitPos = implode( ',', $gtidsWait );
|
2016-05-21 00:26:08 +00:00
|
|
|
} else {
|
|
|
|
|
// Wait on the binlog coordinates
|
2018-02-07 09:39:24 +00:00
|
|
|
$encFile = $this->addQuotes( $pos->getLogFile() );
|
2019-10-12 12:49:17 +00:00
|
|
|
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable
|
2018-04-03 00:36:22 +00:00
|
|
|
$encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
|
2019-05-22 20:31:59 +00:00
|
|
|
$sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
|
2019-10-10 23:15:43 +00:00
|
|
|
$waitPos = $pos->__toString();
|
2016-05-21 00:26:08 +00:00
|
|
|
}
|
2016-02-17 22:31:31 +00:00
|
|
|
|
2019-10-11 16:57:11 +00:00
|
|
|
$start = microtime( true );
|
2020-03-20 13:16:46 +00:00
|
|
|
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
|
|
|
|
|
$res = $this->query( $sql, __METHOD__, $flags );
|
2019-06-19 08:10:15 +00:00
|
|
|
$row = $this->fetchRow( $res );
|
2019-10-11 16:57:11 +00:00
|
|
|
$seconds = max( microtime( true ) - $start, 0 );
|
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 ) {
|
2019-10-11 16:57:11 +00:00
|
|
|
$this->replLogger->error(
|
2020-08-05 04:37:56 +00:00
|
|
|
"An error occurred while waiting for replication to reach {wait_pos}",
|
2019-10-10 23:15:43 +00:00
|
|
|
$this->getLogContext( [
|
|
|
|
|
'raw_pos' => $pos,
|
|
|
|
|
'wait_pos' => $waitPos,
|
2019-10-11 16:57:11 +00:00
|
|
|
'sql' => $sql,
|
|
|
|
|
'seconds_waited' => $seconds,
|
|
|
|
|
'exception' => new RuntimeException()
|
2019-10-10 23:15:43 +00:00
|
|
|
] )
|
2019-06-19 08:10:15 +00:00
|
|
|
);
|
|
|
|
|
} elseif ( $status < 0 ) {
|
2019-10-11 16:57:11 +00:00
|
|
|
$this->replLogger->error(
|
2020-08-05 04:37:56 +00:00
|
|
|
"Timed out waiting for replication to reach {wait_pos}",
|
2019-06-19 08:10:15 +00:00
|
|
|
$this->getLogContext( [
|
2019-10-10 23:15:43 +00:00
|
|
|
'raw_pos' => $pos,
|
|
|
|
|
'wait_pos' => $waitPos,
|
|
|
|
|
'timeout' => $timeout,
|
|
|
|
|
'sql' => $sql,
|
2019-10-11 16:57:11 +00:00
|
|
|
'seconds_waited' => $seconds,
|
|
|
|
|
'exception' => new RuntimeException()
|
2019-06-19 08:10:15 +00:00
|
|
|
] )
|
|
|
|
|
);
|
2016-02-17 22:31:31 +00:00
|
|
|
} elseif ( $status >= 0 ) {
|
2019-10-11 16:57:11 +00:00
|
|
|
$this->replLogger->debug(
|
2020-08-05 04:37:56 +00:00
|
|
|
"Replication has reached {wait_pos}",
|
2019-10-10 23:15:43 +00:00
|
|
|
$this->getLogContext( [
|
|
|
|
|
'raw_pos' => $pos,
|
2019-10-11 16:57:11 +00:00
|
|
|
'wait_pos' => $waitPos,
|
|
|
|
|
'seconds_waited' => $seconds,
|
2019-10-10 23:15:43 +00:00
|
|
|
] )
|
2019-06-19 08:10:15 +00:00
|
|
|
);
|
2016-02-17 22:31:31 +00:00
|
|
|
// 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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2021-09-01 21:04:40 +00:00
|
|
|
* Get the position of the primary DB from SHOW SLAVE STATUS
|
2013-05-03 01:39:16 +00:00
|
|
|
*
|
2021-05-14 19:48:07 +00:00
|
|
|
* @return MySQLPrimaryPos|bool
|
2013-05-03 01:39:16 +00:00
|
|
|
*/
|
2016-11-03 05:24:51 +00:00
|
|
|
public function getReplicaPos() {
|
2018-04-03 00:36:22 +00:00
|
|
|
$now = microtime( true ); // as-of-time *before* fetching GTID variables
|
|
|
|
|
|
|
|
|
|
if ( $this->useGTIDs() ) {
|
|
|
|
|
// Try to use GTIDs, fallbacking to binlog positions if not possible
|
|
|
|
|
$data = $this->getServerGTIDs( __METHOD__ );
|
|
|
|
|
// Use gtid_slave_pos for MariaDB and gtid_executed for MySQL
|
|
|
|
|
foreach ( [ 'gtid_slave_pos', 'gtid_executed' ] as $name ) {
|
|
|
|
|
if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
|
2021-05-14 19:48:07 +00:00
|
|
|
return new MySQLPrimaryPos( $data[$name], $now );
|
2018-04-03 00:36:22 +00:00
|
|
|
}
|
2016-05-21 00:26:08 +00:00
|
|
|
}
|
2018-02-07 09:39:24 +00:00
|
|
|
}
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2018-04-03 00:36:22 +00:00
|
|
|
$data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ );
|
|
|
|
|
if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) {
|
2021-05-14 19:48:07 +00:00
|
|
|
return new MySQLPrimaryPos(
|
2018-04-03 00:36:22 +00:00
|
|
|
"{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
|
2018-02-07 09:39:24 +00:00
|
|
|
$now
|
|
|
|
|
);
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2018-02-07 09:39:24 +00:00
|
|
|
|
|
|
|
|
return false;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2021-09-01 21:04:40 +00:00
|
|
|
* Get the position of the primary DB from SHOW MASTER STATUS
|
2013-05-03 01:39:16 +00:00
|
|
|
*
|
2021-05-14 19:48:07 +00:00
|
|
|
* @return MySQLPrimaryPos|bool
|
2013-05-03 01:39:16 +00:00
|
|
|
*/
|
2021-05-14 21:56:40 +00:00
|
|
|
public function getPrimaryPos() {
|
2018-04-03 00:36:22 +00:00
|
|
|
$now = microtime( true ); // as-of-time *before* fetching GTID variables
|
|
|
|
|
|
|
|
|
|
$pos = false;
|
|
|
|
|
if ( $this->useGTIDs() ) {
|
|
|
|
|
// Try to use GTIDs, fallbacking to binlog positions if not possible
|
|
|
|
|
$data = $this->getServerGTIDs( __METHOD__ );
|
|
|
|
|
// Use gtid_binlog_pos for MariaDB and gtid_executed for MySQL
|
|
|
|
|
foreach ( [ 'gtid_binlog_pos', 'gtid_executed' ] as $name ) {
|
|
|
|
|
if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
|
2021-05-14 19:48:07 +00:00
|
|
|
$pos = new MySQLPrimaryPos( $data[$name], $now );
|
2018-04-03 00:36:22 +00:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Filter domains that are inactive or not relevant to the session
|
|
|
|
|
if ( $pos ) {
|
|
|
|
|
$pos->setActiveOriginServerId( $this->getServerId() );
|
|
|
|
|
$pos->setActiveOriginServerUUID( $this->getServerUUID() );
|
|
|
|
|
if ( isset( $data['gtid_domain_id'] ) ) {
|
|
|
|
|
$pos->setActiveDomain( $data['gtid_domain_id'] );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-05-03 01:39:16 +00:00
|
|
|
|
2018-04-03 00:36:22 +00:00
|
|
|
if ( !$pos ) {
|
|
|
|
|
$data = $this->getServerRoleStatus( 'MASTER', __METHOD__ );
|
|
|
|
|
if ( $data && strlen( $data['File'] ) ) {
|
2021-05-14 19:48:07 +00:00
|
|
|
$pos = new MySQLPrimaryPos( "{$data['File']}/{$data['Position']}", $now );
|
2016-05-21 00:26:08 +00:00
|
|
|
}
|
2018-02-07 09:39:24 +00:00
|
|
|
}
|
2016-05-21 00:26:08 +00:00
|
|
|
|
2018-04-03 00:36:22 +00:00
|
|
|
return $pos;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-14 21:56:40 +00:00
|
|
|
public function getMasterPos() {
|
2021-09-04 00:47:52 +00:00
|
|
|
wfDeprecated( __METHOD__, '1.37' );
|
2021-05-14 21:56:40 +00:00
|
|
|
return $this->getPrimaryPos();
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-07 20:59:01 +00:00
|
|
|
/**
|
|
|
|
|
* @inheritDoc
|
|
|
|
|
* @return string|null 32 bit integer ID; null if not applicable or unknown
|
|
|
|
|
*/
|
2021-05-04 22:03:26 +00:00
|
|
|
public function getTopologyBasedServerId() {
|
|
|
|
|
return $this->getServerId();
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-03 00:36:22 +00:00
|
|
|
/**
|
2021-06-07 20:59:01 +00:00
|
|
|
* @return string Value of server_id (32-bit integer, unique to the replication topology)
|
2021-05-04 22:03:26 +00:00
|
|
|
* @throws DBQueryError
|
2018-04-03 00:36:22 +00:00
|
|
|
*/
|
|
|
|
|
protected function getServerId() {
|
2018-09-30 15:09:58 +00:00
|
|
|
$fname = __METHOD__;
|
2018-04-03 00:36:22 +00:00
|
|
|
return $this->srvCache->getWithSetCallback(
|
2021-03-10 01:38:23 +00:00
|
|
|
$this->srvCache->makeGlobalKey( 'mysql-server-id', $this->getServerName() ),
|
2018-04-03 00:36:22 +00:00
|
|
|
self::SERVER_ID_CACHE_TTL,
|
2018-09-30 15:09:58 +00:00
|
|
|
function () use ( $fname ) {
|
2020-03-20 13:16:46 +00:00
|
|
|
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
|
2019-06-29 00:58:56 +00:00
|
|
|
$res = $this->query( "SELECT @@server_id AS id", $fname, $flags );
|
|
|
|
|
|
2021-06-07 20:59:01 +00:00
|
|
|
return $this->fetchObject( $res )->id;
|
2018-04-03 00:36:22 +00:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2021-06-07 20:59:01 +00:00
|
|
|
* @return string|null Value of server_uuid (hyphenated 128-bit hex string, globally unique)
|
2021-05-04 22:03:26 +00:00
|
|
|
* @throws DBQueryError
|
2018-04-03 00:36:22 +00:00
|
|
|
*/
|
|
|
|
|
protected function getServerUUID() {
|
2019-06-29 00:58:56 +00:00
|
|
|
$fname = __METHOD__;
|
2018-04-03 00:36:22 +00:00
|
|
|
return $this->srvCache->getWithSetCallback(
|
2021-03-10 01:38:23 +00:00
|
|
|
$this->srvCache->makeGlobalKey( 'mysql-server-uuid', $this->getServerName() ),
|
2018-04-03 00:36:22 +00:00
|
|
|
self::SERVER_ID_CACHE_TTL,
|
2019-06-29 00:58:56 +00:00
|
|
|
function () use ( $fname ) {
|
2020-03-20 13:16:46 +00:00
|
|
|
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
|
2019-06-29 00:58:56 +00:00
|
|
|
$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'server_uuid'", $fname, $flags );
|
2018-04-03 00:36:22 +00:00
|
|
|
$row = $this->fetchObject( $res );
|
|
|
|
|
|
|
|
|
|
return $row ? $row->Value : null;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @return string[]
|
|
|
|
|
*/
|
|
|
|
|
protected function getServerGTIDs( $fname = __METHOD__ ) {
|
|
|
|
|
$map = [];
|
2019-06-29 00:58:56 +00:00
|
|
|
|
2020-03-20 13:16:46 +00:00
|
|
|
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
|
|
|
|
|
|
2018-04-03 00:36:22 +00:00
|
|
|
// Get global-only variables like gtid_executed
|
2019-06-29 00:58:56 +00:00
|
|
|
$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname, $flags );
|
2018-04-03 00:36:22 +00:00
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
$map[$row->Variable_name] = $row->Value;
|
|
|
|
|
}
|
|
|
|
|
// Get session-specific (e.g. gtid_domain_id since that is were writes will log)
|
2019-06-29 00:58:56 +00:00
|
|
|
$res = $this->query( "SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname, $flags );
|
2018-04-03 00:36:22 +00:00
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
$map[$row->Variable_name] = $row->Value;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2018-02-07 09:39:24 +00:00
|
|
|
|
2018-04-03 00:36:22 +00:00
|
|
|
return $map;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $role One of "MASTER"/"SLAVE"
|
|
|
|
|
* @param string $fname
|
|
|
|
|
* @return string[] Latest available server status row
|
|
|
|
|
*/
|
|
|
|
|
protected function getServerRoleStatus( $role, $fname = __METHOD__ ) {
|
2020-03-20 13:16:46 +00:00
|
|
|
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
|
|
|
|
|
$res = $this->query( "SHOW $role STATUS", $fname, $flags );
|
2019-06-29 00:58:56 +00:00
|
|
|
|
2020-03-20 13:16:46 +00:00
|
|
|
return $res->fetchRow() ?: [];
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2016-07-22 05:15:30 +00:00
|
|
|
public function serverIsReadOnly() {
|
2019-07-16 06:06:13 +00:00
|
|
|
// Avoid SHOW to avoid internal temporary tables
|
2020-03-20 13:16:46 +00:00
|
|
|
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
|
2019-07-16 06:06:13 +00:00
|
|
|
$res = $this->query( "SELECT @@GLOBAL.read_only AS Value", __METHOD__, $flags );
|
2016-07-22 05:15:30 +00:00
|
|
|
$row = $this->fetchObject( $res );
|
|
|
|
|
|
2019-07-16 06:06:13 +00:00
|
|
|
return $row ? (bool)$row->Value : false;
|
2016-07-22 05:15:30 +00:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
*/
|
2020-05-17 02:07:58 +00:00
|
|
|
public function useIndexClause( $index ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
return "FORCE INDEX (" . $this->indexName( $index ) . ")";
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-04 10:56:40 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $index
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
2020-05-17 02:07:58 +00:00
|
|
|
public function ignoreIndexClause( $index ) {
|
2016-07-04 10:56:40 +00:00
|
|
|
return "IGNORE INDEX (" . $this->indexName( $index ) . ")";
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function getSoftwareLink() {
|
2020-01-10 04:41:16 +00:00
|
|
|
list( $variant ) = $this->getMySqlServerVariant();
|
|
|
|
|
if ( $variant === 'MariaDB' ) {
|
2014-01-09 01:50:21 +00:00
|
|
|
return '[{{int:version-db-mariadb-url}} MariaDB]';
|
|
|
|
|
}
|
2014-01-16 07:18:36 +00:00
|
|
|
|
|
|
|
|
return '[{{int:version-db-mysql-url}} MySQL]';
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2020-01-10 04:41:16 +00:00
|
|
|
/**
|
|
|
|
|
* @return string[] (one of ("MariaDB","MySQL"), x.y.z version string)
|
|
|
|
|
*/
|
|
|
|
|
protected function getMySqlServerVariant() {
|
|
|
|
|
$version = $this->getServerVersion();
|
|
|
|
|
|
|
|
|
|
// MariaDB includes its name in its version string; this is how MariaDB's version of
|
|
|
|
|
// the mysql command-line client identifies MariaDB servers.
|
|
|
|
|
// https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_version
|
|
|
|
|
// https://mariadb.com/kb/en/version/
|
|
|
|
|
$parts = explode( '-', $version, 2 );
|
|
|
|
|
$number = $parts[0];
|
|
|
|
|
$suffix = $parts[1] ?? '';
|
|
|
|
|
if ( strpos( $suffix, 'MariaDB' ) !== false || strpos( $suffix, '-maria-' ) !== false ) {
|
|
|
|
|
$vendor = 'MariaDB';
|
|
|
|
|
} else {
|
|
|
|
|
$vendor = 'MySQL';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [ $vendor, $number ];
|
|
|
|
|
}
|
|
|
|
|
|
2014-08-31 10:43:50 +00:00
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function getServerVersion() {
|
2019-09-04 02:47:35 +00:00
|
|
|
$cache = $this->srvCache;
|
|
|
|
|
$fname = __METHOD__;
|
|
|
|
|
|
|
|
|
|
return $cache->getWithSetCallback(
|
2021-03-10 01:38:23 +00:00
|
|
|
$cache->makeGlobalKey( 'mysql-server-version', $this->getServerName() ),
|
2019-09-04 02:47:35 +00:00
|
|
|
$cache::TTL_HOUR,
|
|
|
|
|
function () use ( $fname ) {
|
|
|
|
|
// 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).
|
|
|
|
|
return $this->selectField( '', 'VERSION()', '', $fname );
|
|
|
|
|
}
|
|
|
|
|
);
|
2014-08-31 10:43:50 +00:00
|
|
|
}
|
|
|
|
|
|
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'];
|
2021-12-14 01:00:28 +00:00
|
|
|
$this->query( "SET net_read_timeout=$timeout", __METHOD__, self::QUERY_CHANGE_TRX );
|
|
|
|
|
$this->query( "SET net_write_timeout=$timeout", __METHOD__, self::QUERY_CHANGE_TRX );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-27 01:54:51 +00:00
|
|
|
/**
|
2017-08-11 00:23:16 +00:00
|
|
|
* @param string &$sql
|
|
|
|
|
* @param string &$newLine
|
2013-12-27 01:54:51 +00:00
|
|
|
* @return bool
|
|
|
|
|
*/
|
2013-05-03 01:39:16 +00:00
|
|
|
public function streamStatementEnd( &$sql, &$newLine ) {
|
2019-09-19 10:58:20 +00:00
|
|
|
if ( preg_match( '/^DELIMITER\s+(\S+)/i', $newLine, $m ) ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
$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 );
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-09 04:14:26 +00:00
|
|
|
public function doLockIsFree( string $lockName, string $method ) {
|
2016-09-21 00:25:58 +00:00
|
|
|
$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
|
2019-06-29 00:58:56 +00:00
|
|
|
|
2021-06-09 04:14:26 +00:00
|
|
|
$res = $this->query(
|
|
|
|
|
"SELECT IS_FREE_LOCK($encName) AS unlocked",
|
|
|
|
|
$method,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
|
|
|
|
);
|
2019-06-29 00:58:56 +00:00
|
|
|
$row = $this->fetchObject( $res );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2021-06-09 04:14:26 +00:00
|
|
|
return ( $row->unlocked == 1 );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2021-06-09 04:14:26 +00:00
|
|
|
public function doLock( string $lockName, string $method, int $timeout ) {
|
2016-09-21 00:25:58 +00:00
|
|
|
$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
|
2021-12-26 11:24:49 +00:00
|
|
|
// Unlike NOW(), SYSDATE() gets the time at invocation rather than query start.
|
2021-06-09 04:14:26 +00:00
|
|
|
// The precision argument is silently ignored for MySQL < 5.6 and MariaDB < 5.3.
|
|
|
|
|
// https://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_sysdate
|
|
|
|
|
// https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html
|
|
|
|
|
$res = $this->query(
|
|
|
|
|
"SELECT IF(GET_LOCK($encName,$timeout),UNIX_TIMESTAMP(SYSDATE(6)),NULL) AS acquired",
|
|
|
|
|
$method,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
|
|
|
|
);
|
2019-06-29 00:58:56 +00:00
|
|
|
$row = $this->fetchObject( $res );
|
2013-05-03 01:39:16 +00:00
|
|
|
|
2021-06-09 04:14:26 +00:00
|
|
|
return ( $row->acquired !== null ) ? (float)$row->acquired : null;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2021-06-09 04:14:26 +00:00
|
|
|
public function doUnlock( string $lockName, string $method ) {
|
2016-09-21 00:25:58 +00:00
|
|
|
$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
|
2019-06-29 00:58:56 +00:00
|
|
|
|
2021-06-09 04:14:26 +00:00
|
|
|
$res = $this->query(
|
|
|
|
|
"SELECT RELEASE_LOCK($encName) AS released",
|
|
|
|
|
$method,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
|
|
|
|
);
|
2019-06-29 00:58:56 +00:00
|
|
|
$row = $this->fetchObject( $res );
|
2013-11-20 06:58:22 +00:00
|
|
|
|
2021-06-09 04:14:26 +00:00
|
|
|
return ( $row->released == 1 );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2015-12-16 05:34:52 +00:00
|
|
|
private function makeLockName( $lockName ) {
|
2021-05-20 19:25:17 +00:00
|
|
|
// https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html#function_get-lock
|
|
|
|
|
// MySQL 5.7+ enforces a 64 char length limit.
|
2015-12-16 05:34:52 +00:00
|
|
|
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
|
|
|
|
2020-03-20 13:16:46 +00:00
|
|
|
$this->query(
|
|
|
|
|
"LOCK TABLES " . implode( ',', $items ),
|
|
|
|
|
$method,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS
|
|
|
|
|
);
|
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 ) {
|
2020-03-20 13:16:46 +00:00
|
|
|
$this->query(
|
|
|
|
|
"UNLOCK TABLES",
|
|
|
|
|
$method,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS
|
|
|
|
|
);
|
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' ) {
|
2018-02-13 06:58:57 +00:00
|
|
|
if ( $this->defaultBigSelects === null ) {
|
2013-05-03 01:39:16 +00:00
|
|
|
# Function hasn't been called before so it must already be set to the default
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
2018-02-13 06:58:57 +00:00
|
|
|
$value = $this->defaultBigSelects;
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2018-02-13 06:58:57 +00:00
|
|
|
} elseif ( $this->defaultBigSelects === null ) {
|
|
|
|
|
$this->defaultBigSelects =
|
2015-09-27 08:15:12 +00:00
|
|
|
(bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2020-03-20 13:16:46 +00:00
|
|
|
|
|
|
|
|
$this->query(
|
|
|
|
|
"SET sql_big_selects=" . ( $value ? '1' : '0' ),
|
|
|
|
|
__METHOD__,
|
2021-12-14 01:00:28 +00:00
|
|
|
self::QUERY_CHANGE_TRX
|
2020-03-20 13:16:46 +00:00
|
|
|
);
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
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
|
|
|
}
|
|
|
|
|
|
2020-03-20 13:16:46 +00:00
|
|
|
$this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
2021-05-27 19:18:56 +00:00
|
|
|
protected function doUpsert(
|
|
|
|
|
string $table,
|
|
|
|
|
array $rows,
|
|
|
|
|
array $identityKey,
|
|
|
|
|
array $set,
|
|
|
|
|
string $fname
|
|
|
|
|
) {
|
2020-03-02 22:20:09 +00:00
|
|
|
$encTable = $this->tableName( $table );
|
|
|
|
|
list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
|
|
|
|
|
$sqlColumnAssignments = $this->makeList( $set, self::LIST_SET );
|
2014-02-03 20:42:04 +00:00
|
|
|
|
2020-03-02 22:20:09 +00:00
|
|
|
$sql =
|
|
|
|
|
"INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples " .
|
|
|
|
|
"ON DUPLICATE KEY UPDATE $sqlColumnAssignments";
|
2013-05-03 01:39:16 +00:00
|
|
|
|
2020-03-20 13:16:46 +00:00
|
|
|
$this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
|
2020-03-02 22:20:09 +00:00
|
|
|
}
|
2013-05-03 01:39:16 +00:00
|
|
|
|
2021-05-27 19:18:56 +00:00
|
|
|
protected function doReplace( $table, array $identityKey, array $rows, $fname ) {
|
2020-03-02 22:20:09 +00:00
|
|
|
$encTable = $this->tableName( $table );
|
|
|
|
|
list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
|
2013-05-03 01:39:16 +00:00
|
|
|
|
2020-03-02 22:20:09 +00:00
|
|
|
$sql = "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples";
|
2018-10-26 20:17:34 +00:00
|
|
|
|
2020-03-20 13:16:46 +00:00
|
|
|
$this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 );
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 23:01:08 +00:00
|
|
|
protected function isConnectionError( $errno ) {
|
2015-08-06 22:09:54 +00:00
|
|
|
return $errno == 2013 || $errno == 2006;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-23 09:57:21 +00:00
|
|
|
protected function wasKnownStatementRollbackError() {
|
|
|
|
|
$errno = $this->lastErrno();
|
|
|
|
|
|
|
|
|
|
if ( $errno === 1205 ) { // lock wait timeout
|
|
|
|
|
// Note that this is uncached to avoid stale values of SET is used
|
2021-03-19 19:30:13 +00:00
|
|
|
$res = $this->query(
|
|
|
|
|
"SELECT @@innodb_rollback_on_timeout AS Value",
|
|
|
|
|
__METHOD__,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
2018-03-23 09:57:21 +00:00
|
|
|
);
|
2021-03-19 19:30:13 +00:00
|
|
|
$row = $res ? $res->fetchObject() : false;
|
2018-03-23 09:57:21 +00:00
|
|
|
// https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
|
|
|
|
|
// https://dev.mysql.com/doc/refman/5.5/en/innodb-parameters.html
|
2021-03-19 19:30:13 +00:00
|
|
|
return ( $row && !$row->Value );
|
2018-03-23 09:57:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// See https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html
|
2019-03-21 23:54:46 +00:00
|
|
|
return in_array( $errno, [ 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ], true );
|
2018-03-23 09:57:21 +00:00
|
|
|
}
|
|
|
|
|
|
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 );
|
2014-01-06 18:32:50 +00:00
|
|
|
|
2020-03-20 15:36:49 +00:00
|
|
|
return $this->query(
|
|
|
|
|
"CREATE $tmp TABLE $newName (LIKE $oldName)",
|
|
|
|
|
$fname,
|
2020-03-20 13:16:46 +00:00
|
|
|
self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA
|
2020-03-20 15:36:49 +00:00
|
|
|
);
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* List all tables on the database
|
|
|
|
|
*
|
2018-06-26 21:14:43 +00:00
|
|
|
* @param string|null $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__ ) {
|
2020-03-20 13:16:46 +00:00
|
|
|
$result = $this->query(
|
|
|
|
|
"SHOW TABLES",
|
|
|
|
|
$fname,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
|
|
|
|
);
|
2013-05-03 01:39:16 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-24 15:14:40 +00:00
|
|
|
/**
|
|
|
|
|
* Lists VIEWs in the database
|
|
|
|
|
*
|
2018-06-26 21:14:43 +00:00
|
|
|
* @param string|null $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
|
2018-08-14 23:44:41 +00:00
|
|
|
$propertyName = 'Tables_in_' . $this->getDBname();
|
2013-05-24 15:14:40 +00:00
|
|
|
|
2016-09-23 03:11:18 +00:00
|
|
|
// Query for the VIEWS
|
2020-03-20 13:16:46 +00:00
|
|
|
$res = $this->query(
|
|
|
|
|
'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"',
|
|
|
|
|
$fname,
|
|
|
|
|
self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
|
|
|
|
|
);
|
|
|
|
|
|
2016-09-23 03:11:18 +00:00
|
|
|
$allViews = [];
|
|
|
|
|
foreach ( $res as $row ) {
|
|
|
|
|
array_push( $allViews, $row->$propertyName );
|
2013-05-24 15:14:40 +00:00
|
|
|
}
|
|
|
|
|
|
2020-01-09 23:48:34 +00:00
|
|
|
if ( $prefix === null || $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
|
2018-06-26 21:14:43 +00:00
|
|
|
* @param string|null $prefix
|
2013-05-24 15:14:40 +00:00
|
|
|
* @return bool
|
|
|
|
|
* @since 1.22
|
|
|
|
|
*/
|
|
|
|
|
public function isView( $name, $prefix = null ) {
|
2020-06-07 19:06:40 +00:00
|
|
|
return in_array( $name, $this->listViews( $prefix, __METHOD__ ) );
|
2013-05-24 15:14:40 +00:00
|
|
|
}
|
2017-03-30 11:29:35 +00:00
|
|
|
|
2018-02-14 08:27:14 +00:00
|
|
|
protected function isTransactableQuery( $sql ) {
|
|
|
|
|
return parent::isTransactableQuery( $sql ) &&
|
|
|
|
|
!preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\(/', $sql );
|
|
|
|
|
}
|
2018-03-04 13:50:28 +00:00
|
|
|
|
2019-02-20 19:41:55 +00:00
|
|
|
public function buildStringCast( $field ) {
|
|
|
|
|
return "CAST( $field AS BINARY )";
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-04 13:50:28 +00:00
|
|
|
/**
|
|
|
|
|
* @param string $field Field or column to cast
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function buildIntegerCast( $field ) {
|
|
|
|
|
return 'CAST( ' . $field . ' AS SIGNED )';
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-04 03:14:08 +00:00
|
|
|
public function selectSQLText(
|
|
|
|
|
$table,
|
|
|
|
|
$vars,
|
|
|
|
|
$conds = '',
|
|
|
|
|
$fname = __METHOD__,
|
|
|
|
|
$options = [],
|
|
|
|
|
$join_conds = []
|
|
|
|
|
) {
|
|
|
|
|
$sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
|
|
|
|
|
// https://dev.mysql.com/doc/refman/5.7/en/optimizer-hints.html
|
|
|
|
|
// https://mariadb.com/kb/en/library/aborting-statements/
|
|
|
|
|
$timeoutMsec = intval( $options['MAX_EXECUTION_TIME'] ?? 0 );
|
|
|
|
|
if ( $timeoutMsec > 0 ) {
|
|
|
|
|
list( $vendor, $number ) = $this->getMySqlServerVariant();
|
|
|
|
|
if ( $vendor === 'MariaDB' && version_compare( $number, '10.1.2', '>=' ) ) {
|
|
|
|
|
$timeoutSec = $timeoutMsec / 1000;
|
|
|
|
|
$sql = "SET STATEMENT max_statement_time=$timeoutSec FOR $sql";
|
|
|
|
|
} elseif ( $vendor === 'MySQL' && version_compare( $number, '5.7.0', '>=' ) ) {
|
|
|
|
|
$sql = preg_replace(
|
|
|
|
|
'/^SELECT(?=\s)/',
|
|
|
|
|
"SELECT /*+ MAX_EXECUTION_TIME($timeoutMsec)*/",
|
|
|
|
|
$sql
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $sql;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
2018-04-03 00:36:22 +00:00
|
|
|
* @return bool Whether GTID support is used (mockable for testing)
|
|
|
|
|
*/
|
|
|
|
|
protected function useGTIDs() {
|
|
|
|
|
return $this->useGTIDs;
|
|
|
|
|
}
|
2013-05-03 01:39:16 +00:00
|
|
|
}
|
2017-02-07 04:49:57 +00:00
|
|
|
|
2018-05-29 16:21:31 +00:00
|
|
|
/**
|
|
|
|
|
* @deprecated since 1.29
|
|
|
|
|
*/
|
2017-02-07 04:49:57 +00:00
|
|
|
class_alias( DatabaseMysqlBase::class, 'DatabaseMysqlBase' );
|