* @ingroup Database
*/
-/** Number of times to re-try an operation in case of deadlock */
-define( 'DEADLOCK_TRIES', 4 );
-/** Minimum time to wait before retry, in microseconds */
-define( 'DEADLOCK_DELAY_MIN', 500000 );
-/** Maximum time to wait before retry */
-define( 'DEADLOCK_DELAY_MAX', 1500000 );
-
/**
* Base interface for all DBMS-specific code. At a bare minimum, all of the
* following must be implemented to support MediaWiki
* @param string $fname Calling function name
* @return Mixed: Database-specific index description class or false if the index does not exist
*/
- function indexInfo( $table, $index, $fname = 'Database::indexInfo' );
+ function indexInfo( $table, $index, $fname = __METHOD__ );
/**
* Get the number of rows affected by the last write query
* @ingroup Database
*/
abstract class DatabaseBase implements DatabaseType {
+ /** Number of times to re-try an operation in case of deadlock */
+ const DEADLOCK_TRIES = 4;
+ /** Minimum time to wait before retry, in microseconds */
+ const DEADLOCK_DELAY_MIN = 500000;
+ /** Maximum time to wait before retry */
+ const DEADLOCK_DELAY_MAX = 1500000;
# ------------------------------------------------------------------------------
# Variables
protected $mConn = null;
protected $mOpened = false;
- /**
- * @since 1.20
- * @var array of Closure
- */
+ /** @var array of Closure */
protected $mTrxIdleCallbacks = array();
+ /** @var array of Closure */
+ protected $mTrxPreCommitCallbacks = array();
protected $mTablePrefix;
protected $mFlags;
+ protected $mForeign;
protected $mTrxLevel = 0;
protected $mErrorCount = 0;
protected $mLBInfo = array();
/**
* Returns true if there is a transaction open with possible write
- * queries or transaction idle callbacks waiting on it to finish.
+ * queries or transaction pre-commit/idle callbacks waiting on it to finish.
*
* @return bool
*/
public function writesOrCallbacksPending() {
- return $this->mTrxLevel && ( $this->mTrxDoneWrites || $this->mTrxIdleCallbacks );
+ return $this->mTrxLevel && (
+ $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
+ );
}
/**
public function setFlag( $flag ) {
global $wgDebugDBTransactions;
$this->mFlags |= $flag;
- if ( ( $flag & DBO_TRX) & $wgDebugDBTransactions ) {
+ if ( ( $flag & DBO_TRX ) & $wgDebugDBTransactions ) {
wfDebug( "Implicit transactions are now disabled.\n" );
}
}
* @param string $dbName database name
* @param $flags
* @param string $tablePrefix database table prefixes. By default use the prefix gave in LocalSettings.php
+ * @param bool $foreign disable some operations specific to local databases
*/
function __construct( $server = false, $user = false, $password = false, $dbName = false,
- $flags = 0, $tablePrefix = 'get from global'
+ $flags = 0, $tablePrefix = 'get from global', $foreign = false
) {
global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions;
$this->mTablePrefix = $tablePrefix;
}
+ $this->mForeign = $foreign;
+
if ( $user ) {
$this->open( $server, $user, $password, $dbName );
}
$dbType = strtolower( $dbType );
$class = 'Database' . ucfirst( $dbType );
- if( in_array( $dbType, $canonicalDBTypes ) || ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) ) {
+ if ( in_array( $dbType, $canonicalDBTypes ) || ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) ) {
return new $class(
isset( $p['host'] ) ? $p['host'] : false,
isset( $p['user'] ) ? $p['user'] : false,
isset( $p['password'] ) ? $p['password'] : false,
isset( $p['dbname'] ) ? $p['dbname'] : false,
isset( $p['flags'] ) ? $p['flags'] : 0,
- isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global'
+ isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global',
+ isset( $p['foreign'] ) ? $p['foreign'] : false
);
} else {
return null;
/**
* @param $errno
* @param $errstr
+ * @access private
*/
- protected function connectionErrorHandler( $errno, $errstr ) {
+ public function connectionErrorHandler( $errno, $errstr ) {
$this->mPHPError = $errstr;
}
* @return boolean|ResultWrapper. true for a successful write query, ResultWrapper object
* for a successful read query, or false on failure if $tempIgnore set
*/
- public function query( $sql, $fname = '', $tempIgnore = false ) {
+ public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
$isMaster = !is_null( $this->getLBInfo( 'master' ) );
if ( !Profiler::instance()->isStub() ) {
# generalizeSQL will probably cut down the query to reasonable
wfDebug( "Query {$this->mDBname} ($cnt) ($master): $sqlx\n" );
}
- if ( istainted( $sql ) & TC_MYSQL ) {
- if ( !Profiler::instance()->isStub() ) {
- wfProfileOut( $queryProf );
- wfProfileOut( $totalProf );
- }
- throw new MWException( 'Tainted query found' );
- }
-
$queryId = MWDebug::query( $sql, $fname, $isMaster );
# Do the query and handle errors
# Transaction is gone, like it or not
$this->mTrxLevel = 0;
$this->mTrxIdleCallbacks = array(); // cancel
+ $this->mTrxPreCommitCallbacks = array(); // cancel
wfDebug( "Connection lost, reconnecting...\n" );
if ( $this->ping() ) {
* @return String
*/
protected function fillPreparedArg( $matches ) {
- switch( $matches[1] ) {
- case '\\?': return '?';
- case '\\!': return '!';
- case '\\&': return '&';
+ switch ( $matches[1] ) {
+ case '\\?':
+ return '?';
+ case '\\!':
+ return '!';
+ case '\\&':
+ return '&';
}
list( /* $n */, $arg ) = each( $this->preparedArgs );
- switch( $matches[1] ) {
- case '?': return $this->addQuotes( $arg );
- case '!': return $arg;
+ switch ( $matches[1] ) {
+ case '?':
+ return $this->addQuotes( $arg );
+ case '!':
+ return $arg;
case '&':
# return $this->addQuotes( file_get_contents( $arg ) );
throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
*
* @param $res Mixed: A SQL result
*/
- public function freeResult( $res ) {}
+ public function freeResult( $res ) {
+ }
/**
* A SELECT wrapper which returns a single field from a single result row.
*
* @return bool|mixed The value from the field, or false on failure.
*/
- public function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField',
- $options = array() )
- {
+ public function selectField( $table, $var, $cond = '', $fname = __METHOD__,
+ $options = array()
+ ) {
if ( !is_array( $options ) ) {
$options = array( $options );
}
* DBQueryError exception will be thrown, except if the "ignore errors"
* option was set, in which case false will be returned.
*/
- public function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+ public function select( $table, $vars, $conds = '', $fname = __METHOD__,
$options = array(), $join_conds = array() ) {
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
* @return string SQL query string.
* @see DatabaseBase::select()
*/
- public function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+ public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
$options = array(), $join_conds = array() )
{
if ( is_array( $vars ) ) {
*
* @return object|bool
*/
- public function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow',
+ public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
$options = array(), $join_conds = array() )
{
$options = (array)$options;
* @return Integer: row count
*/
public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = 'DatabaseBase::estimateRowCount', $options = array() )
+ $fname = __METHOD__, $options = array() )
{
$rows = 0;
$res = $this->select( $table, array( 'rowcount' => 'COUNT(*)' ), $conds, $fname, $options );
* @param string $fname calling function name (optional)
* @return Boolean: whether $table has filed $field
*/
- public function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) {
+ public function fieldExists( $table, $field, $fname = __METHOD__ ) {
$info = $this->fieldInfo( $table, $field );
return (bool)$info;
*
* @return bool|null
*/
- public function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) {
- if( !$this->tableExists( $table ) ) {
+ public function indexExists( $table, $index, $fname = __METHOD__ ) {
+ if ( !$this->tableExists( $table ) ) {
return null;
}
*
* @return bool
*/
- public function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) {
+ public function insert( $table, $a, $fname = __METHOD__, $options = array() ) {
# No rows to insert, easy just return now
if ( !count( $a ) ) {
return true;
* - LOW_PRIORITY: MySQL-specific, see MySQL manual.
* @return Boolean
*/
- function update( $table, $values, $conds, $fname = 'DatabaseBase::update', $options = array() ) {
+ function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) {
$table = $this->tableName( $table );
$opts = $this->makeUpdateOptions( $options );
$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
} else {
list( $table ) = $dbDetails;
if ( $wgSharedDB !== null # We have a shared database
+ && $this->mForeign == false # We're not working on a foreign database
&& !$this->isQuotedIdentifier( $table ) # Paranoia check to prevent shared tables listing '`table`'
&& in_array( $table, $wgSharedTables ) # A shared table is selected
) {
protected function indexName( $index ) {
// Backwards-compatibility hack
$renamed = array(
- 'ar_usertext_timestamp' => 'usertext_timestamp',
- 'un_user_id' => 'user_id',
- 'un_user_ip' => 'user_ip',
+ 'ar_usertext_timestamp' => 'usertext_timestamp',
+ 'un_user_id' => 'user_id',
+ 'un_user_ip' => 'user_ip',
);
if ( isset( $renamed[$index] ) ) {
* a field name or an array of field names
* @param string $fname Calling function name (use __METHOD__) for logs/profiling
*/
- public function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) {
+ public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
$quotedTable = $this->tableName( $table );
if ( count( $rows ) == 0 ) {
$rows = array( $rows );
}
- foreach( $rows as $row ) {
+ foreach ( $rows as $row ) {
# Delete rows which collide
if ( $uniqueIndexes ) {
$sql = "DELETE FROM $quotedTable WHERE ";
* @throws DBUnexpectedError
*/
public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
- $fname = 'DatabaseBase::deleteJoin' )
+ $fname = __METHOD__ )
{
if ( !$conds ) {
throw new DBUnexpectedError( $this,
* @throws DBUnexpectedError
* @return bool|ResultWrapper
*/
- public function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) {
+ public function delete( $table, $conds, $fname = __METHOD__ ) {
if ( !$conds ) {
throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
}
* @return ResultWrapper
*/
public function insertSelect( $destTable, $srcTable, $varMap, $conds,
- $fname = 'DatabaseBase::insertSelect',
+ $fname = __METHOD__,
$insertOptions = array(), $selectOptions = array() )
{
$destTable = $this->tableName( $destTable );
$args = func_get_args();
$function = array_shift( $args );
$oldIgnore = $this->ignoreErrors( true );
- $tries = DEADLOCK_TRIES;
+ $tries = self::DEADLOCK_TRIES;
if ( is_array( $function ) ) {
$fname = $function[0];
if ( $errno ) {
if ( $this->wasDeadlock() ) {
# Retry
- usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
+ usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
} else {
$this->reportQueryError( $error, $errno, $sql, $fname );
}
/**
* Run an anonymous function as soon as there is no transaction pending.
* If there is a transaction and it is rolled back, then the callback is cancelled.
+ * Queries in the function will run in AUTO-COMMIT mode unless there are begin() calls.
* Callbacks must commit any transactions that they begin.
*
- * This is useful for updates to different systems or separate transactions are needed.
+ * This is useful for updates to different systems or when separate transactions are needed.
+ * For example, one might want to enqueue jobs into a system outside the database, but only
+ * after the database is updated so that the jobs will see the data when they actually run.
+ * It can also be used for updates that easily cause deadlocks if locks are held too long.
*
+ * @param Closure $callback
* @since 1.20
+ */
+ final public function onTransactionIdle( Closure $callback ) {
+ $this->mTrxIdleCallbacks[] = $callback;
+ if ( !$this->mTrxLevel ) {
+ $this->runOnTransactionIdleCallbacks();
+ }
+ }
+
+ /**
+ * Run an anonymous function before the current transaction commits or now if there is none.
+ * If there is a transaction and it is rolled back, then the callback is cancelled.
+ * Callbacks must not start nor commit any transactions.
+ *
+ * This is useful for updates that easily cause deadlocks if locks are held too long
+ * but where atomicity is strongly desired for these updates and some related updates.
*
* @param Closure $callback
+ * @since 1.22
*/
- final public function onTransactionIdle( Closure $callback ) {
+ final public function onTransactionPreCommitOrIdle( Closure $callback ) {
if ( $this->mTrxLevel ) {
- $this->mTrxIdleCallbacks[] = $callback;
+ $this->mTrxPreCommitCallbacks[] = $callback;
} else {
- $callback();
+ $this->onTransactionIdle( $callback ); // this will trigger immediately
}
}
/**
- * Actually run the "on transaction idle" callbacks.
+ * Actually any "on transaction idle" callbacks.
*
* @since 1.20
*/
$this->clearFlag( DBO_TRX ); // make each query its own transaction
$callback();
$this->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin()
- } catch ( Exception $e ) {}
+ } catch ( Exception $e ) {
+ }
}
} while ( count( $this->mTrxIdleCallbacks ) );
}
}
+ /**
+ * Actually any "on transaction pre-commit" callbacks.
+ *
+ * @since 1.22
+ */
+ protected function runOnTransactionPreCommitCallbacks() {
+ $e = null; // last exception
+ do { // callbacks may add callbacks :)
+ $callbacks = $this->mTrxPreCommitCallbacks;
+ $this->mTrxPreCommitCallbacks = array(); // recursion guard
+ foreach ( $callbacks as $callback ) {
+ try {
+ $callback();
+ } catch ( Exception $e ) {}
+ }
+ } while ( count( $this->mTrxPreCommitCallbacks ) );
+
+ if ( $e instanceof Exception ) {
+ throw $e; // re-throw any last exception
+ }
+ }
+
/**
* Begin a transaction. If a transaction is already in progress, that transaction will be committed before the
* new transaction is started.
*
* @param $fname string
*/
- final public function begin( $fname = 'DatabaseBase::begin' ) {
+ final public function begin( $fname = __METHOD__ ) {
global $wgDebugDBTransactions;
if ( $this->mTrxLevel ) { // implicit commit
// log it if $wgDebugDBTransactions is enabled.
if ( $this->mTrxDoneWrites && $wgDebugDBTransactions ) {
wfDebug( "$fname: Automatic transaction with writes in progress" .
- " (from {$this->mTrxFname}), performing implicit commit!\n" );
+ " (from {$this->mTrxFname}), performing implicit commit!\n"
+ );
}
}
+ $this->runOnTransactionPreCommitCallbacks();
$this->doCommit( $fname );
$this->runOnTransactionIdleCallbacks();
}
* This will silently break any ongoing explicit transaction. Only set the flush flag if you are sure
* that it is safe to ignore these warnings in your context.
*/
- final public function commit( $fname = 'DatabaseBase::commit', $flush = '' ) {
+ final public function commit( $fname = __METHOD__, $flush = '' ) {
if ( $flush != 'flush' ) {
if ( !$this->mTrxLevel ) {
wfWarn( "$fname: No transaction to commit, something got out of sync!" );
- } elseif( $this->mTrxAutomatic ) {
+ } elseif ( $this->mTrxAutomatic ) {
wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" );
}
} else {
if ( !$this->mTrxLevel ) {
return; // nothing to do
- } elseif( !$this->mTrxAutomatic ) {
+ } elseif ( !$this->mTrxAutomatic ) {
wfWarn( "$fname: Flushing an explicit transaction, getting out of sync!" );
}
}
+ $this->runOnTransactionPreCommitCallbacks();
$this->doCommit( $fname );
$this->runOnTransactionIdleCallbacks();
}
*
* @param $fname string
*/
- final public function rollback( $fname = 'DatabaseBase::rollback' ) {
+ final public function rollback( $fname = __METHOD__ ) {
if ( !$this->mTrxLevel ) {
wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
}
$this->doRollback( $fname );
$this->mTrxIdleCallbacks = array(); // cancel
+ $this->mTrxPreCommitCallbacks = array(); // cancel
}
/**
* @return Boolean: true if operation was successful
*/
public function duplicateTableStructure( $oldName, $newName, $temporary = false,
- $fname = 'DatabaseBase::duplicateTableStructure' )
- {
+ $fname = __METHOD__
+ ) {
throw new MWException(
'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
}
* @param string $fname calling function name
* @throws MWException
*/
- function listTables( $prefix = null, $fname = 'DatabaseBase::listTables' ) {
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
}
* @param $options Array
* @return void
*/
- public function setSessionOptions( array $options ) {}
+ public function setSessionOptions( array $options ) {
+ }
/**
* Read and execute SQL commands from a file.
* @return bool|string
*/
public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
- $fname = 'DatabaseBase::sourceStream', $inputCallback = false )
+ $fname = __METHOD__, $inputCallback = false )
{
$cmd = '';
* @return bool|ResultWrapper
* @since 1.18
*/
- public function dropTable( $tableName, $fName = 'DatabaseBase::dropTable' ) {
- if( !$this->tableExists( $tableName, $fName ) ) {
+ public function dropTable( $tableName, $fName = __METHOD__ ) {
+ if ( !$this->tableExists( $tableName, $fName ) ) {
return false;
}
$sql = "DROP TABLE " . $this->tableName( $tableName );
- if( $this->cascadingDeletes() ) {
+ if ( $this->cascadingDeletes() ) {
$sql .= " CASCADE";
}
return $this->query( $sql, $fName );
}
public function __destruct() {
- if ( count( $this->mTrxIdleCallbacks ) ) { // sanity
- trigger_error( "Transaction idle callbacks still pending." );
+ if ( count( $this->mTrxIdleCallbacks ) || count( $this->mTrxPreCommitCallbacks ) ) {
+ trigger_error( "Transaction idle or pre-commit callbacks still pending." ); // sanity
}
}
}