*/
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
+use Wikimedia\ScopedCallback;
/**
* Relational database abstraction object
/** @var string SQL query */
protected $mLastQuery = '';
- /** @var bool */
- protected $mDoneWrites = false;
+ /** @var float|bool UNIX timestamp of last write query */
+ protected $mLastWriteTime = false;
/** @var string|bool */
protected $mPHPError = false;
/** @var string */
/** @var callback Error logging callback */
protected $errorLogger;
- /** @var resource Database connection */
+ /** @var resource|null Database connection */
protected $mConn = null;
/** @var bool */
protected $mOpened = false;
}
if ( !isset( $p['errorLogger'] ) ) {
$p['errorLogger'] = function ( Exception $e ) {
- trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
+ trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
};
}
}
public function doneWrites() {
- return (bool)$this->mDoneWrites;
+ return (bool)$this->mLastWriteTime;
}
public function lastDoneWrites() {
- return $this->mDoneWrites ?: false;
+ return $this->mLastWriteTime ?: false;
}
public function writesPending() {
return !!( $this->mFlags & $flag );
}
+ /**
+ * @param string $name Class field name
+ * @return mixed
+ * @deprecated Since 1.28
+ */
public function getProperty( $name ) {
return $this->$name;
}
if ( $this->htmlErrors !== false ) {
ini_set( 'html_errors', $this->htmlErrors );
}
+
+ return $this->getLastPHPError();
+ }
+
+ /**
+ * @return string|bool Last PHP error for this DB (typically connection errors)
+ */
+ protected function getLastPHPError() {
if ( $this->mPHPError ) {
$error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
$error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
return $error;
- } else {
- return false;
}
+
+ return false;
}
/**
* @return bool
*/
protected function isTransactableQuery( $sql ) {
- $verb = $this->getQueryVerb( $sql );
- return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
+ return !in_array(
+ $this->getQueryVerb( $sql ),
+ [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET', 'CREATE', 'ALTER' ],
+ true
+ );
}
/**
throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
}
# Set a flag indicating that writes have been done
- $this->mDoneWrites = microtime( true );
+ $this->mLastWriteTime = microtime( true );
}
// Add trace comment to the begin of the sql string, right after the operator.
if ( false === $ret ) {
# Deadlocks cause the entire transaction to abort, not just the statement.
- # http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
+ # https://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
# https://www.postgresql.org/docs/9.1/static/explicit-locking.html
if ( $this->wasDeadlock() ) {
if ( $this->explicitTrxActive() || $priorWritesPending ) {
} elseif ( count( $dbDetails ) == 2 ) {
list( $database, $table ) = $dbDetails;
# We don't want any prefix added in this case
+ $prefix = '';
# In dbs that support it, $database may actually be the schema
# but that doesn't affect any of the functionality here
- $prefix = '';
$schema = '';
} else {
list( $table ) = $dbDetails;
# Quote $table and apply the prefix if not quoted.
# $tableName might be empty if this is called from Database::replaceVars()
$tableName = "{$prefix}{$table}";
- if ( $format == 'quoted'
- && !$this->isQuotedIdentifier( $tableName ) && $tableName !== ''
+ if ( $format === 'quoted'
+ && !$this->isQuotedIdentifier( $tableName )
+ && $tableName !== ''
) {
$tableName = $this->addIdentifierQuotes( $tableName );
}
- # Quote $schema and merge it with the table name if needed
- if ( strlen( $schema ) ) {
- if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
- $schema = $this->addIdentifierQuotes( $schema );
- }
- $tableName = $schema . '.' . $tableName;
- }
+ # Quote $schema and $database and merge them with the table name if needed
+ $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
+ $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
- # Quote $database and merge it with the table name if needed
- if ( $database !== '' ) {
- if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
- $database = $this->addIdentifierQuotes( $database );
+ return $tableName;
+ }
+
+ /**
+ * @param string|null $namespace Database or schema
+ * @param string $relation Name of table, view, sequence, etc...
+ * @param string $format One of (raw, quoted)
+ * @return string Relation name with quoted and merged $namespace as needed
+ */
+ private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
+ if ( strlen( $namespace ) ) {
+ if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
+ $namespace = $this->addIdentifierQuotes( $namespace );
}
- $tableName = $database . '.' . $tableName;
+ $relation = $namespace . '.' . $relation;
}
- return $tableName;
+ return $relation;
}
public function tableNames() {
$this->mTrxTimestamp = microtime( true );
$this->mTrxFname = $fname;
$this->mTrxDoneWrites = false;
- $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
$this->mTrxAutomaticAtomic = false;
$this->mTrxAtomicLevels = [];
$this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
// as lag itself just to be safe
$status = $this->getApproximateLagStatus();
$this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
+ // T147697: make explicitTrxActive() return true until begin() finishes. This way, no
+ // caller will think its OK to muck around with the transaction just because startAtomic()
+ // has not yet completed (e.g. setting mTrxAtomicLevels).
+ $this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
}
/**
$writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
$this->doCommit( $fname );
if ( $this->mTrxDoneWrites ) {
- $this->mDoneWrites = microtime( true );
+ $this->mLastWriteTime = microtime( true );
$this->trxProfiler->transactionWritingOut(
$this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
}
$fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
throw new DBUnexpectedError(
$this,
- "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
+ "$fname: Cannot flush snapshot because writes are pending ($fnames)."
);
}
$fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
throw new DBUnexpectedError(
$this,
- "$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
+ "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
);
}
return true;
}
+ /**
+ * Get the underlying binding handle, mConn
+ *
+ * Makes sure that mConn is set (disconnects and ping() failure can unset it).
+ * This catches broken callers than catch and ignore disconnection exceptions.
+ * Unlike checking isOpen(), this is safe to call inside of open().
+ *
+ * @return resource|object
+ * @throws DBUnexpectedError
+ * @since 1.26
+ */
+ protected function getBindingHandle() {
+ if ( !$this->mConn ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'DB connection was already closed or the connection dropped.'
+ );
+ }
+
+ return $this->mConn;
+ }
+
/**
* @since 1.19
* @return string
}
if ( $this->mConn ) {
- // Avoid connection leaks for sanity
+ // Avoid connection leaks for sanity. Normally, resources close at script completion.
+ // The connection might already be closed in zend/hhvm by now, so suppress warnings.
+ \MediaWiki\suppressWarnings();
$this->closeConnection();
+ \MediaWiki\restoreWarnings();
$this->mConn = false;
$this->mOpened = false;
}