25use InvalidArgumentException;
29use Wikimedia\AtEase\AtEase;
83 private const LAG_STALE_WARN_THRESHOLD = 0.100;
105 $this->lagDetectionMethod = $params[
'lagDetectionMethod'] ??
'Seconds_Behind_Master';
106 $this->lagDetectionOptions = $params[
'lagDetectionOptions'] ?? [];
107 $this->
useGTIDs = !empty( $params[
'useGTIDs' ] );
108 foreach ( [
'KeyPath',
'CertPath',
'CAFile',
'CAPath',
'Ciphers' ] as $name ) {
110 if ( isset( $params[$var] ) ) {
111 $this->$var = $params[$var];
114 $this->sqlMode = $params[
'sqlMode'] ??
null;
115 $this->utf8Mode = !empty( $params[
'utf8Mode'] );
116 $this->insertSelectIsSafe = isset( $params[
'insertSelectIsSafe'] )
117 ? (bool)$params[
'insertSelectIsSafe'] :
null;
119 parent::__construct( $params );
130 $this->
close( __METHOD__ );
132 if ( $schema !==
null ) {
139 }
catch ( RuntimeException $e ) {
145 if ( !$this->conn ) {
151 $db && strlen( $db ) ? $db :
null,
156 $set = [
'group_concat_max_len = 262144' ];
158 if ( is_string( $this->sqlMode ) ) {
159 $set[] =
'sql_mode = ' . $this->
addQuotes( $this->sqlMode );
163 foreach ( $this->connectionVariables as $var => $val ) {
165 if ( !is_int( $val ) && !is_float( $val ) ) {
175 'SET ' . implode(
', ', $set ),
177 self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX
180 }
catch ( RuntimeException $e ) {
189 __CLASS__ .
": domain '{$domain->getId()}' has a schema component"
195 if ( $database ===
null ) {
197 $this->currentDomain->getDatabase(),
205 if ( $database !== $this->
getDBname() ) {
207 list(
$res, $err, $errno ) =
208 $this->
executeQuery( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
210 if (
$res ===
false ) {
217 $this->currentDomain = $domain;
266 # Even if it's non-zero, it can still be invalid
267 AtEase::suppressWarnings();
272 AtEase::restoreWarnings();
294 return in_array( $errno, [ 2062, 3024 ] );
300 if ( $row->binlog_format ===
'ROW' ) {
304 if ( isset( $selectOptions[
'LIMIT'] ) ) {
315 in_array(
'NO_AUTO_COLUMNS', $insertOptions ) ||
316 (
int)$row->innodb_autoinc_lock_mode === 0
324 if ( $this->replicationInfoRow ===
null ) {
325 $this->replicationInfoRow = $this->
selectRow(
328 'innodb_autoinc_lock_mode' =>
'@@innodb_autoinc_lock_mode',
329 'binlog_format' =>
'@@binlog_format',
362 if ( is_string( $column ) && !in_array( $column, [
'*',
'1' ] ) ) {
363 $conds[] =
"$column IS NOT NULL";
366 $options[
'EXPLAIN'] =
true;
367 $res = $this->
select( $tables, $var, $conds, $fname, $options, $join_conds );
368 if (
$res ===
false ) {
376 foreach (
$res as $plan ) {
377 $rows *= $plan->rows > 0 ? $plan->rows : 1;
387 $tableName =
"{$prefix}{$table}";
389 if ( isset( $this->sessionTempTables[$tableName] ) ) {
398 if ( $database !==
'' ) {
400 $sql =
"SHOW TABLES FROM $encDatabase LIKE '$encLike'";
402 $sql =
"SHOW TABLES LIKE '$encLike'";
408 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
411 return $res->numRows() > 0;
421 "SELECT * FROM " . $this->
tableName( $table ) .
" LIMIT 1",
423 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
429 '@phan-var MysqliResultWrapper $res';
430 return $res->getInternalFieldInfo( $field );
442 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
447 'SHOW INDEX FROM ' . $this->
tableName( $table ),
449 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
458 foreach (
$res as $row ) {
459 if ( $row->Key_name == $index ) {
464 return $result ?:
false;
490 return '`' . str_replace( [
"\0",
'`' ], [
'',
'``' ],
$s ) .
'`';
498 return strlen( $name ) && $name[0] ==
'`' && substr( $name, -1, 1 ) ==
'`';
523 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
525 $row =
$res ?
$res->fetchObject() :
false;
527 if ( $row && strval( $row->Seconds_Behind_Master ) !==
'' ) {
530 return intval( $row->Seconds_Behind_Master + ( $row->SQL_Remaining_Delay ?? 0 ) );
543 if ( $currentTrxInfo ) {
545 $staleness = microtime(
true ) - $currentTrxInfo[
'since'];
546 if ( $staleness > self::LAG_STALE_WARN_THRESHOLD ) {
549 $this->queryLogger->warning(
550 "Using cached lag value for {db_server} due to active transaction",
552 'method' => __METHOD__,
554 'exception' =>
new RuntimeException()
559 return $currentTrxInfo[
'lag'];
562 if ( isset( $options[
'conds'] ) ) {
568 if ( !$masterInfo ) {
569 $this->queryLogger->error(
570 "Unable to query primary of {db_server} for server ID",
572 'method' => __METHOD__
579 $conds = [
'server_id' => intval( $masterInfo[
'serverId'] ) ];
583 if ( $ago !==
null ) {
584 return max( $ago, 0.0 );
587 $this->queryLogger->error(
588 "Unable to find pt-heartbeat row for {db_server}",
590 'method' => __METHOD__
607 return $cache->getWithSetCallback(
609 $cache::TTL_INDEFINITE,
610 function () use (
$cache, $key, $fname ) {
612 if ( !
$cache->lock( $key, 0, 10 ) ) {
616 $conn = $this->getLazyMasterHandle();
621 $flags = self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
625 $row =
$res ?
$res->fetchObject() :
false;
626 $id = $row ? (int)$row->id : 0;
632 return $id ? [
'serverId' => $id,
'asOf' => time() ] :
false;
648 $whereSQL = $this->
makeList( $conds, self::LIST_AND );
652 "SELECT TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6)) AS us_ago " .
653 "FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
655 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
657 $row =
$res ?
$res->fetchObject() :
false;
659 return $row ? ( $row->us_ago / 1e6 ) :
null;
667 return parent::getApproximateLagStatus();
670 $key = $this->srvCache->makeGlobalKey(
'mysql-lag', $this->
getServerName() );
671 $approxLag = $this->srvCache->get( $key );
673 $approxLag = parent::getApproximateLagStatus();
674 $this->srvCache->set( $key, $approxLag, 1 );
682 throw new InvalidArgumentException(
"Position not an instance of MySQLPrimaryPos" );
685 if ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
686 $this->queryLogger->debug(
687 "Bypassed replication wait; database has a static dataset",
688 $this->
getLogContext( [
'method' => __METHOD__,
'raw_pos' => $pos ] )
692 } elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
693 $this->queryLogger->debug(
694 "Bypassed replication wait; replication known to have reached {raw_pos}",
695 $this->
getLogContext( [
'method' => __METHOD__,
'raw_pos' => $pos ] )
702 if ( $pos->getGTIDs() ) {
706 $this->queryLogger->error(
707 "Could not get replication position on replica DB to compare to {raw_pos}",
708 $this->
getLogContext( [
'method' => __METHOD__,
'raw_pos' => $pos ] )
714 $gtidsWait = $pos::getRelevantActiveGTIDs( $pos, $refPos );
716 $this->queryLogger->error(
717 "No active GTIDs in {raw_pos} share a domain with those in {current_pos}",
719 'method' => __METHOD__,
721 'current_pos' => $refPos
728 $gtidArg = $this->
addQuotes( implode(
',', $gtidsWait ) );
729 if ( strpos( $gtidArg,
':' ) !==
false ) {
731 $sql =
"SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)";
734 $sql =
"SELECT MASTER_GTID_WAIT($gtidArg, $timeout)";
736 $waitPos = implode(
',', $gtidsWait );
739 $encFile = $this->
addQuotes( $pos->getLogFile() );
741 $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
742 $sql =
"SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
746 $start = microtime(
true );
747 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
750 $seconds = max( microtime(
true ) - $start, 0 );
753 $status = ( $row[0] !== null ) ? intval( $row[0] ) :
null;
754 if ( $status ===
null ) {
755 $this->replLogger->error(
756 "An error occurred while waiting for replication to reach {wait_pos}",
759 'wait_pos' => $waitPos,
761 'seconds_waited' => $seconds,
762 'exception' =>
new RuntimeException()
765 } elseif ( $status < 0 ) {
766 $this->replLogger->error(
767 "Timed out waiting for replication to reach {wait_pos}",
770 'wait_pos' => $waitPos,
771 'timeout' => $timeout,
773 'seconds_waited' => $seconds,
774 'exception' =>
new RuntimeException()
777 } elseif ( $status >= 0 ) {
778 $this->replLogger->debug(
779 "Replication has reached {wait_pos}",
782 'wait_pos' => $waitPos,
783 'seconds_waited' => $seconds,
787 $this->lastKnownReplicaPos = $pos;
799 $now = microtime(
true );
805 foreach ( [
'gtid_slave_pos',
'gtid_executed' ] as $name ) {
806 if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
813 if ( $data && strlen( $data[
'Relay_Master_Log_File'] ) ) {
815 "{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
829 $now = microtime(
true );
836 foreach ( [
'gtid_binlog_pos',
'gtid_executed' ] as $name ) {
837 if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
844 $pos->setActiveOriginServerId( $this->
getServerId() );
846 if ( isset( $data[
'gtid_domain_id'] ) ) {
847 $pos->setActiveDomain( $data[
'gtid_domain_id'] );
854 if ( $data && strlen( $data[
'File'] ) ) {
855 $pos =
new MySQLPrimaryPos(
"{$data['File']}/{$data['Position']}", $now );
881 return $this->srvCache->getWithSetCallback(
882 $this->srvCache->makeGlobalKey(
'mysql-server-id', $this->getServerName() ),
883 self::SERVER_ID_CACHE_TTL,
884 function () use ( $fname ) {
885 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
899 return $this->srvCache->getWithSetCallback(
900 $this->srvCache->makeGlobalKey(
'mysql-server-uuid', $this->getServerName() ),
901 self::SERVER_ID_CACHE_TTL,
902 function () use ( $fname ) {
903 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
904 $res = $this->
query(
"SHOW GLOBAL VARIABLES LIKE 'server_uuid'", $fname,
$flags );
907 return $row ? $row->Value :
null;
919 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
922 $res = $this->
query(
"SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname,
$flags );
923 foreach (
$res as $row ) {
924 $map[$row->Variable_name] = $row->Value;
927 $res = $this->
query(
"SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname,
$flags );
928 foreach (
$res as $row ) {
929 $map[$row->Variable_name] = $row->Value;
941 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
944 return $res->fetchRow() ?: [];
949 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
950 $res = $this->
query(
"SELECT @@GLOBAL.read_only AS Value", __METHOD__,
$flags );
953 return $row ? (bool)$row->Value :
false;
961 return "FORCE INDEX (" . $this->
indexName( $index ) .
")";
969 return "IGNORE INDEX (" . $this->
indexName( $index ) .
")";
977 if ( $variant ===
'MariaDB' ) {
978 return '[{{int:version-db-mariadb-url}} MariaDB]';
981 return '[{{int:version-db-mysql-url}} MySQL]';
994 $parts = explode(
'-', $version, 2 );
996 $suffix = $parts[1] ??
'';
997 if ( strpos( $suffix,
'MariaDB' ) !==
false || strpos( $suffix,
'-maria-' ) !==
false ) {
1003 return [ $vendor, $number ];
1011 $fname = __METHOD__;
1014 $cache->makeGlobalKey(
'mysql-server-version', $this->getServerName() ),
1016 function () use ( $fname ) {
1020 return $this->
selectField(
'',
'VERSION()',
'', $fname );
1029 if ( isset( $options[
'connTimeout'] ) ) {
1030 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_TRX;
1031 $timeout = (int)$options[
'connTimeout'];
1032 $this->
query(
"SET net_read_timeout=$timeout", __METHOD__,
$flags );
1033 $this->
query(
"SET net_write_timeout=$timeout", __METHOD__,
$flags );
1043 if ( preg_match(
'/^DELIMITER\s+(\S+)/i', $newLine, $m ) ) {
1044 $this->delimiter = $m[1];
1048 return parent::streamStatementEnd( $sql, $newLine );
1055 "SELECT IS_FREE_LOCK($encName) AS unlocked",
1057 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1061 return ( $row->unlocked == 1 );
1064 public function doLock(
string $lockName,
string $method,
int $timeout ) {
1071 "SELECT IF(GET_LOCK($encName,$timeout),UNIX_TIMESTAMP(SYSDATE(6)),NULL) AS acquired",
1073 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1077 return ( $row->acquired !==
null ) ? (float)$row->acquired :
null;
1080 public function doUnlock(
string $lockName,
string $method ) {
1084 "SELECT RELEASE_LOCK($encName) AS released",
1086 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1090 return ( $row->released == 1 );
1096 return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
1109 foreach ( $write as $table ) {
1110 $items[] = $this->
tableName( $table ) .
' WRITE';
1112 foreach ( $read as $table ) {
1113 $items[] = $this->
tableName( $table ) .
' READ';
1117 "LOCK TABLES " . implode(
',', $items ),
1119 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS
1129 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_ROWS
1139 if ( $value ===
'default' ) {
1140 if ( $this->defaultBigSelects ===
null ) {
1141 # Function hasn't been called before so it must already be set to the default
1146 } elseif ( $this->defaultBigSelects ===
null ) {
1147 $this->defaultBigSelects =
1148 (bool)$this->
selectField(
false,
'@@sql_big_selects',
'', __METHOD__ );
1152 "SET sql_big_selects=" . ( $value ?
'1' :
'0' ),
1154 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_TRX
1169 $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
1175 $delTable = $this->
tableName( $delTable );
1176 $joinTable = $this->
tableName( $joinTable );
1177 $sql =
"DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
1179 if ( $conds !=
'*' ) {
1180 $sql .=
' AND ' . $this->
makeList( $conds, self::LIST_AND );
1183 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
1195 $sqlColumnAssignments = $this->
makeList( $set, self::LIST_SET );
1198 "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples " .
1199 "ON DUPLICATE KEY UPDATE $sqlColumnAssignments";
1201 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
1204 protected function doReplace( $table, array $identityKey, array $rows, $fname ) {
1208 $sql =
"REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples";
1210 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
1221 return (
int)$vars[
'Uptime'];
1249 ( $this->
lastErrno() == 1290 && strpos( $this->
lastError(),
'--read-only' ) !== false );
1253 return $errno == 2013 || $errno == 2006;
1259 if ( $errno === 1205 ) {
1262 "SELECT @@innodb_rollback_on_timeout AS Value",
1264 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1266 $row =
$res ?
$res->fetchObject() :
false;
1269 return ( $row && !$row->Value );
1273 return in_array( $errno, [ 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ],
true );
1284 $oldName, $newName, $temporary =
false, $fname = __METHOD__
1286 $tmp = $temporary ?
'TEMPORARY ' :
'';
1290 return $this->
query(
1291 "CREATE $tmp TABLE $newName (LIKE $oldName)",
1293 self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA
1304 public function listTables( $prefix =
null, $fname = __METHOD__ ) {
1305 $result = $this->
query(
1308 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1313 foreach ( $result as $table ) {
1314 $vars = get_object_vars( $table );
1315 $table = array_pop( $vars );
1317 if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
1318 $endArray[] = $table;
1333 "SHOW STATUS LIKE '{$which}'",
1335 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1339 foreach (
$res as $row ) {
1340 $status[$row->Variable_name] = $row->Value;
1355 public function listViews( $prefix =
null, $fname = __METHOD__ ) {
1357 $propertyName =
'Tables_in_' . $this->
getDBname();
1361 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"',
1363 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE
1367 foreach (
$res as $row ) {
1368 array_push( $allViews, $row->$propertyName );
1371 if ( $prefix ===
null || $prefix ===
'' ) {
1375 $filteredViews = [];
1376 foreach ( $allViews as $viewName ) {
1378 if ( strpos( $viewName, $prefix ) === 0 ) {
1379 array_push( $filteredViews, $viewName );
1383 return $filteredViews;
1394 public function isView( $name, $prefix =
null ) {
1395 return in_array( $name, $this->
listViews( $prefix, __METHOD__ ) );
1399 return parent::isTransactableQuery( $sql ) &&
1400 !preg_match(
'/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\(/', $sql );
1404 return "CAST( $field AS BINARY )";
1412 return 'CAST( ' . $field .
' AS SIGNED )';
1426class_alias( DatabaseMysqlBase::class,
'DatabaseMysqlBase' );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
getWithSetCallback( $key, $exptime, $callback, $flags=0)
Get an item with the given key, regenerating and setting it if not found.
makeGlobalKey( $collection,... $components)
Make a cache key for the default keyspace and given components.
Class to handle database/schema/prefix specifications for IDatabase.
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s