private $serverVersion = null;
/** @var bool|null */
private $insertSelectIsSafe = null;
+ /** @var stdClass|null */
+ private $replicationInfoRow = null;
+
+ // Cache getServerId() for 24 hours
+ const SERVER_ID_CACHE_TTL = 86400;
/**
* Additional $params include:
return $this->nativeReplace( $table, $rows, $fname );
}
- protected function nativeInsertSelect(
- $destTable, $srcTable, $varMap, $conds,
- $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
- ) {
- $isSafe = in_array( 'NO_AUTO_COLUMNS', $insertOptions, true );
- if ( !$isSafe && $this->insertSelectIsSafe === null ) {
- // 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.
- $row = $this->selectRow(
+ 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.
+ return (
+ in_array( 'NO_AUTO_COLUMNS', $insertOptions ) ||
+ (int)$row->innodb_autoinc_lock_mode === 0
+ );
+ }
+
+ /**
+ * @return stdClass Process cached row
+ */
+ protected function getReplicationSafetyInfo() {
+ if ( $this->replicationInfoRow === null ) {
+ $this->replicationInfoRow = $this->selectRow(
false,
[
'innodb_autoinc_lock_mode' => '@@innodb_autoinc_lock_mode',
[],
__METHOD__
);
- $this->insertSelectIsSafe = $row->binlog_format === 'ROW' ||
- (int)$row->innodb_autoinc_lock_mode === 0;
- }
-
- if ( !$isSafe && !$this->insertSelectIsSafe ) {
- return $this->nonNativeInsertSelect(
- $destTable,
- $srcTable,
- $varMap,
- $conds,
- $fname,
- $insertOptions,
- $selectOptions,
- $selectJoinConds
- );
- } else {
- return parent::nativeInsertSelect(
- $destTable,
- $srcTable,
- $varMap,
- $conds,
- $fname,
- $insertOptions,
- $selectOptions,
- $selectJoinConds
- );
}
+
+ return $this->replicationInfoRow;
}
/**
* Takes same arguments as Database::select()
*
* @param string|array $table
- * @param string|array $vars
+ * @param string|array $var
* @param string|array $conds
* @param string $fname
* @param string|array $options
+ * @param array $join_conds
* @return bool|int
*/
- public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = __METHOD__, $options = []
+ public function estimateRowCount( $table, $var = '*', $conds = '',
+ $fname = __METHOD__, $options = [], $join_conds = []
) {
+ $conds = $this->normalizeConditions( $conds, $fname );
+ $column = $this->extractSingleFieldFromList( $var );
+ if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
+ $conds[] = "$column IS NOT NULL";
+ }
+
$options['EXPLAIN'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
+ $res = $this->select( $table, $var, $conds, $fname, $options, $join_conds );
if ( $res === false ) {
return false;
}
}
}
- return empty( $result ) ? false : $result;
+ return $result ?: false;
}
/**
protected function getLagFromSlaveStatus() {
$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
$row = $res ? $res->fetchObject() : false;
+ // If the server is not replicating, there will be no row
if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
return intval( $row->Seconds_Behind_Master );
}
// 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"
+ "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1",
+ __METHOD__
);
$row = $res ? $res->fetchObject() : false;
} finally {
$rpos = $this->getReplicaPos();
$gtidsWait = $rpos ? MySQLMasterPos::getCommonDomainGTIDs( $pos, $rpos ) : [];
if ( !$gtidsWait ) {
+ $this->queryLogger->error(
+ "No GTIDs with the same domain between master ($pos) and replica ($rpos)",
+ $this->getLogContext( [
+ 'method' => __METHOD__,
+ ] )
+ );
+
return -1; // $pos is from the wrong cluster?
}
// Wait on the GTID set (MariaDB only)
$gtidArg = $this->addQuotes( implode( ',', $gtidsWait ) );
- $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+ if ( strpos( $gtidArg, ':' ) !== false ) {
+ // MySQL GTIDs, e.g "source_id:transaction_id"
+ $res = $this->doQuery( "SELECT WAIT_FOR_EXECUTED_GTID_SET($gtidArg, $timeout)" );
+ } else {
+ // MariaDB GTIDs, e.g."domain:server:sequence"
+ $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
+ }
} else {
// Wait on the binlog coordinates
$encFile = $this->addQuotes( $pos->getLogFile() );
- $encPos = intval( $pos->pos[1] );
+ $encPos = intval( $pos->getLogPosition()[$pos::CORD_EVENT] );
$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
}
$row = $res ? $this->fetchRow( $res ) : false;
if ( !$row ) {
- throw new DBExpectedError( $this,
- "MASTER_POS_WAIT() or MASTER_GTID_WAIT() failed: {$this->lastError()}" );
+ throw new DBExpectedError( $this, "Replication wait failed: {$this->lastError()}" );
}
// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
* @return MySQLMasterPos|bool
*/
public function getReplicaPos() {
- $now = microtime( true );
-
- if ( $this->useGTIDs ) {
- $res = $this->query( "SELECT @@global.gtid_slave_pos AS Value", __METHOD__ );
- $gtidRow = $this->fetchObject( $res );
- if ( $gtidRow && strlen( $gtidRow->Value ) ) {
- return new MySQLMasterPos( $gtidRow->Value, $now );
+ $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_current_pos for MariaDB and gtid_executed for MySQL
+ foreach ( [ 'gtid_current_pos', 'gtid_executed' ] as $name ) {
+ if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
+ return new MySQLMasterPos( $data[$name], $now );
+ }
}
}
- $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
- $row = $this->fetchObject( $res );
- if ( $row && strlen( $row->Relay_Master_Log_File ) ) {
+ $data = $this->getServerRoleStatus( 'SLAVE', __METHOD__ );
+ if ( $data && strlen( $data['Relay_Master_Log_File'] ) ) {
return new MySQLMasterPos(
- "{$row->Relay_Master_Log_File}/{$row->Exec_Master_Log_Pos}",
+ "{$data['Relay_Master_Log_File']}/{$data['Exec_Master_Log_Pos']}",
$now
);
}
* @return MySQLMasterPos|bool
*/
public function getMasterPos() {
- $now = microtime( true );
+ $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_current_pos for MariaDB and gtid_executed for MySQL
+ foreach ( [ 'gtid_current_pos', 'gtid_executed' ] as $name ) {
+ if ( isset( $data[$name] ) && strlen( $data[$name] ) ) {
+ $pos = new MySQLMasterPos( $data[$name], $now );
+ 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'] );
+ }
+ }
+ }
- if ( $this->useGTIDs ) {
- $res = $this->query( "SELECT @@global.gtid_binlog_pos AS Value", __METHOD__ );
- $gtidRow = $this->fetchObject( $res );
- if ( $gtidRow && strlen( $gtidRow->Value ) ) {
- return new MySQLMasterPos( $gtidRow->Value, $now );
+ if ( !$pos ) {
+ $data = $this->getServerRoleStatus( 'MASTER', __METHOD__ );
+ if ( $data && strlen( $data['File'] ) ) {
+ $pos = new MySQLMasterPos( "{$data['File']}/{$data['Position']}", $now );
}
}
- $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
- $row = $this->fetchObject( $res );
- if ( $row && strlen( $row->File ) ) {
- return new MySQLMasterPos( "{$row->File}/{$row->Position}", $now );
+ return $pos;
+ }
+
+ /**
+ * @return int
+ * @throws DBQueryError If the variable doesn't exist for some reason
+ */
+ protected function getServerId() {
+ return $this->srvCache->getWithSetCallback(
+ $this->srvCache->makeGlobalKey( 'mysql-server-id', $this->getServer() ),
+ self::SERVER_ID_CACHE_TTL,
+ function () {
+ $res = $this->query( "SELECT @@server_id AS id", __METHOD__ );
+ return intval( $this->fetchObject( $res )->id );
+ }
+ );
+ }
+
+ /**
+ * @return string|null
+ */
+ protected function getServerUUID() {
+ return $this->srvCache->getWithSetCallback(
+ $this->srvCache->makeGlobalKey( 'mysql-server-uuid', $this->getServer() ),
+ self::SERVER_ID_CACHE_TTL,
+ function () {
+ $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'server_uuid'" );
+ $row = $this->fetchObject( $res );
+
+ return $row ? $row->Value : null;
+ }
+ );
+ }
+
+ /**
+ * @param string $fname
+ * @return string[]
+ */
+ protected function getServerGTIDs( $fname = __METHOD__ ) {
+ $map = [];
+ // Get global-only variables like gtid_executed
+ $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_%'", $fname );
+ 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)
+ $res = $this->query( "SHOW SESSION VARIABLES LIKE 'gtid_%'", $fname );
+ foreach ( $res as $row ) {
+ $map[$row->Variable_name] = $row->Value;
}
- return false;
+ 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__ ) {
+ return $this->query( "SHOW $role STATUS", $fname )->fetchRow() ?: [];
}
public function serverIsReadOnly() {
return $this->lastErrno() == 1205;
}
- public function wasErrorReissuable() {
- return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
- }
-
/**
* Determines if the last failure was due to the database being read-only.
*
return in_array( $name, $this->listViews( $prefix ) );
}
+ protected function isTransactableQuery( $sql ) {
+ return parent::isTransactableQuery( $sql ) &&
+ !preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\(/', $sql );
+ }
+
/**
- * Allows for index remapping in queries where this is not consistent across DBMS
- *
- * @param string $index
+ * @param string $field Field or column to cast
* @return string
*/
- protected function indexName( $index ) {
- /**
- * 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).
- */
- $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;
- }
+ public function buildIntegerCast( $field ) {
+ return 'CAST( ' . $field . ' AS SIGNED )';
}
- protected function isTransactableQuery( $sql ) {
- return parent::isTransactableQuery( $sql ) &&
- !preg_match( '/^SELECT\s+(GET|RELEASE|IS_FREE)_LOCK\(/', $sql );
+ /*
+ * @return bool Whether GTID support is used (mockable for testing)
+ */
+ protected function useGTIDs() {
+ return $this->useGTIDs;
}
}