';
- $text = str_replace(
- $tag,
- $tag . $msg,
- $cache->fetchPageText() );
+ $this->mFlags |= DBO_TRX;
}
}
- return $text;
- }
-}
-
-/**
- * @addtogroup Database
- */
-class DBQueryError extends DBError {
- public $error, $errno, $sql, $fname;
-
- function __construct( Database &$db, $error, $errno, $sql, $fname ) {
- $message = "A database error has occurred\n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
-
- parent::__construct( $db, $message );
- $this->error = $error;
- $this->errno = $errno;
- $this->sql = $sql;
- $this->fname = $fname;
- }
+ /*
+ // Faster read-only access
+ if ( wfReadOnly() ) {
+ $this->mFlags |= DBO_PERSISTENT;
+ $this->mFlags &= ~DBO_TRX;
+ }*/
- function getText() {
- if ( $this->useMessageCache() ) {
- return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
- htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+ /** Get the default table prefix*/
+ if ( $tablePrefix == 'get from global' ) {
+ $this->mTablePrefix = $wgDBprefix;
} else {
- return $this->getMessage();
+ $this->mTablePrefix = $tablePrefix;
}
- }
-
- function getSQL() {
- global $wgShowSQLErrors;
- if( !$wgShowSQLErrors ) {
- return $this->msg( 'sqlhidden', 'SQL hidden' );
- } else {
- return $this->sql;
+
+ if ( $server ) {
+ $this->open( $server, $user, $password, $dbName );
}
}
-
- function getLogMessage() {
- # Don't send to the exception log
- return false;
- }
- function getPageTitle() {
- return $this->msg( 'databaseerror', 'Database error' );
+ /**
+ * @static
+ * @param failFunction
+ * @param $flags
+ */
+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
+ {
+ return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
}
- function getHTML() {
- if ( $this->useMessageCache() ) {
- return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
- htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
- } else {
- return nl2br( htmlspecialchars( $this->getMessage() ) );
+ /**
+ * Usually aborts on failure
+ * If the failFunction is set to a non-zero integer, returns success
+ */
+ function open( $server, $user, $password, $dbName ) {
+ global $wguname;
+ wfProfileIn( __METHOD__ );
+
+ $server = 'localhost'; debugging_code_left_in();
+
+ # Test for missing mysql.so
+ # First try to load it
+ if (!@extension_loaded('mysql')) {
+ @dl('mysql.so');
}
- }
-}
-/**
- * @addtogroup Database
- */
-class DBUnexpectedError extends DBError {}
+ # Fail now
+ # Otherwise we get a suppressed fatal error, which is very hard to track down
+ if ( !function_exists( 'mysql_connect' ) ) {
+ throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
+ }
-/******************************************************************************/
+ $this->close();
+ $this->mServer = $server;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
-/**
- * Database abstraction object
- * @addtogroup Database
- */
-class Database {
+ $success = false;
-#------------------------------------------------------------------------------
-# Variables
-#------------------------------------------------------------------------------
+ wfProfileIn("dbconnect-$server");
- protected $mLastQuery = '';
+ # Try to connect up to three times
+ # The kernel's default SYN retransmission period is far too slow for us,
+ # so we use a short timeout plus a manual retry.
+ $this->mConn = false;
+ $max = 3;
+ for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
+ if ( $i > 1 ) {
+ usleep( 1000 );
+ }
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ @/**/$this->mConn = mysql_pconnect( $server, $user, $password );
+ } else {
+ # Create a new connection...
+ @/**/$this->mConn = mysql_connect( $server, $user, $password, true );
+ }
+ if ($this->mConn === false) {
+ #$iplus = $i + 1;
+ #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
+ }
+ }
+
+ wfProfileOut("dbconnect-$server");
- protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
- protected $mOut, $mOpened = false;
+ if ( $dbName != '' ) {
+ if ( $this->mConn !== false ) {
+ $success = @/**/mysql_select_db( $dbName, $this->mConn );
+ if ( !$success ) {
+ $error = "Error selecting database $dbName on server {$this->mServer} " .
+ "from client host {$wguname['nodename']}\n";
+ wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
+ wfDebug( $error );
+ }
+ } else {
+ wfDebug( "DB connection error\n" );
+ wfDebug( "Server: $server, User: $user, Password: " .
+ substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
+ $success = false;
+ }
+ } else {
+ # Delay USE query
+ $success = (bool)$this->mConn;
+ }
- protected $mFailFunction;
- protected $mTablePrefix;
- protected $mFlags;
- protected $mTrxLevel = 0;
- protected $mErrorCount = 0;
- protected $mLBInfo = array();
+ if ( $success ) {
+ $version = $this->getServerVersion();
+ if ( version_compare( $version, '4.1' ) >= 0 ) {
+ // Tell the server we're communicating with it in UTF-8.
+ // This may engage various charset conversions.
+ global $wgDBmysql5;
+ if( $wgDBmysql5 ) {
+ $this->query( 'SET NAMES utf8', __METHOD__ );
+ }
+ // Turn off strict mode
+ $this->query( "SET sql_mode = ''", __METHOD__ );
+ }
-#------------------------------------------------------------------------------
-# Accessors
-#------------------------------------------------------------------------------
- # These optionally set a variable and return the previous state
+ // Turn off strict mode if it is on
+ } else {
+ $this->reportConnectionError();
+ }
- /**
- * Fail function, takes a Database as a parameter
- * Set to false for default, 1 for ignore errors
- */
- function failFunction( $function = NULL ) {
- return wfSetVar( $this->mFailFunction, $function );
+ $this->mOpened = $success;
+ wfProfileOut( __METHOD__ );
+ return $success;
}
+ /**@}}*/
/**
- * Output page, used for reporting errors
- * FALSE means discard output
+ * Closes a database connection.
+ * if it is open : commits any open transactions
+ *
+ * @return bool operation success. true if already closed.
*/
- function setOutputPage( $out ) {
- $this->mOut = $out;
+ function close()
+ {
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ if ( $this->trxLevel() ) {
+ $this->immediateCommit();
+ }
+ return mysql_close( $this->mConn );
+ } else {
+ return true;
+ }
}
/**
- * Boolean, controls output of large amounts of debug information
+ * @param string $error fallback error message, used if none is given by MySQL
*/
- function debug( $debug = NULL ) {
- return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
- }
+ function reportConnectionError( $error = 'Unknown error' ) {
+ $myError = $this->lastError();
+ if ( $myError ) {
+ $error = $myError;
+ }
- /**
- * Turns buffering of SQL result sets on (true) or off (false).
- * Default is "on" and it should not be changed without good reasons.
- */
- function bufferResults( $buffer = NULL ) {
- if ( is_null( $buffer ) ) {
- return !(bool)( $this->mFlags & DBO_NOBUFFER );
+ if ( $this->mFailFunction ) {
+ # Legacy error handling method
+ if ( !is_int( $this->mFailFunction ) ) {
+ $ff = $this->mFailFunction;
+ $ff( $this, $error );
+ }
} else {
- return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
+ # New method
+ wfLogDBError( "Connection error: $error\n" );
+ throw new DBConnectionError( $this, $error );
}
}
/**
- * Turns on (false) or off (true) the automatic generation and sending
- * of a "we're sorry, but there has been a database error" page on
- * database errors. Default is on (false). When turned off, the
- * code should use lastErrno() and lastError() to handle the
- * situation as appropriate.
+ * Usually aborts on failure. If errors are explicitly ignored, returns success.
+ *
+ * @param $sql String: SQL query
+ * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
+ * comment (you can use __METHOD__ or add some extra info)
+ * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors...
+ * maybe best to catch the exception instead?
+ * @return true for a successful write query, ResultWrapper object for a successful read query,
+ * or false on failure if $tempIgnore set
+ * @throws DBQueryError Thrown when the database returns an error of any kind
*/
- function ignoreErrors( $ignoreErrors = NULL ) {
- return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
- }
+ public function query( $sql, $fname = '', $tempIgnore = false ) {
+ global $wgProfiling;
- /**
- * The current depth of nested transactions
- * @param $level Integer: , default NULL.
- */
- function trxLevel( $level = NULL ) {
- return wfSetVar( $this->mTrxLevel, $level );
- }
+ $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+ if ( $wgProfiling ) {
+ # generalizeSQL will probably cut down the query to reasonable
+ # logging size most of the time. The substr is really just a sanity check.
- /**
- * Number of errors logged, only useful when errors are ignored
- */
- function errorCount( $count = NULL ) {
- return wfSetVar( $this->mErrorCount, $count );
- }
+ # Who's been wasting my precious column space? -- TS
+ #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
- /**
- * Properties passed down from the server info array of the load balancer
- */
- function getLBInfo( $name = NULL ) {
- if ( is_null( $name ) ) {
- return $this->mLBInfo;
- } else {
- if ( array_key_exists( $name, $this->mLBInfo ) ) {
- return $this->mLBInfo[$name];
+ if ( $isMaster ) {
+ $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'Database::query-master';
} else {
- return NULL;
+ $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'Database::query';
}
+ wfProfileIn( $totalProf );
+ wfProfileIn( $queryProf );
}
- }
- function setLBInfo( $name, $value = NULL ) {
- if ( is_null( $value ) ) {
- $this->mLBInfo = $name;
- } else {
- $this->mLBInfo[$name] = $value;
+ $this->mLastQuery = $sql;
+
+ # Add a comment for easy SHOW PROCESSLIST interpretation
+ #if ( $fname ) {
+ global $wgUser;
+ if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
+ $userName = $wgUser->getName();
+ if ( mb_strlen( $userName ) > 15 ) {
+ $userName = mb_substr( $userName, 0, 15 ) . '...';
+ }
+ $userName = str_replace( '/', '', $userName );
+ } else {
+ $userName = '';
+ }
+ $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
+ #} else {
+ # $commentedSql = $sql;
+ #}
+
+ # If DBO_TRX is set, start a transaction
+ if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
+ $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
+ // avoid establishing transactions for SHOW and SET statements too -
+ // that would delay transaction initializations to once connection
+ // is really used by application
+ $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
+ if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
+ $this->begin();
}
- }
- /**
- * Returns true if this database supports (and uses) cascading deletes
- */
- function cascadingDeletes() {
- return false;
+ if ( $this->debug() ) {
+ $sqlx = substr( $commentedSql, 0, 500 );
+ $sqlx = strtr( $sqlx, "\t\n", ' ' );
+ if ( $isMaster ) {
+ wfDebug( "SQL-master: $sqlx\n" );
+ } else {
+ wfDebug( "SQL: $sqlx\n" );
+ }
+ }
+
+ # Do the query and handle errors
+ $ret = $this->doQuery( $commentedSql );
+
+ # Try reconnecting if the connection was lost
+ if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
+ # Transaction is gone, like it or not
+ $this->mTrxLevel = 0;
+ wfDebug( "Connection lost, reconnecting...\n" );
+ if ( $this->ping() ) {
+ wfDebug( "Reconnected\n" );
+ $sqlx = substr( $commentedSql, 0, 500 );
+ $sqlx = strtr( $sqlx, "\t\n", ' ' );
+ global $wgRequestTime;
+ $elapsed = round( microtime(true) - $wgRequestTime, 3 );
+ wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
+ $ret = $this->doQuery( $commentedSql );
+ } else {
+ wfDebug( "Failed\n" );
+ }
+ }
+
+ if ( false === $ret ) {
+ $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
+ }
+
+ if ( $wgProfiling ) {
+ wfProfileOut( $queryProf );
+ wfProfileOut( $totalProf );
+ }
+ return $this->resultObject( $ret );
}
/**
- * Returns true if this database supports (and uses) triggers (e.g. on the page table)
+ * The DBMS-dependent part of query()
+ * @param $sql String: SQL query.
+ * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
+ * @access private
*/
- function cleanupTriggers() {
- return false;
+ /*private*/ function doQuery( $sql ) {
+ if( $this->bufferResults() ) {
+ $ret = mysql_query( $sql, $this->mConn );
+ } else {
+ $ret = mysql_unbuffered_query( $sql, $this->mConn );
+ }
+ return $ret;
}
/**
- * Returns true if this database is strict about what can be put into an IP field.
- * Specifically, it uses a NULL value instead of an empty string.
+ * @param $error
+ * @param $errno
+ * @param $sql
+ * @param string $fname
+ * @param bool $tempIgnore
*/
- function strictIPs() {
- return false;
+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+ global $wgCommandLineMode;
+ # Ignore errors during error handling to avoid infinite recursion
+ $ignore = $this->ignoreErrors( true );
+ ++$this->mErrorCount;
+
+ if( $ignore || $tempIgnore ) {
+ wfDebug("SQL ERROR (ignored): $error\n");
+ $this->ignoreErrors( $ignore );
+ } else {
+ $sql1line = str_replace( "\n", "\\n", $sql );
+ wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
+ wfDebug("SQL ERROR: " . $error . "\n");
+ throw new DBQueryError( $this, $error, $errno, $sql, $fname );
+ }
}
+
/**
- * Returns true if this database uses timestamps rather than integers
- */
- function realTimestamps() {
- return false;
+ * Intended to be compatible with the PEAR::DB wrapper functions.
+ * http://pear.php.net/manual/en/package.database.db.intro-execute.php
+ *
+ * ? = scalar value, quoted as necessary
+ * ! = raw SQL bit (a function for instance)
+ * & = filename; reads the file and inserts as a blob
+ * (we don't use this though...)
+ */
+ function prepare( $sql, $func = 'Database::prepare' ) {
+ /* MySQL doesn't support prepared statements (yet), so just
+ pack up the query for reference. We'll manually replace
+ the bits later. */
+ return array( 'query' => $sql, 'func' => $func );
+ }
+
+ function freePrepared( $prepared ) {
+ /* No-op for MySQL */
}
/**
- * Returns true if this database does an implicit sort when doing GROUP BY
+ * Execute a prepared query with the various arguments
+ * @param string $prepared the prepared sql
+ * @param mixed $args Either an array here, or put scalars as varargs
*/
- function implicitGroupby() {
- return true;
+ function execute( $prepared, $args = null ) {
+ if( !is_array( $args ) ) {
+ # Pull the var args
+ $args = func_get_args();
+ array_shift( $args );
+ }
+ $sql = $this->fillPrepared( $prepared['query'], $args );
+ return $this->query( $sql, $prepared['func'] );
}
/**
- * Returns true if this database does an implicit order by when the column has an index
- * For example: SELECT page_title FROM page LIMIT 1
+ * Prepare & execute an SQL statement, quoting and inserting arguments
+ * in the appropriate places.
+ * @param string $query
+ * @param string $args ...
*/
- function implicitOrderby() {
- return true;
+ function safeQuery( $query, $args = null ) {
+ $prepared = $this->prepare( $query, 'Database::safeQuery' );
+ if( !is_array( $args ) ) {
+ # Pull the var args
+ $args = func_get_args();
+ array_shift( $args );
+ }
+ $retval = $this->execute( $prepared, $args );
+ $this->freePrepared( $prepared );
+ return $retval;
}
/**
- * Returns true if this database can do a native search on IP columns
- * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
+ * For faking prepared SQL statements on DBs that don't support
+ * it directly.
+ * @param string $preparedSql - a 'preparable' SQL statement
+ * @param array $args - array of arguments to fill it with
+ * @return string executable SQL
*/
- function searchableIPs() {
- return false;
+ function fillPrepared( $preparedQuery, $args ) {
+ reset( $args );
+ $this->preparedArgs =& $args;
+ return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
+ array( &$this, 'fillPreparedArg' ), $preparedQuery );
}
/**
- * Returns true if this database can use functional indexes
+ * preg_callback func for fillPrepared()
+ * The arguments should be in $this->preparedArgs and must not be touched
+ * while we're doing this.
+ *
+ * @param array $matches
+ * @return string
+ * @private
*/
- function functionalIndexes() {
- return false;
+ function fillPreparedArg( $matches ) {
+ 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;
+ 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.' );
+ default:
+ throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
+ }
}
/**#@+
- * Get function
+ * @param mixed $res A SQL result
*/
- function lastQuery() { return $this->mLastQuery; }
- function isOpen() { return $this->mOpened; }
- /**#@-*/
-
- function setFlag( $flag ) {
- $this->mFlags |= $flag;
- }
-
- function clearFlag( $flag ) {
- $this->mFlags &= ~$flag;
- }
-
- function getFlag( $flag ) {
- return !!($this->mFlags & $flag);
- }
-
/**
- * General read-only accessor
+ * Free a result object
*/
- function getProperty( $name ) {
- return $this->$name;
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( !@/**/mysql_free_result( $res ) ) {
+ throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+ }
}
-#------------------------------------------------------------------------------
-# Other functions
-#------------------------------------------------------------------------------
-
- /**@{{
- * Constructor.
- * @param string $server database server host
- * @param string $user database user name
- * @param string $password database user password
- * @param string $dbname database name
- * @param failFunction
- * @param $flags
- * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
+ /**
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
*/
- function __construct( $server = false, $user = false, $password = false, $dbName = false,
- $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
-
- global $wgOut, $wgDBprefix, $wgCommandLineMode;
- # Can't get a reference if it hasn't been set yet
- if ( !isset( $wgOut ) ) {
- $wgOut = NULL;
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
}
- $this->mOut =& $wgOut;
-
- $this->mFailFunction = $failFunction;
- $this->mFlags = $flags;
-
- if ( $this->mFlags & DBO_DEFAULT ) {
- if ( $wgCommandLineMode ) {
- $this->mFlags &= ~DBO_TRX;
- } else {
- $this->mFlags |= DBO_TRX;
- }
+ @/**/$row = mysql_fetch_object( $res );
+ if( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
}
+ return $row;
+ }
- /*
- // Faster read-only access
- if ( wfReadOnly() ) {
- $this->mFlags |= DBO_PERSISTENT;
- $this->mFlags &= ~DBO_TRX;
- }*/
-
- /** Get the default table prefix*/
- if ( $tablePrefix == 'get from global' ) {
- $this->mTablePrefix = $wgDBprefix;
- } else {
- $this->mTablePrefix = $tablePrefix;
+ /**
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
+ */
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
}
-
- if ( $server ) {
- $this->open( $server, $user, $password, $dbName );
+ @/**/$row = mysql_fetch_array( $res );
+ if ( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
}
+ return $row;
}
/**
- * @static
- * @param failFunction
- * @param $flags
+ * Get the number of rows in a result object
*/
- static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
- {
- return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ @/**/$n = mysql_num_rows( $res );
+ if( $this->lastErrno() ) {
+ throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $n;
}
/**
- * Usually aborts on failure
- * If the failFunction is set to a non-zero integer, returns success
+ * Get the number of fields in a result object
+ * See documentation for mysql_num_fields()
*/
- function open( $server, $user, $password, $dbName ) {
- global $wguname;
- wfProfileIn( __METHOD__ );
-
- # Test for missing mysql.so
- # First try to load it
- if (!@extension_loaded('mysql')) {
- @dl('mysql.so');
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
}
+ return mysql_num_fields( $res );
+ }
- # Fail now
- # Otherwise we get a suppressed fatal error, which is very hard to track down
- if ( !function_exists( 'mysql_connect' ) ) {
- throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
+ /**
+ * Get a field name in a result object
+ * See documentation for mysql_field_name():
+ * http://www.php.net/mysql_field_name
+ */
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
}
-
- $this->close();
- $this->mServer = $server;
- $this->mUser = $user;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
- $success = false;
-
- wfProfileIn("dbconnect-$server");
-
- # LIVE PATCH by Tim, ask Domas for why: retry loop
- $this->mConn = false;
- $max = 3;
- for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
- if ( $i > 1 ) {
- usleep( 1000 );
- }
- if ( $this->mFlags & DBO_PERSISTENT ) {
- @/**/$this->mConn = mysql_pconnect( $server, $user, $password );
- } else {
- # Create a new connection...
- @/**/$this->mConn = mysql_connect( $server, $user, $password, true );
- }
- if ($this->mConn === false) {
- #$iplus = $i + 1;
- #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
- }
+ return mysql_field_name( $res, $n );
+ }
+
+ /**
+ * Get the inserted value of an auto-increment row
+ *
+ * The value inserted should be fetched from nextSequenceValue()
+ *
+ * Example:
+ * $id = $dbw->nextSequenceValue('page_page_id_seq');
+ * $dbw->insert('page',array('page_id' => $id));
+ * $id = $dbw->insertId();
+ */
+ function insertId() { return mysql_insert_id( $this->mConn ); }
+
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
}
-
- wfProfileOut("dbconnect-$server");
+ return mysql_data_seek( $res, $row );
+ }
- if ( $dbName != '' ) {
- if ( $this->mConn !== false ) {
- $success = @/**/mysql_select_db( $dbName, $this->mConn );
- if ( !$success ) {
- $error = "Error selecting database $dbName on server {$this->mServer} " .
- "from client host {$wguname['nodename']}\n";
- wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
- wfDebug( $error );
- }
- } else {
- wfDebug( "DB connection error\n" );
- wfDebug( "Server: $server, User: $user, Password: " .
- substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
- $success = false;
- }
+ /**
+ * Get the last error number
+ * See mysql_errno()
+ */
+ function lastErrno() {
+ if ( $this->mConn ) {
+ return mysql_errno( $this->mConn );
} else {
- # Delay USE query
- $success = (bool)$this->mConn;
+ return mysql_errno();
}
+ }
- if ( $success ) {
- $version = $this->getServerVersion();
- if ( version_compare( $version, '4.1' ) >= 0 ) {
- // Tell the server we're communicating with it in UTF-8.
- // This may engage various charset conversions.
- global $wgDBmysql5;
- if( $wgDBmysql5 ) {
- $this->query( 'SET NAMES utf8', __METHOD__ );
- }
- // Turn off strict mode
- $this->query( "SET sql_mode = ''", __METHOD__ );
+ /**
+ * Get a description of the last error
+ * See mysql_error() for more details
+ */
+ function lastError() {
+ if ( $this->mConn ) {
+ # Even if it's non-zero, it can still be invalid
+ wfSuppressWarnings();
+ $error = mysql_error( $this->mConn );
+ if ( !$error ) {
+ $error = mysql_error();
}
-
- // Turn off strict mode if it is on
+ wfRestoreWarnings();
} else {
- $this->reportConnectionError();
+ $error = mysql_error();
}
-
- $this->mOpened = $success;
- wfProfileOut( __METHOD__ );
- return $success;
+ if( $error ) {
+ $error .= ' (' . $this->mServer . ')';
+ }
+ return $error;
}
- /**@}}*/
+ /**
+ * Get the number of rows affected by the last write query
+ * See mysql_affected_rows() for more details
+ */
+ function affectedRows() { return mysql_affected_rows( $this->mConn ); }
+ /**#@-*/ // end of template : @param $result
/**
- * Closes a database connection.
- * if it is open : commits any open transactions
+ * Simple UPDATE wrapper
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
*
- * @return bool operation success. true if already closed.
+ * This function exists for historical reasons, Database::update() has a more standard
+ * calling convention and feature set
*/
- function close()
+ function set( $table, $var, $value, $cond, $fname = 'Database::set' )
{
- $this->mOpened = false;
- if ( $this->mConn ) {
- if ( $this->trxLevel() ) {
- $this->immediateCommit();
- }
- return mysql_close( $this->mConn );
- } else {
- return true;
- }
+ $table = $this->tableName( $table );
+ $sql = "UPDATE $table SET $var = '" .
+ $this->strencode( $value ) . "' WHERE ($cond)";
+ return (bool)$this->query( $sql, $fname );
}
/**
- * @param string $error fallback error message, used if none is given by MySQL
+ * Simple SELECT wrapper, returns a single field, input must be encoded
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns FALSE on failure
*/
- function reportConnectionError( $error = 'Unknown error' ) {
- $myError = $this->lastError();
- if ( $myError ) {
- $error = $myError;
+ function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
}
+ $options['LIMIT'] = 1;
- if ( $this->mFailFunction ) {
- # Legacy error handling method
- if ( !is_int( $this->mFailFunction ) ) {
- $ff = $this->mFailFunction;
- $ff( $this, $error );
- }
+ $res = $this->select( $table, $var, $cond, $fname, $options );
+ if ( $res === false || !$this->numRows( $res ) ) {
+ return false;
+ }
+ $row = $this->fetchRow( $res );
+ if ( $row !== false ) {
+ $this->freeResult( $res );
+ return $row[0];
} else {
- # New method
- wfLogDBError( "Connection error: $error\n" );
- throw new DBConnectionError( $this, $error );
+ return false;
}
}
/**
- * Usually aborts on failure. If errors are explicitly ignored, returns success.
+ * Returns an optional USE INDEX clause to go after the table, and a
+ * string to go at the end of the query
*
- * @param $sql String: SQL query
- * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
- * comment (you can use __METHOD__ or add some extra info)
- * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors...
- * maybe best to catch the exception instead?
- * @return true for a successful write query, ResultWrapper object for a successful read query,
- * or false on failure if $tempIgnore set
- * @throws DBQueryError Thrown when the database returns an error of any kind
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
*/
- public function query( $sql, $fname = '', $tempIgnore = false ) {
- global $wgProfiling;
-
- if ( $wgProfiling ) {
- # generalizeSQL will probably cut down the query to reasonable
- # logging size most of the time. The substr is really just a sanity check.
-
- # Who's been wasting my precious column space? -- TS
- #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
- if ( is_null( $this->getLBInfo( 'master' ) ) ) {
- $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'Database::query';
- } else {
- $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'Database::query-master';
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
}
- wfProfileIn( $totalProf );
- wfProfileIn( $queryProf );
}
- $this->mLastQuery = $sql;
+ if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+ if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+
+ //if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ //}
- # Add a comment for easy SHOW PROCESSLIST interpretation
- #if ( $fname ) {
- global $wgUser;
- if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
- $userName = $wgUser->getName();
- if ( mb_strlen( $userName ) > 15 ) {
- $userName = mb_substr( $userName, 0, 15 ) . '...';
- }
- $userName = str_replace( '/', '', $userName );
- } else {
- $userName = '';
- }
- $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
- #} else {
- # $commentedSql = $sql;
- #}
+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
- # If DBO_TRX is set, start a transaction
- if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
- $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
- // avoid establishing transactions for SHOW and SET statements too -
- // that would delay transaction initializations to once connection
- // is really used by application
- $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
- if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
- $this->begin();
+ # Various MySQL extensions
+ if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
+ if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
+ if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
+ if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
+ if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
+ if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
+ if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
+ if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
+
+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ } else {
+ $useIndex = '';
+ }
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ }
+
+ /**
+ * SELECT wrapper
+ *
+ * @param mixed $table Array or string, table name(s) (prefix auto-added)
+ * @param mixed $vars Array or string, field name(s) to be retrieved
+ * @param mixed $conds Array or string, condition(s) for WHERE
+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
+ * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * see Database::makeSelectOptions code for list of supported stuff
+ * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
+ */
+ function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() )
+ {
+ if( is_array( $vars ) ) {
+ $vars = implode( ',', $vars );
}
-
- if ( $this->debug() ) {
- $sqlx = substr( $commentedSql, 0, 500 );
- $sqlx = strtr( $sqlx, "\t\n", ' ' );
- wfDebug( "SQL: $sqlx\n" );
+ if( !is_array( $options ) ) {
+ $options = array( $options );
}
-
- # Do the query and handle errors
- $ret = $this->doQuery( $commentedSql );
-
- # Try reconnecting if the connection was lost
- if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
- # Transaction is gone, like it or not
- $this->mTrxLevel = 0;
- wfDebug( "Connection lost, reconnecting...\n" );
- if ( $this->ping() ) {
- wfDebug( "Reconnected\n" );
- $sqlx = substr( $commentedSql, 0, 500 );
- $sqlx = strtr( $sqlx, "\t\n", ' ' );
- global $wgRequestTime;
- $elapsed = round( microtime(true) - $wgRequestTime, 3 );
- wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
- $ret = $this->doQuery( $commentedSql );
+ if( is_array( $table ) ) {
+ if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
+ $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
+ else
+ $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
+ } elseif ($table!='') {
+ if ($table{0}==' ') {
+ $from = ' FROM ' . $table;
} else {
- wfDebug( "Failed\n" );
+ $from = ' FROM ' . $this->tableName( $table );
}
+ } else {
+ $from = '';
}
- if ( false === $ret ) {
- $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
+ list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+
+ if( !empty( $conds ) ) {
+ if ( is_array( $conds ) ) {
+ $conds = $this->makeList( $conds, LIST_AND );
+ }
+ $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
+ } else {
+ $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
}
- if ( $wgProfiling ) {
- wfProfileOut( $queryProf );
- wfProfileOut( $totalProf );
+ if (isset($options['LIMIT']))
+ $sql = $this->limitResult($sql, $options['LIMIT'],
+ isset($options['OFFSET']) ? $options['OFFSET'] : false);
+ $sql = "$sql $postLimitTail";
+
+ if (isset($options['EXPLAIN'])) {
+ $sql = 'EXPLAIN ' . $sql;
}
- return $this->resultObject( $ret );
+ return $this->query( $sql, $fname );
}
/**
- * The DBMS-dependent part of query()
- * @param $sql String: SQL query.
- * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
- * @access private
+ * Single row SELECT wrapper
+ * Aborts or returns FALSE on error
+ *
+ * $vars: the selected variables
+ * $conds: a condition map, terms are ANDed together.
+ * Items with numeric keys are taken to be literal conditions
+ * Takes an array of selected variables, and a condition map, which is ANDed
+ * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
+ * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
+ * $obj- >page_id is the ID of the Astronomy article
+ *
+ * @todo migrate documentation to phpdocumentor format
*/
- /*private*/ function doQuery( $sql ) {
- if( $this->bufferResults() ) {
- $ret = mysql_query( $sql, $this->mConn );
- } else {
- $ret = mysql_unbuffered_query( $sql, $this->mConn );
+ function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array() ) {
+ $options['LIMIT'] = 1;
+ $res = $this->select( $table, $vars, $conds, $fname, $options );
+ if ( $res === false )
+ return false;
+ if ( !$this->numRows($res) ) {
+ $this->freeResult($res);
+ return false;
}
- return $ret;
- }
+ $obj = $this->fetchObject( $res );
+ $this->freeResult( $res );
+ return $obj;
+ }
+
/**
- * @param $error
- * @param $errno
- * @param $sql
- * @param string $fname
- * @param bool $tempIgnore
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * Takes same arguments as Database::select()
*/
- function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- global $wgCommandLineMode;
- # Ignore errors during error handling to avoid infinite recursion
- $ignore = $this->ignoreErrors( true );
- ++$this->mErrorCount;
-
- if( $ignore || $tempIgnore ) {
- wfDebug("SQL ERROR (ignored): $error\n");
- $this->ignoreErrors( $ignore );
- } else {
- $sql1line = str_replace( "\n", "\\n", $sql );
- wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
- wfDebug("SQL ERROR: " . $error . "\n");
- throw new DBQueryError( $this, $error, $errno, $sql, $fname );
+
+ function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ $options['EXPLAIN']=true;
+ $res = $this->select ($table, $vars, $conds, $fname, $options );
+ if ( $res === false )
+ return false;
+ if (!$this->numRows($res)) {
+ $this->freeResult($res);
+ return 0;
+ }
+
+ $rows=1;
+
+ while( $plan = $this->fetchObject( $res ) ) {
+ $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
}
+
+ $this->freeResult($res);
+ return $rows;
}
-
+
/**
- * Intended to be compatible with the PEAR::DB wrapper functions.
- * http://pear.php.net/manual/en/package.database.db.intro-execute.php
+ * Removes most variables from an SQL query and replaces them with X or N for numbers.
+ * It's only slightly flawed. Don't use for anything important.
*
- * ? = scalar value, quoted as necessary
- * ! = raw SQL bit (a function for instance)
- * & = filename; reads the file and inserts as a blob
- * (we don't use this though...)
+ * @param string $sql A SQL Query
+ * @static
*/
- function prepare( $sql, $func = 'Database::prepare' ) {
- /* MySQL doesn't support prepared statements (yet), so just
- pack up the query for reference. We'll manually replace
- the bits later. */
- return array( 'query' => $sql, 'func' => $func );
- }
+ static function generalizeSQL( $sql ) {
+ # This does the same as the regexp below would do, but in such a way
+ # as to avoid crashing php on some large strings.
+ # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
- function freePrepared( $prepared ) {
- /* No-op for MySQL */
- }
+ $sql = str_replace ( "\\\\", '', $sql);
+ $sql = str_replace ( "\\'", '', $sql);
+ $sql = str_replace ( "\\\"", '', $sql);
+ $sql = preg_replace ("/'.*'/s", "'X'", $sql);
+ $sql = preg_replace ('/".*"/s', "'X'", $sql);
- /**
- * Execute a prepared query with the various arguments
- * @param string $prepared the prepared sql
- * @param mixed $args Either an array here, or put scalars as varargs
- */
- function execute( $prepared, $args = null ) {
- if( !is_array( $args ) ) {
- # Pull the var args
- $args = func_get_args();
- array_shift( $args );
- }
- $sql = $this->fillPrepared( $prepared['query'], $args );
- return $this->query( $sql, $prepared['func'] );
+ # All newlines, tabs, etc replaced by single space
+ $sql = preg_replace ( '/\s+/', ' ', $sql);
+
+ # All numbers => N
+ $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
+
+ return $sql;
}
/**
- * Prepare & execute an SQL statement, quoting and inserting arguments
- * in the appropriate places.
- * @param string $query
- * @param string $args ...
+ * Determines whether a field exists in a table
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
*/
- function safeQuery( $query, $args = null ) {
- $prepared = $this->prepare( $query, 'Database::safeQuery' );
- if( !is_array( $args ) ) {
- # Pull the var args
- $args = func_get_args();
- array_shift( $args );
+ function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( 'DESCRIBE '.$table, $fname );
+ if ( !$res ) {
+ return NULL;
}
- $retval = $this->execute( $prepared, $args );
- $this->freePrepared( $prepared );
- return $retval;
- }
- /**
- * For faking prepared SQL statements on DBs that don't support
- * it directly.
- * @param string $preparedSql - a 'preparable' SQL statement
- * @param array $args - array of arguments to fill it with
- * @return string executable SQL
- */
- function fillPrepared( $preparedQuery, $args ) {
- reset( $args );
- $this->preparedArgs =& $args;
- return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
- array( &$this, 'fillPreparedArg' ), $preparedQuery );
+ $found = false;
+
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Field == $field ) {
+ $found = true;
+ break;
+ }
+ }
+ return $found;
}
/**
- * preg_callback func for fillPrepared()
- * The arguments should be in $this->preparedArgs and must not be touched
- * while we're doing this.
- *
- * @param array $matches
- * @return string
- * @private
+ * Determines whether an index exists
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
*/
- function fillPreparedArg( $matches ) {
- 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;
- 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.' );
- default:
- throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
+ function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
+ $info = $this->indexInfo( $table, $index, $fname );
+ if ( is_null( $info ) ) {
+ return NULL;
+ } else {
+ return $info !== false;
}
}
- /**#@+
- * @param mixed $res A SQL result
- */
+
/**
- * Free a result object
+ * Get information about an index into an object
+ * Returns false if the index does not exist
*/
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
+ # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
+ # SHOW INDEX should work for 3.x and up:
+ # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
+ $table = $this->tableName( $table );
+ $sql = 'SHOW INDEX FROM '.$table;
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return NULL;
}
- if ( !@/**/mysql_free_result( $res ) ) {
- throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+
+ $result = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Key_name == $index ) {
+ $result[] = $row;
+ }
}
+ $this->freeResult($res);
+
+ return empty($result) ? false : $result;
}
/**
- * Fetch the next row from the given result object, in object form.
- * Fields can be retrieved with $row->fieldname, with fields acting like
- * member variables.
- *
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return MySQL row object
- * @throws DBUnexpectedError Thrown if the database returns an error
+ * Query whether a given table exists
*/
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @/**/$row = mysql_fetch_object( $res );
- if( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+ function tableExists( $table ) {
+ $table = $this->tableName( $table );
+ $old = $this->ignoreErrors( true );
+ $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
+ $this->ignoreErrors( $old );
+ if( $res ) {
+ $this->freeResult( $res );
+ return true;
+ } else {
+ return false;
}
- return $row;
}
/**
- * Fetch the next row from the given result object, in associative array
- * form. Fields are retrieved with $row['fieldname'].
+ * mysql_fetch_field() wrapper
+ * Returns false if the field doesn't exist
*
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return MySQL row object
- * @throws DBUnexpectedError Thrown if the database returns an error
+ * @param $table
+ * @param $field
*/
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @/**/$row = mysql_fetch_array( $res );
- if ( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+ function fieldInfo( $table, $field ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT * FROM $table LIMIT 1" );
+ $n = mysql_num_fields( $res->result );
+ for( $i = 0; $i < $n; $i++ ) {
+ $meta = mysql_fetch_field( $res->result, $i );
+ if( $field == $meta->name ) {
+ return new MySQLField($meta);
+ }
}
- return $row;
+ return false;
}
/**
- * Get the number of rows in a result object
+ * mysql_field_type() wrapper
*/
- function numRows( $res ) {
+ function fieldType( $res, $index ) {
if ( $res instanceof ResultWrapper ) {
$res = $res->result;
}
- @/**/$n = mysql_num_rows( $res );
- if( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $n;
+ return mysql_field_type( $res, $index );
}
/**
- * Get the number of fields in a result object
- * See documentation for mysql_num_fields()
+ * Determines if a given index is unique
*/
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ function indexUnique( $table, $index ) {
+ $indexInfo = $this->indexInfo( $table, $index );
+ if ( !$indexInfo ) {
+ return NULL;
}
- return mysql_num_fields( $res );
+ return !$indexInfo[0]->Non_unique;
}
/**
- * Get a field name in a result object
- * See documentation for mysql_field_name():
- * http://www.php.net/mysql_field_name
+ * INSERT wrapper, inserts an array into a table
+ *
+ * $a may be a single associative array, or an array of these with numeric keys, for
+ * multi-row insert.
+ *
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
*/
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+ # No rows to insert, easy just return now
+ if ( !count( $a ) ) {
+ return true;
}
- return mysql_field_name( $res, $n );
+
+ $table = $this->tableName( $table );
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $a[0] );
+ } else {
+ $multi = false;
+ $keys = array_keys( $a );
+ }
+
+ $sql = 'INSERT ' . implode( ' ', $options ) .
+ " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+ if ( $multi ) {
+ $first = true;
+ foreach ( $a as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
+ } else {
+ $sql .= '(' . $this->makeList( $a ) . ')';
+ }
+ return (bool)$this->query( $sql, $fname );
}
/**
- * Get the inserted value of an auto-increment row
- *
- * The value inserted should be fetched from nextSequenceValue()
+ * Make UPDATE options for the Database::update function
*
- * Example:
- * $id = $dbw->nextSequenceValue('page_page_id_seq');
- * $dbw->insert('page',array('page_id' => $id));
- * $id = $dbw->insertId();
+ * @private
+ * @param array $options The options passed to Database::update
+ * @return string
*/
- function insertId() { return mysql_insert_id( $this->mConn ); }
+ function makeUpdateOptions( $options ) {
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $opts = array();
+ if ( in_array( 'LOW_PRIORITY', $options ) )
+ $opts[] = $this->lowPriorityOption();
+ if ( in_array( 'IGNORE', $options ) )
+ $opts[] = 'IGNORE';
+ return implode(' ', $opts);
+ }
/**
- * Change the position of the cursor in a result object
- * See mysql_data_seek()
+ * UPDATE wrapper, takes a condition array and a SET array
+ *
+ * @param string $table The table to UPDATE
+ * @param array $values An array of values to SET
+ * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
+ * @param string $fname The Class::Function calling this function
+ * (for the log)
+ * @param array $options An array of UPDATE options, can be one or
+ * more of IGNORE, LOW_PRIORITY
+ * @return bool
*/
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
+ $table = $this->tableName( $table );
+ $opts = $this->makeUpdateOptions( $options );
+ $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
+ if ( $conds != '*' ) {
+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
}
- return mysql_data_seek( $res, $row );
+ return $this->query( $sql, $fname );
}
/**
- * Get the last error number
- * See mysql_errno()
+ * Makes an encoded list of strings from an array
+ * $mode:
+ * LIST_COMMA - comma separated, no field names
+ * LIST_AND - ANDed WHERE clause (without the WHERE)
+ * LIST_OR - ORed WHERE clause (without the WHERE)
+ * LIST_SET - comma separated with field names, like a SET clause
+ * LIST_NAMES - comma separated field names
*/
- function lastErrno() {
- if ( $this->mConn ) {
- return mysql_errno( $this->mConn );
- } else {
- return mysql_errno();
+ function makeList( $a, $mode = LIST_COMMA ) {
+ if ( !is_array( $a ) ) {
+ throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
+ }
+
+ $first = true;
+ $list = '';
+ foreach ( $a as $field => $value ) {
+ if ( !$first ) {
+ if ( $mode == LIST_AND ) {
+ $list .= ' AND ';
+ } elseif($mode == LIST_OR) {
+ $list .= ' OR ';
+ } else {
+ $list .= ',';
+ }
+ } else {
+ $first = false;
+ }
+ if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
+ $list .= "($value)";
+ } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
+ $list .= "$value";
+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
+ if( count( $value ) == 0 ) {
+ throw new MWException( __METHOD__.': empty input' );
+ } elseif( count( $value ) == 1 ) {
+ // Special-case single values, as IN isn't terribly efficient
+ // Don't necessarily assume the single key is 0; we don't
+ // enforce linear numeric ordering on other arrays here.
+ $value = array_values( $value );
+ $list .= $field." = ".$this->addQuotes( $value[0] );
+ } else {
+ $list .= $field." IN (".$this->makeList($value).") ";
+ }
+ } elseif( is_null($value) ) {
+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
+ $list .= "$field IS ";
+ } elseif ( $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= 'NULL';
+ } else {
+ if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
+ }
}
+ return $list;
}
/**
- * Get a description of the last error
- * See mysql_error() for more details
+ * Change the current database
*/
- function lastError() {
- if ( $this->mConn ) {
- # Even if it's non-zero, it can still be invalid
- wfSuppressWarnings();
- $error = mysql_error( $this->mConn );
- if ( !$error ) {
- $error = mysql_error();
- }
- wfRestoreWarnings();
- } else {
- $error = mysql_error();
- }
- if( $error ) {
- $error .= ' (' . $this->mServer . ')';
- }
- return $error;
+ function selectDB( $db ) {
+ $this->mDBname = $db;
+ return mysql_select_db( $db, $this->mConn );
}
- /**
- * Get the number of rows affected by the last write query
- * See mysql_affected_rows() for more details
- */
- function affectedRows() { return mysql_affected_rows( $this->mConn ); }
- /**#@-*/ // end of template : @param $result
/**
- * Simple UPDATE wrapper
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
- *
- * This function exists for historical reasons, Database::update() has a more standard
- * calling convention and feature set
+ * Get the current DB name
*/
- function set( $table, $var, $value, $cond, $fname = 'Database::set' )
- {
- $table = $this->tableName( $table );
- $sql = "UPDATE $table SET $var = '" .
- $this->strencode( $value ) . "' WHERE ($cond)";
- return (bool)$this->query( $sql, $fname );
+ function getDBname() {
+ return $this->mDBname;
}
/**
- * Simple SELECT wrapper, returns a single field, input must be encoded
- * Usually aborts on failure
- * If errors are explicitly ignored, returns FALSE on failure
+ * Get the server hostname or IP address
*/
- function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
- if ( !is_array( $options ) ) {
- $options = array( $options );
- }
- $options['LIMIT'] = 1;
-
- $res = $this->select( $table, $var, $cond, $fname, $options );
- if ( $res === false || !$this->numRows( $res ) ) {
- return false;
- }
- $row = $this->fetchRow( $res );
- if ( $row !== false ) {
- $this->freeResult( $res );
- return $row[0];
- } else {
- return false;
- }
+ function getServer() {
+ return $this->mServer;
}
/**
- * Returns an optional USE INDEX clause to go after the table, and a
- * string to go at the end of the query
+ * Format a table name ready for use in constructing an SQL query
*
- * @private
+ * This does two important things: it quotes table names which as necessary,
+ * and it adds a table prefix if there is one.
*
- * @param array $options an associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
- */
- function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = '';
-
- $noKeyOptions = array();
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
- }
-
- if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
- if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
- if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
-
- //if (isset($options['LIMIT'])) {
- // $tailOpts .= $this->limitResult('', $options['LIMIT'],
- // isset($options['OFFSET']) ? $options['OFFSET']
- // : false);
- //}
-
- if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
- if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
- # Various MySQL extensions
- if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
- if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
- if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
- if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
- if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
- if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
- if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
- if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
-
- if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
- $useIndex = $this->useIndexClause( $options['USE INDEX'] );
- } else {
- $useIndex = '';
- }
-
- return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
- }
-
- /**
- * SELECT wrapper
+ * All functions of this object which require a table name call this function
+ * themselves. Pass the canonical name to such functions. This is only needed
+ * when calling query() directly.
*
- * @param mixed $table Array or string, table name(s) (prefix auto-added)
- * @param mixed $vars Array or string, field name(s) to be retrieved
- * @param mixed $conds Array or string, condition(s) for WHERE
- * @param string $fname Calling function name (use __METHOD__) for logs/profiling
- * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
- * see Database::makeSelectOptions code for list of supported stuff
- * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
+ * @param string $name database table name
*/
- function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() )
- {
- if( is_array( $vars ) ) {
- $vars = implode( ',', $vars );
- }
- if( !is_array( $options ) ) {
- $options = array( $options );
- }
- if( is_array( $table ) ) {
- if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
- $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
- else
- $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
- } elseif ($table!='') {
- if ($table{0}==' ') {
- $from = ' FROM ' . $table;
- } else {
- $from = ' FROM ' . $this->tableName( $table );
+ function tableName( $name ) {
+ global $wgSharedDB;
+ # Skip quoted literals
+ if ( $name{0} != '`' ) {
+ if ( $this->mTablePrefix !== '' && strpos( $name, '.' ) === false ) {
+ $name = "{$this->mTablePrefix}$name";
}
- } else {
- $from = '';
- }
-
- list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
-
- if( !empty( $conds ) ) {
- if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
+ if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
+ $name = "`$wgSharedDB`.`$name`";
+ } else {
+ # Standard quoting
+ $name = "`$name`";
}
- $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
- } else {
- $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
- }
-
- if (isset($options['LIMIT']))
- $sql = $this->limitResult($sql, $options['LIMIT'],
- isset($options['OFFSET']) ? $options['OFFSET'] : false);
- $sql = "$sql $postLimitTail";
-
- if (isset($options['EXPLAIN'])) {
- $sql = 'EXPLAIN ' . $sql;
}
- return $this->query( $sql, $fname );
+ return $name;
}
/**
- * Single row SELECT wrapper
- * Aborts or returns FALSE on error
- *
- * $vars: the selected variables
- * $conds: a condition map, terms are ANDed together.
- * Items with numeric keys are taken to be literal conditions
- * Takes an array of selected variables, and a condition map, which is ANDed
- * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
- * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
- * $obj- >page_id is the ID of the Astronomy article
+ * Fetch a number of table names into an array
+ * This is handy when you need to construct SQL for joins
*
- * @todo migrate documentation to phpdocumentor format
+ * Example:
+ * extract($dbr->tableNames('user','watchlist'));
+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*/
- function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array() ) {
- $options['LIMIT'] = 1;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
- if ( $res === false )
- return false;
- if ( !$this->numRows($res) ) {
- $this->freeResult($res);
- return false;
+ public function tableNames() {
+ $inArray = func_get_args();
+ $retVal = array();
+ foreach ( $inArray as $name ) {
+ $retVal[$name] = $this->tableName( $name );
}
- $obj = $this->fetchObject( $res );
- $this->freeResult( $res );
- return $obj;
-
+ return $retVal;
}
/**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * Takes same arguments as Database::select()
+ * Fetch a number of table names into an zero-indexed numerical array
+ * This is handy when you need to construct SQL for joins
+ *
+ * Example:
+ * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*/
-
- function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
- $options['EXPLAIN']=true;
- $res = $this->select ($table, $vars, $conds, $fname, $options );
- if ( $res === false )
- return false;
- if (!$this->numRows($res)) {
- $this->freeResult($res);
- return 0;
- }
-
- $rows=1;
-
- while( $plan = $this->fetchObject( $res ) ) {
- $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
+ public function tableNamesN() {
+ $inArray = func_get_args();
+ $retVal = array();
+ foreach ( $inArray as $name ) {
+ $retVal[] = $this->tableName( $name );
}
-
- $this->freeResult($res);
- return $rows;
+ return $retVal;
}
-
/**
- * Removes most variables from an SQL query and replaces them with X or N for numbers.
- * It's only slightly flawed. Don't use for anything important.
- *
- * @param string $sql A SQL Query
- * @static
+ * @private
*/
- static function generalizeSQL( $sql ) {
- # This does the same as the regexp below would do, but in such a way
- # as to avoid crashing php on some large strings.
- # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
-
- $sql = str_replace ( "\\\\", '', $sql);
- $sql = str_replace ( "\\'", '', $sql);
- $sql = str_replace ( "\\\"", '', $sql);
- $sql = preg_replace ("/'.*'/s", "'X'", $sql);
- $sql = preg_replace ('/".*"/s', "'X'", $sql);
-
- # All newlines, tabs, etc replaced by single space
- $sql = preg_replace ( '/\s+/', ' ', $sql);
+ function tableNamesWithUseIndex( $tables, $use_index ) {
+ $ret = array();
- # All numbers => N
- $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
+ foreach ( $tables as $table )
+ if ( @$use_index[$table] !== null )
+ $ret[] = $this->tableName( $table ) . ' ' . $this->useIndexClause( implode( ',', (array)$use_index[$table] ) );
+ else
+ $ret[] = $this->tableName( $table );
- return $sql;
+ return implode( ',', $ret );
}
/**
- * Determines whether a field exists in a table
- * Usually aborts on failure
- * If errors are explicitly ignored, returns NULL on failure
+ * Wrapper for addslashes()
+ * @param string $s String to be slashed.
+ * @return string slashed string.
*/
- function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
- $table = $this->tableName( $table );
- $res = $this->query( 'DESCRIBE '.$table, $fname );
- if ( !$res ) {
- return NULL;
- }
-
- $found = false;
-
- while ( $row = $this->fetchObject( $res ) ) {
- if ( $row->Field == $field ) {
- $found = true;
- break;
- }
- }
- return $found;
+ function strencode( $s ) {
+ return mysql_real_escape_string( $s, $this->mConn );
}
/**
- * Determines whether an index exists
- * Usually aborts on failure
- * If errors are explicitly ignored, returns NULL on failure
+ * If it's a string, adds quotes and backslashes
+ * Otherwise returns as-is
*/
- function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
- $info = $this->indexInfo( $table, $index, $fname );
- if ( is_null( $info ) ) {
- return NULL;
+ function addQuotes( $s ) {
+ if ( is_null( $s ) ) {
+ return 'NULL';
} else {
- return $info !== false;
+ # This will also quote numeric values. This should be harmless,
+ # and protects against weird problems that occur when they really
+ # _are_ strings such as article titles and string->number->string
+ # conversion is not 1:1.
+ return "'" . $this->strencode( $s ) . "'";
}
}
-
/**
- * Get information about an index into an object
- * Returns false if the index does not exist
+ * Escape string for safe LIKE usage
*/
- function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
- # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
- # SHOW INDEX should work for 3.x and up:
- # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
- $table = $this->tableName( $table );
- $sql = 'SHOW INDEX FROM '.$table;
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return NULL;
- }
-
- $result = array();
- while ( $row = $this->fetchObject( $res ) ) {
- if ( $row->Key_name == $index ) {
- $result[] = $row;
- }
- }
- $this->freeResult($res);
-
- return empty($result) ? false : $result;
+ function escapeLike( $s ) {
+ $s=$this->strencode( $s );
+ $s=str_replace(array('%','_'),array('\%','\_'),$s);
+ return $s;
}
/**
- * Query whether a given table exists
+ * Returns an appropriately quoted sequence value for inserting a new row.
+ * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
+ * subclass will return an integer, and save the value for insertId()
*/
- function tableExists( $table ) {
- $table = $this->tableName( $table );
- $old = $this->ignoreErrors( true );
- $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
- $this->ignoreErrors( $old );
- if( $res ) {
- $this->freeResult( $res );
- return true;
- } else {
- return false;
- }
+ function nextSequenceValue( $seqName ) {
+ return NULL;
}
/**
- * mysql_fetch_field() wrapper
- * Returns false if the field doesn't exist
- *
- * @param $table
- * @param $field
+ * USE INDEX clause
+ * PostgreSQL doesn't have them and returns ""
*/
- function fieldInfo( $table, $field ) {
- $table = $this->tableName( $table );
- $res = $this->query( "SELECT * FROM $table LIMIT 1" );
- $n = mysql_num_fields( $res->result );
- for( $i = 0; $i < $n; $i++ ) {
- $meta = mysql_fetch_field( $res->result, $i );
- if( $field == $meta->name ) {
- return new MySQLField($meta);
- }
- }
- return false;
+ function useIndexClause( $index ) {
+ return "FORCE INDEX ($index)";
}
/**
- * mysql_field_type() wrapper
+ * REPLACE query wrapper
+ * PostgreSQL simulates this with a DELETE followed by INSERT
+ * $row is the row to insert, an associative array
+ * $uniqueIndexes is an array of indexes. Each element may be either a
+ * field name or an array of field names
+ *
+ * It may be more efficient to leave off unique indexes which are unlikely to collide.
+ * However if you do this, you run the risk of encountering errors which wouldn't have
+ * occurred in MySQL
+ *
+ * @todo migrate comment to phodocumentor format
*/
- function fieldType( $res, $index ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+ $table = $this->tableName( $table );
+
+ # Single row case
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
}
- return mysql_field_type( $res, $index );
- }
- /**
- * Determines if a given index is unique
- */
- function indexUnique( $table, $index ) {
- $indexInfo = $this->indexInfo( $table, $index );
- if ( !$indexInfo ) {
- return NULL;
+ $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
+ $first = true;
+ foreach ( $rows as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
}
- return !$indexInfo[0]->Non_unique;
+ return $this->query( $sql, $fname );
}
/**
- * INSERT wrapper, inserts an array into a table
+ * DELETE where the condition is a join
+ * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
*
- * $a may be a single associative array, or an array of these with numeric keys, for
- * multi-row insert.
+ * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
+ * join condition matches, set $conds='*'
*
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
+ * DO NOT put the join condition in $conds
+ *
+ * @param string $delTable The table to delete from.
+ * @param string $joinTable The other table.
+ * @param string $delVar The variable to join on, in the first table.
+ * @param string $joinVar The variable to join on, in the second table.
+ * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
*/
- function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
- # No rows to insert, easy just return now
- if ( !count( $a ) ) {
- return true;
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
}
- $table = $this->tableName( $table );
- if ( !is_array( $options ) ) {
- $options = array( $options );
- }
- if ( isset( $a[0] ) && is_array( $a[0] ) ) {
- $multi = true;
- $keys = array_keys( $a[0] );
- } else {
- $multi = false;
- $keys = array_keys( $a );
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+ if ( $conds != '*' ) {
+ $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
}
- $sql = 'INSERT ' . implode( ' ', $options ) .
- " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+ return $this->query( $sql, $fname );
+ }
- if ( $multi ) {
- $first = true;
- foreach ( $a as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
+ /**
+ * Returns the size of a text field, or -1 for "unlimited"
+ */
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
+ $res = $this->query( $sql, 'Database::textFieldSize' );
+ $row = $this->fetchObject( $res );
+ $this->freeResult( $res );
+
+ $m = array();
+ if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
+ $size = $m[1];
} else {
- $sql .= '(' . $this->makeList( $a ) . ')';
+ $size = -1;
}
- return (bool)$this->query( $sql, $fname );
+ return $size;
+ }
+
+ /**
+ * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
+ */
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
}
/**
- * Make UPDATE options for the Database::update function
+ * DELETE query wrapper
*
- * @private
- * @param array $options The options passed to Database::update
- * @return string
+ * Use $conds == "*" to delete all rows
*/
- function makeUpdateOptions( $options ) {
- if( !is_array( $options ) ) {
- $options = array( $options );
+ function delete( $table, $conds, $fname = 'Database::delete' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
}
- $opts = array();
- if ( in_array( 'LOW_PRIORITY', $options ) )
- $opts[] = $this->lowPriorityOption();
- if ( in_array( 'IGNORE', $options ) )
- $opts[] = 'IGNORE';
- return implode(' ', $opts);
+ $table = $this->tableName( $table );
+ $sql = "DELETE FROM $table";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
}
/**
- * UPDATE wrapper, takes a condition array and a SET array
- *
- * @param string $table The table to UPDATE
- * @param array $values An array of values to SET
- * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
- * @param string $fname The Class::Function calling this function
- * (for the log)
- * @param array $options An array of UPDATE options, can be one or
- * more of IGNORE, LOW_PRIORITY
- * @return bool
+ * INSERT SELECT wrapper
+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+ * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
+ * $conds may be "*" to copy the whole table
+ * srcTable may be an array of tables.
*/
- function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
- $table = $this->tableName( $table );
- $opts = $this->makeUpdateOptions( $options );
- $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
+ $insertOptions = array(), $selectOptions = array() )
+ {
+ $destTable = $this->tableName( $destTable );
+ if ( is_array( $insertOptions ) ) {
+ $insertOptions = implode( ' ', $insertOptions );
+ }
+ if( !is_array( $selectOptions ) ) {
+ $selectOptions = array( $selectOptions );
+ }
+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+ if( is_array( $srcTable ) ) {
+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
+ } else {
+ $srcTable = $this->tableName( $srcTable );
+ }
+ $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ " SELECT $startOpts " . implode( ',', $varMap ) .
+ " FROM $srcTable $useIndex ";
if ( $conds != '*' ) {
- $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
}
+ $sql .= " $tailOpts";
return $this->query( $sql, $fname );
}
/**
- * Makes an encoded list of strings from an array
- * $mode:
- * LIST_COMMA - comma separated, no field names
- * LIST_AND - ANDed WHERE clause (without the WHERE)
- * LIST_OR - ORed WHERE clause (without the WHERE)
- * LIST_SET - comma separated with field names, like a SET clause
- * LIST_NAMES - comma separated field names
+ * Construct a LIMIT query with optional offset
+ * This is used for query pages
+ * $sql string SQL query we will append the limit too
+ * $limit integer the SQL limit
+ * $offset integer the SQL offset (default false)
*/
- function makeList( $a, $mode = LIST_COMMA ) {
- if ( !is_array( $a ) ) {
- throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
+ function limitResult($sql, $limit, $offset=false) {
+ if( !is_numeric($limit) ) {
+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
}
+ return " $sql LIMIT "
+ . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
+ . "{$limit} ";
+ }
+ function limitResultForUpdate($sql, $num) {
+ return $this->limitResult($sql, $num, 0);
+ }
- $first = true;
- $list = '';
- foreach ( $a as $field => $value ) {
- if ( !$first ) {
- if ( $mode == LIST_AND ) {
- $list .= ' AND ';
- } elseif($mode == LIST_OR) {
- $list .= ' OR ';
- } else {
- $list .= ',';
- }
- } else {
- $first = false;
- }
- if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
- $list .= "($value)";
- } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
- $list .= "$value";
- } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
- if( count( $value ) == 0 ) {
- throw new MWException( __METHOD__.': empty input' );
- } elseif( count( $value ) == 1 ) {
- // Special-case single values, as IN isn't terribly efficient
- // Don't necessarily assume the single key is 0; we don't
- // enforce linear numeric ordering on other arrays here.
- $value = array_values( $value );
- $list .= $field." = ".$this->addQuotes( $value[0] );
- } else {
- $list .= $field." IN (".$this->makeList($value).") ";
- }
- } elseif( is_null($value) ) {
- if ( $mode == LIST_AND || $mode == LIST_OR ) {
- $list .= "$field IS ";
- } elseif ( $mode == LIST_SET ) {
- $list .= "$field = ";
- }
- $list .= 'NULL';
- } else {
- if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
- $list .= "$field = ";
- }
- $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
- }
- }
- return $list;
+ /**
+ * Returns an SQL expression for a simple conditional.
+ * Uses IF on MySQL.
+ *
+ * @param string $cond SQL expression which will result in a boolean value
+ * @param string $trueVal SQL expression to return if true
+ * @param string $falseVal SQL expression to return if false
+ * @return string SQL fragment
+ */
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " IF($cond, $trueVal, $falseVal) ";
}
/**
- * Change the current database
+ * Determines if the last failure was due to a deadlock
*/
- function selectDB( $db ) {
- $this->mDBname = $db;
- return mysql_select_db( $db, $this->mConn );
+ function wasDeadlock() {
+ return $this->lastErrno() == 1213;
}
/**
- * Format a table name ready for use in constructing an SQL query
+ * Perform a deadlock-prone transaction.
*
- * This does two important things: it quotes table names which as necessary,
- * and it adds a table prefix if there is one.
+ * This function invokes a callback function to perform a set of write
+ * queries. If a deadlock occurs during the processing, the transaction
+ * will be rolled back and the callback function will be called again.
*
- * All functions of this object which require a table name call this function
- * themselves. Pass the canonical name to such functions. This is only needed
- * when calling query() directly.
+ * Usage:
+ * $dbw->deadlockLoop( callback, ... );
*
- * @param string $name database table name
+ * Extra arguments are passed through to the specified callback function.
+ *
+ * Returns whatever the callback function returned on its successful,
+ * iteration, or false on error, for example if the retry limit was
+ * reached.
*/
- function tableName( $name ) {
- global $wgSharedDB;
- # Skip quoted literals
- if ( $name{0} != '`' ) {
- if ( $this->mTablePrefix !== '' && strpos( $name, '.' ) === false ) {
- $name = "{$this->mTablePrefix}$name";
+ function deadlockLoop() {
+ $myFname = 'Database::deadlockLoop';
+
+ $this->begin();
+ $args = func_get_args();
+ $function = array_shift( $args );
+ $oldIgnore = $this->ignoreErrors( true );
+ $tries = DEADLOCK_TRIES;
+ if ( is_array( $function ) ) {
+ $fname = $function[0];
+ } else {
+ $fname = $function;
+ }
+ do {
+ $retVal = call_user_func_array( $function, $args );
+ $error = $this->lastError();
+ $errno = $this->lastErrno();
+ $sql = $this->lastQuery();
+
+ if ( $errno ) {
+ if ( $this->wasDeadlock() ) {
+ # Retry
+ usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
+ } else {
+ $this->reportQueryError( $error, $errno, $sql, $fname );
+ }
}
- if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
- $name = "`$wgSharedDB`.`$name`";
+ } while( $this->wasDeadlock() && --$tries > 0 );
+ $this->ignoreErrors( $oldIgnore );
+ if ( $tries <= 0 ) {
+ $this->query( 'ROLLBACK', $myFname );
+ $this->reportQueryError( $error, $errno, $sql, $fname );
+ return false;
+ } else {
+ $this->query( 'COMMIT', $myFname );
+ return $retVal;
+ }
+ }
+
+ /**
+ * Do a SELECT MASTER_POS_WAIT()
+ *
+ * @param string $file the binlog file
+ * @param string $pos the binlog position
+ * @param integer $timeout the maximum number of seconds to wait for synchronisation
+ */
+ function masterPosWait( MySQLMasterPos $pos, $timeout ) {
+ $fname = 'Database::masterPosWait';
+ wfProfileIn( $fname );
+
+ # Commit any open transactions
+ if ( $this->mTrxLevel ) {
+ $this->immediateCommit();
+ }
+
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
+ if ( $wait > $timeout * 1e6 ) {
+ wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
+ wfProfileOut( $fname );
+ return -1;
+ } elseif ( $wait > 0 ) {
+ wfDebug( "Fake slave waiting $wait us\n" );
+ usleep( $wait );
+ wfProfileOut( $fname );
+ return 1;
} else {
- # Standard quoting
- $name = "`$name`";
+ wfDebug( "Fake slave up to date ($wait us)\n" );
+ wfProfileOut( $fname );
+ return 0;
}
}
- return $name;
+
+ # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
+ $encFile = $this->addQuotes( $pos->file );
+ $encPos = intval( $pos->pos );
+ $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
+ $res = $this->doQuery( $sql );
+ if ( $res && $row = $this->fetchRow( $res ) ) {
+ $this->freeResult( $res );
+ wfProfileOut( $fname );
+ return $row[0];
+ } else {
+ wfProfileOut( $fname );
+ return false;
+ }
}
/**
- * Fetch a number of table names into an array
- * This is handy when you need to construct SQL for joins
- *
- * Example:
- * extract($dbr->tableNames('user','watchlist'));
- * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
- * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+ * Get the position of the master from SHOW SLAVE STATUS
*/
- public function tableNames() {
- $inArray = func_get_args();
- $retVal = array();
- foreach ( $inArray as $name ) {
- $retVal[$name] = $this->tableName( $name );
+ function getSlavePos() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
+ wfDebug( __METHOD__.": fake slave pos = $pos\n" );
+ return $pos;
+ }
+ $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
+ } else {
+ return false;
}
- return $retVal;
}
-
+
/**
- * Fetch a number of table names into an zero-indexed numerical array
- * This is handy when you need to construct SQL for joins
- *
- * Example:
- * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
- * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
- * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+ * Get the position of the master from SHOW MASTER STATUS
*/
- public function tableNamesN() {
- $inArray = func_get_args();
- $retVal = array();
- foreach ( $inArray as $name ) {
- $retVal[] = $this->tableName( $name );
+ function getMasterPos() {
+ if ( $this->mFakeMaster ) {
+ return new MySQLMasterPos( 'fake', microtime( true ) );
+ }
+ $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
+ $row = $this->fetchObject( $res );
+ if ( $row ) {
+ return new MySQLMasterPos( $row->File, $row->Position );
+ } else {
+ return false;
}
- return $retVal;
}
/**
- * @private
+ * Begin a transaction, committing any previously open transaction
*/
- function tableNamesWithUseIndex( $tables, $use_index ) {
- $ret = array();
-
- foreach ( $tables as $table )
- if ( @$use_index[$table] !== null )
- $ret[] = $this->tableName( $table ) . ' ' . $this->useIndexClause( implode( ',', (array)$use_index[$table] ) );
- else
- $ret[] = $this->tableName( $table );
-
- return implode( ',', $ret );
+ function begin( $fname = 'Database::begin' ) {
+ $this->query( 'BEGIN', $fname );
+ $this->mTrxLevel = 1;
}
/**
- * Wrapper for addslashes()
- * @param string $s String to be slashed.
- * @return string slashed string.
+ * End a transaction
*/
- function strencode( $s ) {
- return mysql_real_escape_string( $s, $this->mConn );
+ function commit( $fname = 'Database::commit' ) {
+ $this->query( 'COMMIT', $fname );
+ $this->mTrxLevel = 0;
}
/**
- * If it's a string, adds quotes and backslashes
- * Otherwise returns as-is
+ * Rollback a transaction.
+ * No-op on non-transactional databases.
*/
- function addQuotes( $s ) {
- if ( is_null( $s ) ) {
- return 'NULL';
- } else {
- # This will also quote numeric values. This should be harmless,
- # and protects against weird problems that occur when they really
- # _are_ strings such as article titles and string->number->string
- # conversion is not 1:1.
- return "'" . $this->strencode( $s ) . "'";
- }
+ function rollback( $fname = 'Database::rollback' ) {
+ $this->query( 'ROLLBACK', $fname, true );
+ $this->mTrxLevel = 0;
}
/**
- * Escape string for safe LIKE usage
+ * Begin a transaction, committing any previously open transaction
+ * @deprecated use begin()
*/
- function escapeLike( $s ) {
- $s=$this->strencode( $s );
- $s=str_replace(array('%','_'),array('\%','\_'),$s);
- return $s;
+ function immediateBegin( $fname = 'Database::immediateBegin' ) {
+ $this->begin();
}
/**
- * Returns an appropriately quoted sequence value for inserting a new row.
- * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
- * subclass will return an integer, and save the value for insertId()
+ * Commit transaction, if one is open
+ * @deprecated use commit()
*/
- function nextSequenceValue( $seqName ) {
- return NULL;
+ function immediateCommit( $fname = 'Database::immediateCommit' ) {
+ $this->commit();
}
/**
- * USE INDEX clause
- * PostgreSQL doesn't have them and returns ""
+ * Return MW-style timestamp used for MySQL schema
*/
- function useIndexClause( $index ) {
- return "FORCE INDEX ($index)";
+ function timestamp( $ts=0 ) {
+ return wfTimestamp(TS_MW,$ts);
}
/**
- * REPLACE query wrapper
- * PostgreSQL simulates this with a DELETE followed by INSERT
- * $row is the row to insert, an associative array
- * $uniqueIndexes is an array of indexes. Each element may be either a
- * field name or an array of field names
- *
- * It may be more efficient to leave off unique indexes which are unlikely to collide.
- * However if you do this, you run the risk of encountering errors which wouldn't have
- * occurred in MySQL
- *
- * @todo migrate comment to phodocumentor format
+ * Local database timestamp format or null
*/
- function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
- $table = $this->tableName( $table );
-
- # Single row case
- if ( !is_array( reset( $rows ) ) ) {
- $rows = array( $rows );
- }
-
- $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
- $first = true;
- foreach ( $rows as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
+ function timestampOrNull( $ts = null ) {
+ if( is_null( $ts ) ) {
+ return null;
+ } else {
+ return $this->timestamp( $ts );
}
- return $this->query( $sql, $fname );
}
/**
- * DELETE where the condition is a join
- * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
- *
- * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
- * join condition matches, set $conds='*'
- *
- * DO NOT put the join condition in $conds
- *
- * @param string $delTable The table to delete from.
- * @param string $joinTable The other table.
- * @param string $delVar The variable to join on, in the first table.
- * @param string $joinVar The variable to join on, in the second table.
- * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
+ * @todo document
*/
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
- }
-
- $delTable = $this->tableName( $delTable );
- $joinTable = $this->tableName( $joinTable );
- $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
- if ( $conds != '*' ) {
- $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+ function resultObject( $result ) {
+ if( empty( $result ) ) {
+ return false;
+ } elseif ( $result instanceof ResultWrapper ) {
+ return $result;
+ } elseif ( $result === true ) {
+ // Successful write query
+ return $result;
+ } else {
+ return new ResultWrapper( $this, $result );
}
+ }
- return $this->query( $sql, $fname );
+ /**
+ * Return aggregated value alias
+ */
+ function aggregateValue ($valuedata,$valuename='value') {
+ return $valuename;
}
/**
- * Returns the size of a text field, or -1 for "unlimited"
+ * @return string wikitext of a link to the server software's web site
*/
- function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
- $res = $this->query( $sql, 'Database::textFieldSize' );
- $row = $this->fetchObject( $res );
- $this->freeResult( $res );
+ function getSoftwareLink() {
+ return "[http://www.mysql.com/ MySQL]";
+ }
- $m = array();
- if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
- $size = $m[1];
- } else {
- $size = -1;
- }
- return $size;
+ /**
+ * @return string Version information from the database
+ */
+ function getServerVersion() {
+ return mysql_get_server_info( $this->mConn );
}
/**
- * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
+ * Ping the server and try to reconnect if it there is no connection
*/
- function lowPriorityOption() {
- return 'LOW_PRIORITY';
+ function ping() {
+ if( !function_exists( 'mysql_ping' ) ) {
+ wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
+ return true;
+ }
+ $ping = mysql_ping( $this->mConn );
+ if ( $ping ) {
+ return true;
+ }
+
+ // Need to reconnect manually in MySQL client 5.0.13+
+ if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
+ mysql_close( $this->mConn );
+ $this->mOpened = false;
+ $this->mConn = false;
+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ return true;
+ }
+ return false;
}
/**
- * DELETE query wrapper
- *
- * Use $conds == "*" to delete all rows
+ * Get slave lag.
+ * At the moment, this will only work if the DB user has the PROCESS privilege
*/
- function delete( $table, $conds, $fname = 'Database::delete' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
+ function getLag() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
+ return $this->mFakeSlaveLag;
}
- $table = $this->tableName( $table );
- $sql = "DELETE FROM $table";
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ $res = $this->query( 'SHOW PROCESSLIST' );
+ # Find slave SQL thread
+ while ( $row = $this->fetchObject( $res ) ) {
+ /* This should work for most situations - when default db
+ * for thread is not specified, it had no events executed,
+ * and therefore it doesn't know yet how lagged it is.
+ *
+ * Relay log I/O thread does not select databases.
+ */
+ if ( $row->User == 'system user' &&
+ $row->State != 'Waiting for master to send event' &&
+ $row->State != 'Connecting to master' &&
+ $row->State != 'Queueing master event to the relay log' &&
+ $row->State != 'Waiting for master update' &&
+ $row->State != 'Requesting binlog dump'
+ ) {
+ # This is it, return the time (except -ve)
+ if ( $row->Time > 0x7fffffff ) {
+ return false;
+ } else {
+ return $row->Time;
+ }
+ }
}
- return $this->query( $sql, $fname );
+ return false;
}
/**
- * INSERT SELECT wrapper
- * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
- * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
- * $conds may be "*" to copy the whole table
- * srcTable may be an array of tables.
+ * Get status information from SHOW STATUS in an associative array
*/
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
- $insertOptions = array(), $selectOptions = array() )
- {
- $destTable = $this->tableName( $destTable );
- if ( is_array( $insertOptions ) ) {
- $insertOptions = implode( ' ', $insertOptions );
- }
- if( !is_array( $selectOptions ) ) {
- $selectOptions = array( $selectOptions );
- }
- list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
- if( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
- } else {
- $srcTable = $this->tableName( $srcTable );
- }
- $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
- " SELECT $startOpts " . implode( ',', $varMap ) .
- " FROM $srcTable $useIndex ";
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ function getStatus($which="%") {
+ $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
+ $status = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ $status[$row->Variable_name] = $row->Value;
}
- $sql .= " $tailOpts";
- return $this->query( $sql, $fname );
+ return $status;
}
/**
- * Construct a LIMIT query with optional offset
- * This is used for query pages
- * $sql string SQL query we will append the limit too
- * $limit integer the SQL limit
- * $offset integer the SQL offset (default false)
+ * Return the maximum number of items allowed in a list, or 0 for unlimited.
*/
- function limitResult($sql, $limit, $offset=false) {
- if( !is_numeric($limit) ) {
- throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
- }
- return " $sql LIMIT "
- . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
- . "{$limit} ";
+ function maxListLen() {
+ return 0;
}
- function limitResultForUpdate($sql, $num) {
- return $this->limitResult($sql, $num, 0);
+
+ function encodeBlob($b) {
+ return $b;
+ }
+
+ function decodeBlob($b) {
+ return $b;
}
/**
- * Returns an SQL expression for a simple conditional.
- * Uses IF on MySQL.
- *
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
+ * Override database's default connection timeout.
+ * May be useful for very long batch queries such as
+ * full-wiki dumps, where a single query reads out
+ * over hours or days.
+ * @param int $timeout in seconds
*/
- function conditional( $cond, $trueVal, $falseVal ) {
- return " IF($cond, $trueVal, $falseVal) ";
+ public function setTimeout( $timeout ) {
+ $this->query( "SET net_read_timeout=$timeout" );
+ $this->query( "SET net_write_timeout=$timeout" );
}
/**
- * Determines if the last failure was due to a deadlock
+ * Read and execute SQL commands from a file.
+ * Returns true on success, error string on failure
+ * @param string $filename File name to open
+ * @param callback $lineCallback Optional function called before reading each line
+ * @param callback $resultCallback Optional function called for each MySQL result
*/
- function wasDeadlock() {
- return $this->lastErrno() == 1213;
+ function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
+ $fp = fopen( $filename, 'r' );
+ if ( false === $fp ) {
+ return "Could not open \"{$filename}\".\n";
+ }
+ $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
+ fclose( $fp );
+ return $error;
}
/**
- * Perform a deadlock-prone transaction.
- *
- * This function invokes a callback function to perform a set of write
- * queries. If a deadlock occurs during the processing, the transaction
- * will be rolled back and the callback function will be called again.
- *
- * Usage:
- * $dbw->deadlockLoop( callback, ... );
- *
- * Extra arguments are passed through to the specified callback function.
- *
- * Returns whatever the callback function returned on its successful,
- * iteration, or false on error, for example if the retry limit was
- * reached.
+ * Read and execute commands from an open file handle
+ * Returns true on success, error string on failure
+ * @param string $fp File handle
+ * @param callback $lineCallback Optional function called before reading each line
+ * @param callback $resultCallback Optional function called for each MySQL result
*/
- function deadlockLoop() {
- $myFname = 'Database::deadlockLoop';
+ function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
+ $cmd = "";
+ $done = false;
+ $dollarquote = false;
- $this->begin();
- $args = func_get_args();
- $function = array_shift( $args );
- $oldIgnore = $this->ignoreErrors( true );
- $tries = DEADLOCK_TRIES;
- if ( is_array( $function ) ) {
- $fname = $function[0];
- } else {
- $fname = $function;
- }
- do {
- $retVal = call_user_func_array( $function, $args );
- $error = $this->lastError();
- $errno = $this->lastErrno();
- $sql = $this->lastQuery();
+ while ( ! feof( $fp ) ) {
+ if ( $lineCallback ) {
+ call_user_func( $lineCallback );
+ }
+ $line = trim( fgets( $fp, 1024 ) );
+ $sl = strlen( $line ) - 1;
- if ( $errno ) {
- if ( $this->wasDeadlock() ) {
- # Retry
- usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
- } else {
- $this->reportQueryError( $error, $errno, $sql, $fname );
+ if ( $sl < 0 ) { continue; }
+ if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
+
+ ## Allow dollar quoting for function declarations
+ if (substr($line,0,4) == '$mw$') {
+ if ($dollarquote) {
+ $dollarquote = false;
+ $done = true;
+ }
+ else {
+ $dollarquote = true;
}
}
- } while( $this->wasDeadlock() && --$tries > 0 );
- $this->ignoreErrors( $oldIgnore );
- if ( $tries <= 0 ) {
- $this->query( 'ROLLBACK', $myFname );
- $this->reportQueryError( $error, $errno, $sql, $fname );
- return false;
- } else {
- $this->query( 'COMMIT', $myFname );
- return $retVal;
+ else if (!$dollarquote) {
+ if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
+ $done = true;
+ $line = substr( $line, 0, $sl );
+ }
+ }
+
+ if ( '' != $cmd ) { $cmd .= ' '; }
+ $cmd .= "$line\n";
+
+ if ( $done ) {
+ $cmd = str_replace(';;', ";", $cmd);
+ $cmd = $this->replaceVars( $cmd );
+ $res = $this->query( $cmd, __METHOD__, true );
+ if ( $resultCallback ) {
+ call_user_func( $resultCallback, $res );
+ }
+
+ if ( false === $res ) {
+ $err = $this->lastError();
+ return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+ }
+
+ $cmd = '';
+ $done = false;
+ }
+ }
+ return true;
+ }
+
+
+ /**
+ * Replace variables in sourced SQL
+ */
+ protected function replaceVars( $ins ) {
+ $varnames = array(
+ 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
+ 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
+ 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
+ );
+
+ // Ordinary variables
+ foreach ( $varnames as $var ) {
+ if( isset( $GLOBALS[$var] ) ) {
+ $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
+ $ins = str_replace( '{$' . $var . '}', $val, $ins );
+ $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
+ $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
+ }
}
+
+ // Table prefixes
+ $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-z_]*)/',
+ array( &$this, 'tableNameCallback' ), $ins );
+ return $ins;
}
/**
- * Do a SELECT MASTER_POS_WAIT()
- *
- * @param string $file the binlog file
- * @param string $pos the binlog position
- * @param integer $timeout the maximum number of seconds to wait for synchronisation
+ * Table name callback
+ * @private
*/
- function masterPosWait( $file, $pos, $timeout ) {
- $fname = 'Database::masterPosWait';
- wfProfileIn( $fname );
+ protected function tableNameCallback( $matches ) {
+ return $this->tableName( $matches[1] );
+ }
+
+ /*
+ * Build a concatenation list to feed into a SQL query
+ */
+ function buildConcat( $stringList ) {
+ return 'CONCAT(' . implode( ',', $stringList ) . ')';
+ }
+}
- # Commit any open transactions
- $this->immediateCommit();
+/**
+ * Database abstraction object for mySQL
+ * Inherit all methods and properties of Database::Database()
+ *
+ * @addtogroup Database
+ * @see Database
+ */
+class DatabaseMysql extends Database {
+ # Inherit all
+}
+
+/******************************************************************************
+ * Utility classes
+ *****************************************************************************/
+
+/**
+ * Utility class.
+ * @addtogroup Database
+ */
+class DBObject {
+ public $mData;
- # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
- $encFile = $this->strencode( $file );
- $sql = "SELECT MASTER_POS_WAIT('$encFile', $pos, $timeout)";
- $res = $this->doQuery( $sql );
- if ( $res && $row = $this->fetchRow( $res ) ) {
- $this->freeResult( $res );
- wfProfileOut( $fname );
- return $row[0];
- } else {
- wfProfileOut( $fname );
- return false;
- }
+ function DBObject($data) {
+ $this->mData = $data;
}
- /**
- * Get the position of the master from SHOW SLAVE STATUS
- */
- function getSlavePos() {
- $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
- $row = $this->fetchObject( $res );
- if ( $row ) {
- return array( $row->Master_Log_File, $row->Read_Master_Log_Pos );
- } else {
- return array( false, false );
- }
+ function isLOB() {
+ return false;
}
- /**
- * Get the position of the master from SHOW MASTER STATUS
- */
- function getMasterPos() {
- $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
- $row = $this->fetchObject( $res );
- if ( $row ) {
- return array( $row->File, $row->Position );
- } else {
- return array( false, false );
- }
+ function data() {
+ return $this->mData;
}
+}
- /**
- * Begin a transaction, committing any previously open transaction
- */
- function begin( $fname = 'Database::begin' ) {
- $this->query( 'BEGIN', $fname );
- $this->mTrxLevel = 1;
+/**
+ * Utility class
+ * @addtogroup Database
+ *
+ * This allows us to distinguish a blob from a normal string and an array of strings
+ */
+class Blob {
+ private $mData;
+ function __construct($data) {
+ $this->mData = $data;
}
-
- /**
- * End a transaction
- */
- function commit( $fname = 'Database::commit' ) {
- $this->query( 'COMMIT', $fname );
- $this->mTrxLevel = 0;
+ function fetch() {
+ return $this->mData;
}
+}
- /**
- * Rollback a transaction.
- * No-op on non-transactional databases.
- */
- function rollback( $fname = 'Database::rollback' ) {
- $this->query( 'ROLLBACK', $fname, true );
- $this->mTrxLevel = 0;
+/**
+ * Utility class.
+ * @addtogroup Database
+ */
+class MySQLField {
+ private $name, $tablename, $default, $max_length, $nullable,
+ $is_pk, $is_unique, $is_key, $type;
+ function __construct ($info) {
+ $this->name = $info->name;
+ $this->tablename = $info->table;
+ $this->default = $info->def;
+ $this->max_length = $info->max_length;
+ $this->nullable = !$info->not_null;
+ $this->is_pk = $info->primary_key;
+ $this->is_unique = $info->unique_key;
+ $this->is_multiple = $info->multiple_key;
+ $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
+ $this->type = $info->type;
}
- /**
- * Begin a transaction, committing any previously open transaction
- * @deprecated use begin()
- */
- function immediateBegin( $fname = 'Database::immediateBegin' ) {
- $this->begin();
+ function name() {
+ return $this->name;
}
- /**
- * Commit transaction, if one is open
- * @deprecated use commit()
- */
- function immediateCommit( $fname = 'Database::immediateCommit' ) {
- $this->commit();
+ function tableName() {
+ return $this->tableName;
}
- /**
- * Return MW-style timestamp used for MySQL schema
- */
- function timestamp( $ts=0 ) {
- return wfTimestamp(TS_MW,$ts);
+ function defaultValue() {
+ return $this->default;
}
- /**
- * Local database timestamp format or null
- */
- function timestampOrNull( $ts = null ) {
- if( is_null( $ts ) ) {
- return null;
- } else {
- return $this->timestamp( $ts );
- }
+ function maxLength() {
+ return $this->max_length;
}
- /**
- * @todo document
- */
- function resultObject( $result ) {
- if( empty( $result ) ) {
- return false;
- } elseif ( $result instanceof ResultWrapper ) {
- return $result;
- } elseif ( $result === true ) {
- // Successful write query
- return $result;
- } else {
- return new ResultWrapper( $this, $result );
- }
+ function nullable() {
+ return $this->nullable;
}
- /**
- * Return aggregated value alias
- */
- function aggregateValue ($valuedata,$valuename='value') {
- return $valuename;
+ function isKey() {
+ return $this->is_key;
}
- /**
- * @return string wikitext of a link to the server software's web site
- */
- function getSoftwareLink() {
- return "[http://www.mysql.com/ MySQL]";
+ function isMultipleKey() {
+ return $this->is_multiple;
}
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- return mysql_get_server_info( $this->mConn );
+ function type() {
+ return $this->type;
}
+}
- /**
- * Ping the server and try to reconnect if it there is no connection
- */
- function ping() {
- if( !function_exists( 'mysql_ping' ) ) {
- wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
- return true;
- }
- $ping = mysql_ping( $this->mConn );
- if ( $ping ) {
- return true;
- }
+/******************************************************************************
+ * Error classes
+ *****************************************************************************/
- // Need to reconnect manually in MySQL client 5.0.13+
- if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
- mysql_close( $this->mConn );
- $this->mOpened = false;
- $this->mConn = false;
- $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
- return true;
- }
- return false;
- }
+/**
+ * Database error base class
+ * @addtogroup Database
+ */
+class DBError extends MWException {
+ public $db;
/**
- * Get slave lag.
- * At the moment, this will only work if the DB user has the PROCESS privilege
+ * Construct a database error
+ * @param Database $db The database object which threw the error
+ * @param string $error A simple error message to be used for debugging
*/
- function getLag() {
- $res = $this->query( 'SHOW PROCESSLIST' );
- # Find slave SQL thread
- while ( $row = $this->fetchObject( $res ) ) {
- /* This should work for most situations - when default db
- * for thread is not specified, it had no events executed,
- * and therefore it doesn't know yet how lagged it is.
- *
- * Relay log I/O thread does not select databases.
- */
- if ( $row->User == 'system user' &&
- $row->State != 'Waiting for master to send event' &&
- $row->State != 'Connecting to master' &&
- $row->State != 'Queueing master event to the relay log' &&
- $row->State != 'Waiting for master update' &&
- $row->State != 'Requesting binlog dump'
- ) {
- # This is it, return the time (except -ve)
- if ( $row->Time > 0x7fffffff ) {
- return false;
- } else {
- return $row->Time;
- }
- }
- }
- return false;
+ function __construct( Database &$db, $error ) {
+ $this->db =& $db;
+ parent::__construct( $error );
}
+}
- /**
- * Get status information from SHOW STATUS in an associative array
- */
- function getStatus($which="%") {
- $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
- $status = array();
- while ( $row = $this->fetchObject( $res ) ) {
- $status[$row->Variable_name] = $row->Value;
+/**
+ * @addtogroup Database
+ */
+class DBConnectionError extends DBError {
+ public $error;
+
+ function __construct( Database &$db, $error = 'unknown error' ) {
+ $msg = 'DB connection error';
+ if ( trim( $error ) != '' ) {
+ $msg .= ": $error";
}
- return $status;
- }
-
- /**
- * Return the maximum number of items allowed in a list, or 0 for unlimited.
- */
- function maxListLen() {
- return 0;
+ $this->error = $error;
+ parent::__construct( $db, $msg );
}
- function encodeBlob($b) {
- return $b;
+ function useOutputPage() {
+ // Not likely to work
+ return false;
}
- function decodeBlob($b) {
- return $b;
+ function useMessageCache() {
+ // Not likely to work
+ return false;
+ }
+
+ function getText() {
+ return $this->getMessage() . "\n";
}
- /**
- * Override database's default connection timeout.
- * May be useful for very long batch queries such as
- * full-wiki dumps, where a single query reads out
- * over hours or days.
- * @param int $timeout in seconds
- */
- public function setTimeout( $timeout ) {
- $this->query( "SET net_read_timeout=$timeout" );
- $this->query( "SET net_write_timeout=$timeout" );
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
}
- /**
- * Read and execute SQL commands from a file.
- * Returns true on success, error string on failure
- * @param string $filename File name to open
- * @param callback $lineCallback Optional function called before reading each line
- * @param callback $resultCallback Optional function called for each MySQL result
- */
- function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
- $fp = fopen( $filename, 'r' );
- if ( false === $fp ) {
- return "Could not open \"{$filename}\".\n";
- }
- $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
- fclose( $fp );
- return $error;
+ function getPageTitle() {
+ global $wgSitename;
+ return "$wgSitename has a problem";
}
- /**
- * Read and execute commands from an open file handle
- * Returns true on success, error string on failure
- * @param string $fp File handle
- * @param callback $lineCallback Optional function called before reading each line
- * @param callback $resultCallback Optional function called for each MySQL result
- */
- function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
- $cmd = "";
- $done = false;
- $dollarquote = false;
+ function getHTML() {
+ global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
+ global $wgSitename, $wgServer, $wgMessageCache;
- while ( ! feof( $fp ) ) {
- if ( $lineCallback ) {
- call_user_func( $lineCallback );
- }
- $line = trim( fgets( $fp, 1024 ) );
- $sl = strlen( $line ) - 1;
+ # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
+ # Hard coding strings instead.
- if ( $sl < 0 ) { continue; }
- if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
+ $noconnect = "
Sorry! This site is experiencing technical difficulties.
Try waiting a few minutes and reloading.
(Can't contact the database server: $1)
";
+ $mainpage = 'Main Page';
+ $searchdisabled = <<
$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
+Note that their indexes of $wgSitename content may be out of date.',
+EOT;
- ## Allow dollar quoting for function declarations
- if (substr($line,0,4) == '$mw$') {
- if ($dollarquote) {
- $dollarquote = false;
- $done = true;
- }
- else {
- $dollarquote = true;
- }
- }
- else if (!$dollarquote) {
- if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
- $done = true;
- $line = substr( $line, 0, $sl );
- }
- }
+ $googlesearch = "
+
+
+";
+ $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
- if ( '' != $cmd ) { $cmd .= ' '; }
- $cmd .= "$line\n";
+ # No database access
+ if ( is_object( $wgMessageCache ) ) {
+ $wgMessageCache->disable();
+ }
- if ( $done ) {
- $cmd = str_replace(';;', ";", $cmd);
- $cmd = $this->replaceVars( $cmd );
- $res = $this->query( $cmd, __METHOD__, true );
- if ( $resultCallback ) {
- call_user_func( $resultCallback, $res );
- }
+ if ( trim( $this->error ) == '' ) {
+ $this->error = $this->db->getProperty('mServer');
+ }
- if ( false === $res ) {
- $err = $this->lastError();
- return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+ $text = str_replace( '$1', $this->error, $noconnect );
+ $text .= wfGetSiteNotice();
+
+ if($wgUseFileCache) {
+ if($wgTitle) {
+ $t =& $wgTitle;
+ } else {
+ if($title) {
+ $t = Title::newFromURL( $title );
+ } elseif (@/**/$_REQUEST['search']) {
+ $search = $_REQUEST['search'];
+ return $searchdisabled .
+ str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
+ $wgInputEncoding ), $googlesearch );
+ } else {
+ $t = Title::newFromText( $mainpage );
}
+ }
- $cmd = '';
- $done = false;
+ $cache = new HTMLFileCache( $t );
+ if( $cache->isFileCached() ) {
+ // @todo, FIXME: $msg is not defined on the next line.
+ $msg = ''.$msg."
\n" .
+ $cachederror . "
\n";
+
+ $tag = '';
+ $text = str_replace(
+ $tag,
+ $tag . $msg,
+ $cache->fetchPageText() );
}
}
- return true;
+
+ return $text;
}
+}
+/**
+ * @addtogroup Database
+ */
+class DBQueryError extends DBError {
+ public $error, $errno, $sql, $fname;
+
+ function __construct( Database &$db, $error, $errno, $sql, $fname ) {
+ $message = "A database error has occurred\n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
- /**
- * Replace variables in sourced SQL
- */
- protected function replaceVars( $ins ) {
- $varnames = array(
- 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
- 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
- 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
- );
+ parent::__construct( $db, $message );
+ $this->error = $error;
+ $this->errno = $errno;
+ $this->sql = $sql;
+ $this->fname = $fname;
+ }
- // Ordinary variables
- foreach ( $varnames as $var ) {
- if( isset( $GLOBALS[$var] ) ) {
- $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
- $ins = str_replace( '{$' . $var . '}', $val, $ins );
- $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
- $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
- }
+ function getText() {
+ if ( $this->useMessageCache() ) {
+ return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+ } else {
+ return $this->getMessage();
}
-
- // Table prefixes
- $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-z_]*)/',
- array( &$this, 'tableNameCallback' ), $ins );
- return $ins;
}
-
- /**
- * Table name callback
- * @private
- */
- protected function tableNameCallback( $matches ) {
- return $this->tableName( $matches[1] );
+
+ function getSQL() {
+ global $wgShowSQLErrors;
+ if( !$wgShowSQLErrors ) {
+ return $this->msg( 'sqlhidden', 'SQL hidden' );
+ } else {
+ return $this->sql;
+ }
+ }
+
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
}
- /*
- * Build a concatenation list to feed into a SQL query
- */
- function buildConcat( $stringList ) {
- return 'CONCAT(' . implode( ',', $stringList ) . ')';
+ function getPageTitle() {
+ return $this->msg( 'databaseerror', 'Database error' );
}
+ function getHTML() {
+ if ( $this->useMessageCache() ) {
+ return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
+ } else {
+ return nl2br( htmlspecialchars( $this->getMessage() ) );
+ }
+ }
}
/**
- * Database abstraction object for mySQL
- * Inherit all methods and properties of Database::Database()
- *
* @addtogroup Database
- * @see Database
*/
-class DatabaseMysql extends Database {
- # Inherit all
-}
+class DBUnexpectedError extends DBError {}
/**
@@ -2454,4 +2526,15 @@ class ResultWrapper implements Iterator {
}
}
+class MySQLMasterPos {
+ var $file, $pos;
+ function __construct( $file, $pos ) {
+ $this->file = $file;
+ $this->pos = $pos;
+ }
+
+ function __toString() {
+ return "{$this->file}/{$this->pos}";
+ }
+}
diff --git a/includes/DatabaseOracle.php b/includes/DatabaseOracle.php
index 38485481c6..06fab298d7 100644
--- a/includes/DatabaseOracle.php
+++ b/includes/DatabaseOracle.php
@@ -692,6 +692,17 @@ echo "error!\n";
return 0;
}
+ function setFakeSlaveLag() {}
+ function setFakeMaster() {}
+
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ function getServer() {
+ return $this->mServer;
+ }
+
} // end DatabaseOracle class
diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php
index 012137159a..2cdc342629 100644
--- a/includes/DatabasePostgres.php
+++ b/includes/DatabasePostgres.php
@@ -1304,6 +1304,17 @@ END;
return false;
}
+ function setFakeSlaveLag() {}
+ function setFakeMaster() {}
+
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ function getServer() {
+ return $this->mServer;
+ }
+
function buildConcat( $stringList ) {
return implode( ' || ', $stringList );
}
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 7be844a6fe..6977b8bdd7 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -580,48 +580,61 @@ $wgCheckDBSchema = true;
*/
$wgSharedDB = null;
-# Database load balancer
-# This is a two-dimensional array, an array of server info structures
-# Fields are:
-# host: Host name
-# dbname: Default database name
-# user: DB user
-# password: DB password
-# type: "mysql" or "postgres"
-# load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0
-# groupLoads: array of load ratios, the key is the query group name. A query may belong
-# to several groups, the most specific group defined here is used.
-#
-# flags: bit field
-# DBO_DEFAULT -- turns on DBO_TRX only if !$wgCommandLineMode (recommended)
-# DBO_DEBUG -- equivalent of $wgDebugDumpSql
-# DBO_TRX -- wrap entire request in a transaction
-# DBO_IGNORE -- ignore errors (not useful in LocalSettings.php)
-# DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php)
-#
-# max lag: (optional) Maximum replication lag before a slave will taken out of rotation
-# max threads: (optional) Maximum number of running threads
-#
-# These and any other user-defined properties will be assigned to the mLBInfo member
-# variable of the Database object.
-#
-# Leave at false to use the single-server variables above. If you set this
-# variable, the single-server variables will generally be ignored (except
-# perhaps in some command-line scripts).
-#
-# The first server listed in this array (with key 0) will be the master. The
-# rest of the servers will be slaves. To prevent writes to your slaves due to
-# accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your
-# slaves in my.cnf. You can set read_only mode at runtime using:
-#
-# SET @@read_only=1;
-#
-# Since the effect of writing to a slave is so damaging and difficult to clean
-# up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even
-# our masters, and then set read_only=0 on masters at runtime.
-#
+/**
+ * Database load balancer
+ * This is a two-dimensional array, an array of server info structures
+ * Fields are:
+ * host: Host name
+ * dbname: Default database name
+ * user: DB user
+ * password: DB password
+ * type: "mysql" or "postgres"
+ * load: ratio of DB_SLAVE load, must be >=0, the sum of all loads must be >0
+ * groupLoads: array of load ratios, the key is the query group name. A query may belong
+ * to several groups, the most specific group defined here is used.
+ *
+ * flags: bit field
+ * DBO_DEFAULT -- turns on DBO_TRX only if !$wgCommandLineMode (recommended)
+ * DBO_DEBUG -- equivalent of $wgDebugDumpSql
+ * DBO_TRX -- wrap entire request in a transaction
+ * DBO_IGNORE -- ignore errors (not useful in LocalSettings.php)
+ * DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php)
+ *
+ * max lag: (optional) Maximum replication lag before a slave will taken out of rotation
+ * max threads: (optional) Maximum number of running threads
+ *
+ * These and any other user-defined properties will be assigned to the mLBInfo member
+ * variable of the Database object.
+ *
+ * Leave at false to use the single-server variables above. If you set this
+ * variable, the single-server variables will generally be ignored (except
+ * perhaps in some command-line scripts).
+ *
+ * The first server listed in this array (with key 0) will be the master. The
+ * rest of the servers will be slaves. To prevent writes to your slaves due to
+ * accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your
+ * slaves in my.cnf. You can set read_only mode at runtime using:
+ *
+ * SET @@read_only=1;
+ *
+ * Since the effect of writing to a slave is so damaging and difficult to clean
+ * up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even
+ * our masters, and then set read_only=0 on masters at runtime.
+ */
$wgDBservers = false;
+/**
+ * Load balancer factory configuration
+ * To set up a multi-master wiki farm, set the class here to something that
+ * can return a LoadBalancer with an appropriate master on a call to getMainLB().
+ * The class identified here is responsible for reading $wgDBservers,
+ * $wgDBserver, etc., so overriding it may cause those globals to be ignored.
+ *
+ * The LBFactory_Multi class is provided for this purpose, please see
+ * includes/LBFactory_Multi.php for configuration information.
+ */
+$wgLBFactoryConf = array( 'class' => 'LBFactory_Simple' );
+
/** How long to wait for a slave to catch up to the master */
$wgMasterWaitTimeout = 10;
@@ -676,19 +689,6 @@ $wgDBmysql5 = false;
*/
$wgLocalDatabases = array();
-/**
- * For multi-wiki clusters with multiple master servers; if an alternate
- * is listed for the requested database, a connection to it will be opened
- * instead of to the current wiki's regular master server when cross-wiki
- * data operations are done from here.
- *
- * Requires that the other server be accessible by network, with the same
- * username/password as the primary.
- *
- * eg $wgAlternateMaster['enwiki'] = 'ariel';
- */
-$wgAlternateMaster = array();
-
/**
* Object cache settings
* See Defines.php for types
diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php
index f9046f74c9..ab9cc8db90 100644
--- a/includes/ExternalStoreDB.php
+++ b/includes/ExternalStoreDB.php
@@ -32,12 +32,7 @@ class ExternalStoreDB {
/** @todo Document.*/
function &getLoadBalancer( $cluster ) {
- global $wgExternalServers, $wgExternalLoadBalancers;
- if ( !array_key_exists( $cluster, $wgExternalLoadBalancers ) ) {
- $wgExternalLoadBalancers[$cluster] = LoadBalancer::newFromParams( $wgExternalServers[$cluster] );
- }
- $wgExternalLoadBalancers[$cluster]->allowLagged(true);
- return $wgExternalLoadBalancers[$cluster];
+ return wfGetLBFactory()->getExternalLB( $cluster );
}
/** @todo Document.*/
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 7acd3e85c1..17dd351edd 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -613,7 +613,6 @@ function wfMsgExt( $key, $options ) {
* @deprecated Please return control to the caller or throw an exception
*/
function wfAbruptExit( $error = false ){
- global $wgLoadBalancer;
static $called = false;
if ( $called ){
exit( -1 );
@@ -634,7 +633,7 @@ function wfAbruptExit( $error = false ){
wfLogProfilingData();
if ( !$error ) {
- $wgLoadBalancer->closeAll();
+ wfGetLB()->closeAll();
}
exit( -1 );
}
@@ -2234,7 +2233,9 @@ function wfSetupSession() {
}
session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure);
session_cache_limiter( 'private, must-revalidate' );
- @session_start();
+ wfSuppressWarnings();
+ session_start();
+ wfRestoreWarnings();
}
/**
@@ -2317,6 +2318,17 @@ function wfWikiID() {
}
}
+/**
+ * Split a wiki ID into DB name and table prefix
+ */
+function wfSplitWikiID( $wiki ) {
+ $bits = explode( '-', $wiki, 2 );
+ if ( count( $bits ) < 2 ) {
+ $bits[] = '';
+ }
+ return $bits;
+}
+
/*
* Get a Database object
* @param integer $db Index of the connection to get. May be DB_MASTER for the
@@ -2326,11 +2338,29 @@ function wfWikiID() {
* @param mixed $groups Query groups. An array of group names that this query
* belongs to. May contain a single string if the query is only
* in one group.
+ *
+ * @param string $wiki The wiki ID, or false for the current wiki
+ */
+function &wfGetDB( $db = DB_LAST, $groups = array(), $wiki = false ) {
+ return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki );
+}
+
+/**
+ * Get a load balancer object.
+ *
+ * @param array $groups List of query groups
+ * @param string $wiki Wiki ID, or false for the current wiki
+ * @return LoadBalancer
+ */
+function wfGetLB( $wiki = false ) {
+ return wfGetLBFactory()->getMainLB( $wiki );
+}
+
+/**
+ * Get the load balancer factory object
*/
-function &wfGetDB( $db = DB_LAST, $groups = array() ) {
- global $wgLoadBalancer;
- $ret = $wgLoadBalancer->getConnection( $db, true, $groups );
- return $ret;
+function &wfGetLBFactory() {
+ return LBFactory::singleton();
}
/**
@@ -2457,9 +2487,9 @@ function wfDeprecated( $function ) {
* @return null
*/
function wfWaitForSlaves( $maxLag ) {
- global $wgLoadBalancer;
if( $maxLag ) {
- list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
+ $lb = wfGetLB();
+ list( $host, $lag ) = $lb->getMaxLag();
while( $lag > $maxLag ) {
$name = @gethostbyaddr( $host );
if( $name !== false ) {
@@ -2467,7 +2497,7 @@ function wfWaitForSlaves( $maxLag ) {
}
print "Waiting for $host (lagged $lag seconds)...\n";
sleep($maxLag);
- list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
+ list( $host, $lag ) = $lb->getMaxLag();
}
}
}
diff --git a/includes/LBFactory.php b/includes/LBFactory.php
new file mode 100644
index 0000000000..ba509f727d
--- /dev/null
+++ b/includes/LBFactory.php
@@ -0,0 +1,211 @@
+forEachLB( array( $this, 'callMethod' ), array( $methodName, $args ) );
+ }
+
+ /**
+ * Private helper for forEachLBCallMethod
+ */
+ function callMethod( $loadBalancer, $methodName, $args ) {
+ call_user_func_array( array( $loadBalancer, $methodName ), $args );
+ }
+
+ /**
+ * Commit changes on all master connections
+ */
+ function commitMasterChanges() {
+ $this->forEachLBCallMethod( 'commitMasterChanges' );
+ }
+}
+
+/**
+ * A simple single-master LBFactory that gets its configuration from the b/c globals
+ */
+class LBFactory_Simple extends LBFactory {
+ var $mainLB;
+ var $extLBs = array();
+
+ # Chronology protector
+ var $chronProt;
+
+ function __construct( $conf ) {
+ $this->chronProt = new ChronologyProtector;
+ }
+
+ function getMainLB( $wiki = false ) {
+ if ( !isset( $this->mainLB ) ) {
+ global $wgDBservers, $wgMasterWaitTimeout;
+ if ( !$wgDBservers ) {
+ global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
+ $wgDBservers = array(array(
+ 'host' => $wgDBserver,
+ 'user' => $wgDBuser,
+ 'password' => $wgDBpassword,
+ 'dbname' => $wgDBname,
+ 'type' => $wgDBtype,
+ 'load' => 1,
+ 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
+ ));
+ }
+
+ $this->mainLB = new LoadBalancer( $wgDBservers, false, $wgMasterWaitTimeout, true );
+ $this->mainLB->parentInfo( array( 'id' => 'main' ) );
+ $this->chronProt->initLB( $this->mainLB );
+ }
+ return $this->mainLB;
+ }
+
+ function getExternalLB( $cluster, $wiki = false ) {
+ global $wgExternalServers;
+ if ( !isset( $this->extLBs[$cluster] ) ) {
+ if ( !isset( $wgExternalServers[$cluster] ) ) {
+ throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+ }
+ $this->extLBs[$cluster] = new LoadBalancer( $wgExternalServers[$cluster] );
+ $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
+ }
+ return $this->extLBs[$cluster];
+ }
+
+ /**
+ * Execute a function for each tracked load balancer
+ * The callback is called with the load balancer as the first parameter,
+ * and $params passed as the subsequent parameters.
+ */
+ function forEachLB( $callback, $params = array() ) {
+ if ( isset( $this->mainLB ) ) {
+ call_user_func_array( $callback, array_merge( array( $this->mainLB ), $params ) );
+ }
+ foreach ( $this->extLBs as $lb ) {
+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+ }
+ }
+
+ function shutdown() {
+ if ( $this->mainLB ) {
+ $this->chronProt->shutdownLB( $this->mainLB );
+ }
+ $this->chronProt->shutdown();
+ $this->commitMasterChanges();
+ }
+}
+
+/**
+ * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
+ * Kind of like Hawking's [[Chronology Protection Agency]].
+ */
+class ChronologyProtector {
+ var $startupPos;
+ var $shutdownPos = array();
+
+ /**
+ * Initialise a LoadBalancer to give it appropriate chronology protection.
+ *
+ * @param LoadBalancer $lb
+ */
+ function initLB( $lb ) {
+ if ( $this->startupPos === null ) {
+ if ( !empty( $_SESSION[__CLASS__] ) ) {
+ $this->startupPos = $_SESSION[__CLASS__];
+ }
+ }
+ if ( !$this->startupPos ) {
+ return;
+ }
+ $masterName = $lb->getServerName( 0 );
+
+ if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) {
+ $info = $lb->parentInfo();
+ $pos = $this->startupPos[$masterName];
+ wfDebug( __METHOD__.": LB " . $info['id'] . " waiting for master pos $pos\n" );
+ $lb->waitFor( $this->startupPos[$masterName] );
+ }
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LoadBalancer is about to shut
+ * down. Saves replication positions.
+ *
+ * @param LoadBalancer $lb
+ */
+ function shutdownLB( $lb ) {
+ if ( session_id() != '' && $lb->getServerCount() > 1 ) {
+ $masterName = $lb->getServerName( 0 );
+ if ( !isset( $this->shutdownPos[$masterName] ) ) {
+ $pos = $lb->getMasterPos();
+ $info = $lb->parentInfo();
+ wfDebug( __METHOD__.": LB " . $info['id'] . " has master pos $pos\n" );
+ $this->shutdownPos[$masterName] = $pos;
+ }
+ }
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
+ * May commit chronology data to persistent storage.
+ */
+ function shutdown() {
+ if ( session_id() != '' && count( $this->shutdownPos ) ) {
+ wfDebug( __METHOD__.": saving master pos for " .
+ count( $this->shutdownPos ) . " master(s)\n" );
+ $_SESSION[__CLASS__] = $this->shutdownPos;
+ }
+ }
+}
+
diff --git a/includes/LBFactory_Multi.php b/includes/LBFactory_Multi.php
new file mode 100644
index 0000000000..aee033af57
--- /dev/null
+++ b/includes/LBFactory_Multi.php
@@ -0,0 +1,221 @@
+ array( 'db1' => 100, 'db2' => 100 ) )
+ *
+ * mainTemplate A server info associative array as documented for $wgDBservers. The host,
+ * hostName and load entries will be overridden.
+ *
+ * groupLoadsBySection A 3-d map giving server load ratios for each section and group. For example:
+ * array( 'section1' => array( 'group1' => array( 'db1' => 100, 'db2' => 100 ) ) )
+ *
+ * groupLoadsByDB A 3-d map giving server load ratios by DB name.
+ *
+ * hostsByName A map of hostname to IP address.
+ *
+ * externalLoads A map of external storage cluster name to server load map
+ *
+ * externalTemplate A server info structure used for external storage servers
+ *
+ * templateOverridesByServer A 2-d map overriding mainTemplate or externalTemplate on a
+ * server-by-server basis.
+ *
+ * templateOverridesByCluster A 2-d map overriding externalTemplate by cluster
+ *
+ * masterTemplateOverrides An override array for mainTemplate and externalTemplate for all
+ * master servers.
+ *
+ */
+class LBFactory_Multi extends LBFactory {
+ // Required settings
+ var $sectionsByDB, $sectionLoads, $mainTemplate;
+ // Optional settings
+ var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array();
+ var $externalLoads = array(), $externalTemplate, $templateOverridesByServer;
+ var $templateOverridesByCluster, $masterTemplateOverrides;
+ // Other stuff
+ var $conf, $mainLBs = array(), $extLBs = array();
+ var $localSection = null;
+
+ function __construct( $conf ) {
+ $this->chronProt = new ChronologyProtector;
+ $this->conf = $conf;
+ $required = array( 'sectionsByDB', 'sectionLoads', 'mainTemplate' );
+ $optional = array( 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
+ 'externalLoads', 'externalTemplate', 'templateOverridesByServer',
+ 'templateOverridesByCluster', 'masterTemplateOverrides' );
+
+ foreach ( $required as $key ) {
+ if ( !isset( $conf[$key] ) ) {
+ throw new MWException( __CLASS__.": $key is required in configuration" );
+ }
+ $this->$key = $conf[$key];
+ }
+
+ foreach ( $optional as $key ) {
+ if ( isset( $conf[$key] ) ) {
+ $this->$key = $conf[$key];
+ }
+ }
+ }
+
+ function getSectionForWiki( $wiki ) {
+ list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
+ if ( isset( $this->sectionsByDB[$dbName] ) ) {
+ return $this->sectionsByDB[$dbName];
+ } else {
+ return 'DEFAULT';
+ }
+ }
+
+ function getMainLB( $wiki = false ) {
+ // Determine section
+ if ( $wiki === false ) {
+ if ( $this->localSection === null ) {
+ $this->localSection = $this->getSectionForWiki( $wiki );
+ }
+ $section = $this->localSection;
+ } else {
+ $section = $this->getSectionForWiki( $wiki );
+ }
+
+ if ( !isset( $this->mainLBs[$section] ) ) {
+ list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
+ $groupLoads = array();
+ if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
+ $groupLoads = $this->groupLoadsByDB[$dbName];
+ }
+ if ( isset( $this->groupLoadsBySection[$section] ) ) {
+ $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] );
+ }
+ $this->mainLBs[$section] = $this->newLoadBalancer( $this->mainTemplate,
+ $this->sectionLoads[$section], $groupLoads, "main-$section" );
+ $this->chronProt->initLB( $this->mainLBs[$section] );
+ }
+ return $this->mainLBs[$section];
+ }
+
+ function getExternalLB( $cluster, $wiki = false ) {
+ global $wgExternalServers;
+ if ( !isset( $this->extLBs[$cluster] ) ) {
+ if ( !isset( $this->externalLoads[$cluster] ) ) {
+ throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+ }
+ if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
+ $template = $this->templateOverridesByCluster[$cluster];
+ } elseif ( isset( $this->externalTemplate ) ) {
+ $template = $this->externalTemplate;
+ } else {
+ $template = $this->mainTemplate;
+ }
+ $this->extLBs[$cluster] = $this->newLoadBalancer( $template,
+ $this->externalLoads[$cluster], array(), "ext-$cluster" );
+ }
+ return $this->extLBs[$cluster];
+ }
+
+ /**
+ * Make a new load balancer object based on template and load array
+ */
+ function newLoadBalancer( $template, $loads, $groupLoads, $id ) {
+ global $wgMasterWaitTimeout;
+ $servers = $this->makeServerArray( $template, $loads, $groupLoads );
+ $lb = new LoadBalancer( $servers, false, $wgMasterWaitTimeout );
+ $lb->parentInfo( array( 'id' => $id ) );
+ return $lb;
+ }
+
+ /**
+ * Make a server array as expected by LoadBalancer::__construct, using a template and load array
+ */
+ function makeServerArray( $template, $loads, $groupLoads ) {
+ $servers = array();
+ $master = true;
+ $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
+ foreach ( $groupLoadsByServer as $server => $stuff ) {
+ if ( !isset( $loads[$server] ) ) {
+ $loads[$server] = 0;
+ }
+ }
+ foreach ( $loads as $serverName => $load ) {
+ $serverInfo = $template;
+ if ( $master ) {
+ $serverInfo['master'] = true;
+ if ( isset( $this->masterTemplateOverrides ) ) {
+ $serverInfo = $this->masterTemplateOverrides + $serverInfo;
+ }
+ $master = false;
+ }
+ if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
+ $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
+ }
+ if ( isset( $groupLoadsByServer[$serverName] ) ) {
+ $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
+ }
+ if ( isset( $this->hostsByName[$serverName] ) ) {
+ $serverInfo['host'] = $this->hostsByName[$serverName];
+ } else {
+ $serverInfo['host'] = $serverName;
+ }
+ $serverInfo['hostName'] = $serverName;
+ $serverInfo['load'] = $load;
+ $servers[] = $serverInfo;
+ }
+ return $servers;
+ }
+
+ /**
+ * Take a group load array indexed by group then server, and reindex it by server then group
+ */
+ function reindexGroupLoads( $groupLoads ) {
+ $reindexed = array();
+ foreach ( $groupLoads as $group => $loads ) {
+ foreach ( $loads as $server => $load ) {
+ $reindexed[$server][$group] = $load;
+ }
+ }
+ return $reindexed;
+ }
+
+ /**
+ * Get the database name and prefix based on the wiki ID
+ */
+ function getDBNameAndPrefix( $wiki = false ) {
+ if ( $wiki === false ) {
+ global $wgDBname, $wgDBprefix;
+ return array( $wgDBname, $wgDBprefix );
+ } else {
+ return wfSplitWikiID( $wiki );
+ }
+ }
+
+ /**
+ * Execute a function for each tracked load balancer
+ * The callback is called with the load balancer as the first parameter,
+ * and $params passed as the subsequent parameters.
+ */
+ function forEachLB( $callback, $params = array() ) {
+ foreach ( $this->mainLBs as $lb ) {
+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+ }
+ foreach ( $this->extLBs as $lb ) {
+ call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+ }
+ }
+
+ function shutdown() {
+ foreach ( $this->mainLBs as $lb ) {
+ $this->chronProt->shutdownLB( $lb );
+ }
+ $this->chronProt->shutdown();
+ $this->commitMasterChanges();
+ }
+}
+
diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php
index 0cdadd1e8e..d6f3d7672d 100644
--- a/includes/LoadBalancer.php
+++ b/includes/LoadBalancer.php
@@ -1,32 +1,29 @@
mServers = $servers;
$this->mFailFunction = $failFunction;
$this->mReadIndex = -1;
$this->mWriteIndex = -1;
- $this->mForce = -1;
- $this->mConnections = array();
+ $this->mConns = array(
+ 'local' => array(),
+ 'foreignUsed' => array(),
+ 'foreignFree' => array() );
$this->mLastIndex = -1;
$this->mLoads = array();
- $this->mWaitForFile = false;
$this->mWaitForPos = false;
$this->mWaitTimeout = $waitTimeout;
$this->mLaggedSlaveMode = false;
@@ -44,9 +41,6 @@ class LoadBalancer {
}
}
}
- if ( $waitForMasterNow ) {
- $this->loadMasterPos();
- }
}
static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 )
@@ -54,6 +48,13 @@ class LoadBalancer {
return new LoadBalancer( $servers, $failFunction, $waitTimeout );
}
+ /**
+ * Get or set arbitrary data used by the parent object, usually an LBFactory
+ */
+ function parentInfo( $x = null ) {
+ return wfSetVar( $this->mParentInfo, $x );
+ }
+
/**
* Given an array of non-normalised probabilities, this function will select
* an element and return the appropriate key
@@ -89,10 +90,14 @@ class LoadBalancer {
# Unset excessively lagged servers
$lags = $this->getLagTimes();
foreach ( $lags as $i => $lag ) {
- if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) &&
- ( $lag === false || $lag > $this->mServers[$i]['max lag'] ) )
- {
- unset( $loads[$i] );
+ if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) ) {
+ if ( $lag === false ) {
+ wfDebug( "Server #$i is not replicating\n" );
+ unset( $loads[$i] );
+ } elseif ( $lag > $this->mServers[$i]['max lag'] ) {
+ wfDebug( "Server #$i is excessively lagged ($lag seconds)\n" );
+ unset( $loads[$i] );
+ }
}
}
@@ -126,114 +131,168 @@ class LoadBalancer {
*
* Side effect: opens connections to databases
*/
- function getReaderIndex() {
- global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll;
-
- $fname = 'LoadBalancer::getReaderIndex';
- wfProfileIn( $fname );
+ function getReaderIndex( $group = false, $wiki = false ) {
+ global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype;
+
+ # FIXME: For now, only go through all this for mysql databases
+ if ($wgDBtype != 'mysql') {
+ return $this->getWriterIndex();
+ }
- $i = false;
- if ( $this->mForce >= 0 ) {
- $i = $this->mForce;
- } elseif ( count( $this->mServers ) == 1 ) {
+ if ( count( $this->mServers ) == 1 ) {
# Skip the load balancing if there's only one server
- $i = 0;
- } else {
- if ( $this->mReadIndex >= 0 ) {
- $i = $this->mReadIndex;
+ return 0;
+ } elseif ( $this->mReadIndex >= 0 ) {
+ return $this->mReadIndex;
+ }
+
+ wfProfileIn( __METHOD__ );
+
+ $totalElapsed = 0;
+
+ # convert from seconds to microseconds
+ $timeout = $wgDBClusterTimeout * 1e6;
+
+ # Find the relevant load array
+ if ( $group !== false ) {
+ if ( isset( $this->mGroupLoads[$group] ) ) {
+ $nonErrorLoads = $this->mGroupLoads[$group];
} else {
- # $loads is $this->mLoads except with elements knocked out if they
- # don't work
- $loads = $this->mLoads;
- $done = false;
- $totalElapsed = 0;
- do {
- if ( $wgReadOnly or $this->mAllowLagged ) {
- $i = $this->pickRandom( $loads );
- } else {
- $i = $this->getRandomNonLagged( $loads );
- if ( $i === false && count( $loads ) != 0 ) {
- # All slaves lagged. Switch to read-only mode
- $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' );
- $i = $this->pickRandom( $loads );
- }
- }
- $serverIndex = $i;
- if ( $i !== false ) {
- wfDebugLog( 'connect', "$fname: Using reader #$i: {$this->mServers[$i]['host']}...\n" );
- $this->openConnection( $i );
-
- if ( !$this->isOpen( $i ) ) {
- wfDebug( "$fname: Failed\n" );
- unset( $loads[$i] );
- $sleepTime = 0;
- } else {
- if ( isset( $this->mServers[$i]['max threads'] ) ) {
- $status = $this->mConnections[$i]->getStatus("Thread%");
- if ( $status['Threads_running'] > $this->mServers[$i]['max threads'] ) {
- # Too much load, back off and wait for a while.
- # The sleep time is scaled by the number of threads connected,
- # to produce a roughly constant global poll rate.
- $sleepTime = $wgDBAvgStatusPoll * $status['Threads_connected'];
-
- # If we reach the timeout and exit the loop, don't use it
- $i = false;
- } else {
- $done = true;
- $sleepTime = 0;
- }
- } else {
- $done = true;
- $sleepTime = 0;
- }
- }
- } else {
- $sleepTime = 500000;
- }
- if ( $sleepTime ) {
- $totalElapsed += $sleepTime;
- $x = "{$this->mServers[$serverIndex]['host']} [$serverIndex]";
- wfProfileIn( "$fname-sleep $x" );
- usleep( $sleepTime );
- wfProfileOut( "$fname-sleep $x" );
+ # No loads for this group, return false and the caller can use some other group
+ wfDebug( __METHOD__.": no loads for group $group\n" );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ } else {
+ $nonErrorLoads = $this->mLoads;
+ }
+
+ if ( !$nonErrorLoads ) {
+ throw new MWException( "Empty server array given to LoadBalancer" );
+ }
+
+ $i = false;
+ $found = false;
+ $laggedSlaveMode = false;
+
+ # First try quickly looking through the available servers for a server that
+ # meets our criteria
+ do {
+ $totalThreadsConnected = 0;
+ $overloadedServers = 0;
+ $currentLoads = $nonErrorLoads;
+ while ( count( $currentLoads ) ) {
+ if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) {
+ $i = $this->pickRandom( $currentLoads );
+ } else {
+ $i = $this->getRandomNonLagged( $currentLoads );
+ if ( $i === false && count( $currentLoads ) != 0 ) {
+ # All slaves lagged. Switch to read-only mode
+ $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' );
+ $i = $this->pickRandom( $currentLoads );
+ $laggedSlaveMode = true;
}
- } while ( count( $loads ) && !$done && $totalElapsed / 1e6 < $wgDBClusterTimeout );
+ }
+
+ if ( $i === false ) {
+ # pickRandom() returned false
+ # This is permanent and means the configuration wants us to return false
+ wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
+ $conn = $this->openConnection( $i, $wiki );
- if ( $totalElapsed / 1e6 >= $wgDBClusterTimeout ) {
- $this->mErrorConnection = false;
- $this->mLastError = 'All servers busy';
+ if ( !$conn ) {
+ wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" );
+ unset( $nonErrorLoads[$i] );
+ unset( $currentLoads[$i] );
+ continue;
}
- if ( $i !== false && $this->isOpen( $i ) ) {
- # Wait for the session master pos for a short time
- if ( $this->mWaitForFile ) {
- if ( !$this->doWait( $i ) ) {
- $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos();
- }
+ if ( isset( $this->mServers[$i]['max threads'] ) ) {
+ $status = $conn->getStatus("Thread%");
+ if ( $wiki !== false ) {
+ $this->reuseConnection( $conn );
}
- if ( $i !== false ) {
- $this->mReadIndex = $i;
+ if ( $status['Threads_running'] > $this->mServers[$i]['max threads'] ) {
+ $totalThreadsConnected += $status['Threads_connected'];
+ $overloadedServers++;
+ unset( $currentLoads[$i] );
+ } else {
+ # Max threads satisfied, return this server
+ break 2;
}
} else {
- $i = false;
+ # No maximum, return this server
+ if ( $wiki !== false ) {
+ $this->reuseConnection( $conn );
+ }
+ $found = true;
+ break 2;
}
}
+
+ # No server found yet
+ $i = false;
+
+ # If all servers were down, quit now
+ if ( !count( $nonErrorLoads ) ) {
+ wfDebugLog( 'connect', "All servers down\n" );
+ break;
+ }
+
+ # Some servers must have been overloaded
+ if ( $overloadedServers == 0 ) {
+ throw new MWException( __METHOD__.": unexpectedly found no overloaded servers" );
+ }
+ # Back off for a while
+ # Scale the sleep time by the number of connected threads, to produce a
+ # roughly constant global poll rate
+ $avgThreads = $totalThreadsConnected / $overloadedServers;
+ $totalElapsed += $this->sleep( $wgDBAvgStatusPoll * $avgThreads );
+ } while ( $totalElapsed < $timeout );
+
+ if ( $totalElapsed >= $timeout ) {
+ wfDebugLog( 'connect', "All servers busy\n" );
+ $this->mErrorConnection = false;
+ $this->mLastError = 'All servers busy';
+ }
+
+ if ( $i !== false ) {
+ # Wait for the session master pos for a short time
+ if ( $this->mWaitForPos && $i > 0 ) {
+ if ( !$this->doWait( $i ) ) {
+ $this->mServers[$i]['slave pos'] = $conn->getSlavePos();
+ }
+ }
+ if ( $i !== false ) {
+ $this->mReadIndex = $i;
+ }
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $i;
}
+ /**
+ * Wait for a specified number of microseconds, and return the period waited
+ */
+ function sleep( $t ) {
+ wfProfileIn( __METHOD__ );
+ wfDebug( __METHOD__.": waiting $t us\n" );
+ usleep( $t );
+ wfProfileOut( __METHOD__ );
+ return $t;
+ }
+
/**
* Get a random server to use in a query group
+ * @deprecated use getReaderIndex
*/
function getGroupIndex( $group ) {
- if ( isset( $this->mGroupLoads[$group] ) ) {
- $i = $this->pickRandom( $this->mGroupLoads[$group] );
- } else {
- $i = false;
- }
- wfDebug( "Query group $group => $i\n" );
- return $i;
+ return $this->getReaderIndex( $group );
}
/**
@@ -241,104 +300,92 @@ class LoadBalancer {
* If a DB_SLAVE connection has been opened already, waits
* Otherwise sets a variable telling it to wait if such a connection is opened
*/
- function waitFor( $file, $pos ) {
- $fname = 'LoadBalancer::waitFor';
- wfProfileIn( $fname );
-
- wfDebug( "User master pos: $file $pos\n" );
- $this->mWaitForFile = false;
- $this->mWaitForPos = false;
+ public function waitFor( $pos ) {
+ wfProfileIn( __METHOD__ );
+ $this->mWaitForPos = $pos;
+ $i = $this->mReadIndex;
- if ( count( $this->mServers ) > 1 ) {
- $this->mWaitForFile = $file;
- $this->mWaitForPos = $pos;
- $i = $this->mReadIndex;
+ if ( $i > 0 ) {
+ if ( !$this->doWait( $i ) ) {
+ $this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
+ $this->mLaggedSlaveMode = true;
+ }
+ }
+ wfProfileOut( __METHOD__ );
+ }
- if ( $i > 0 ) {
- if ( !$this->doWait( $i ) ) {
- $this->mServers[$i]['slave pos'] = $this->mConnections[$i]->getSlavePos();
- $this->mLaggedSlaveMode = true;
- }
+ /**
+ * Get any open connection to a given server index, local or foreign
+ * Returns false if there is no connection open
+ */
+ function getAnyOpenConnection( $i ) {
+ foreach ( $this->mConns as $type => $conns ) {
+ if ( !empty( $conns[$i] ) ) {
+ return reset( $conns[$i] );
}
}
- wfProfileOut( $fname );
+ return false;
}
/**
* Wait for a given slave to catch up to the master pos stored in $this
*/
function doWait( $index ) {
- global $wgMemc;
-
- $retVal = false;
-
- # Debugging hacks
- if ( isset( $this->mServers[$index]['lagged slave'] ) ) {
+ # Find a connection to wait on
+ $conn = $this->getAnyOpenConnection( $index );
+ if ( !$conn ) {
+ wfDebug( __METHOD__ . ": no connection open\n" );
return false;
- } elseif ( isset( $this->mServers[$index]['fake slave'] ) ) {
- return true;
- }
-
- $key = 'masterpos:' . $index;
- $memcPos = $wgMemc->get( $key );
- if ( $memcPos ) {
- list( $file, $pos ) = explode( ' ', $memcPos );
- # If the saved position is later than the requested position, return now
- if ( $file == $this->mWaitForFile && $this->mWaitForPos <= $pos ) {
- $retVal = true;
- }
}
- if ( !$retVal && $this->isOpen( $index ) ) {
- $conn =& $this->mConnections[$index];
- wfDebug( "Waiting for slave #$index to catch up...\n" );
- $result = $conn->masterPosWait( $this->mWaitForFile, $this->mWaitForPos, $this->mWaitTimeout );
+ wfDebug( __METHOD__.": Waiting for slave #$index to catch up...\n" );
+ $result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout );
- if ( $result == -1 || is_null( $result ) ) {
- # Timed out waiting for slave, use master instead
- wfDebug( "Timed out waiting for slave #$index pos {$this->mWaitForFile} {$this->mWaitForPos}\n" );
- $retVal = false;
- } else {
- $retVal = true;
- wfDebug( "Done\n" );
- }
+ if ( $result == -1 || is_null( $result ) ) {
+ # Timed out waiting for slave, use master instead
+ wfDebug( __METHOD__.": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" );
+ return false;
+ } else {
+ wfDebug( __METHOD__.": Done\n" );
+ return true;
}
- return $retVal;
}
/**
* Get a connection by index
+ * This is the main entry point for this class.
*/
- function &getConnection( $i, $fail = true, $groups = array() )
- {
+ public function &getConnection( $i, $groups = array(), $wiki = false ) {
global $wgDBtype;
- $fname = 'LoadBalancer::getConnection';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
+ if ( $wiki === wfWikiID() ) {
+ $wiki = false;
+ }
# Query groups
if ( !is_array( $groups ) ) {
- $groupIndex = $this->getGroupIndex( $groups );
+ $groupIndex = $this->getReaderIndex( $groups, $wiki );
if ( $groupIndex !== false ) {
+ $serverName = $this->getServerName( $groupIndex );
+ wfDebug( __METHOD__.": using server $serverName for group $groups\n" );
$i = $groupIndex;
}
} else {
foreach ( $groups as $group ) {
- $groupIndex = $this->getGroupIndex( $group );
+ $groupIndex = $this->getReaderIndex( $group, $wiki );
if ( $groupIndex !== false ) {
+ $serverName = $this->getServerName( $groupIndex );
+ wfDebug( __METHOD__.": using server $serverName for group $group\n" );
$i = $groupIndex;
break;
}
}
}
- # For now, only go through all this for mysql databases
- if ($wgDBtype != 'mysql') {
- $i = $this->getWriterIndex();
- }
# Operation-based index
- elseif ( $i == DB_SLAVE ) {
- $i = $this->getReaderIndex();
+ if ( $i == DB_SLAVE ) {
+ $i = $this->getReaderIndex( false, $wiki );
} elseif ( $i == DB_MASTER ) {
$i = $this->getWriterIndex();
} elseif ( $i == DB_LAST ) {
@@ -354,40 +401,168 @@ class LoadBalancer {
if ( $i === false ) {
$this->reportConnectionError( $this->mErrorConnection );
}
+
# Now we have an explicit index into the servers array
- $this->openConnection( $i, $fail );
+ $conn = $this->openConnection( $i, $wiki );
+ if ( !$conn ) {
+ $this->reportConnectionError( $this->mErrorConnection );
+ }
- wfProfileOut( $fname );
- return $this->mConnections[$i];
+ wfProfileOut( __METHOD__ );
+ return $conn;
+ }
+
+ /**
+ * Mark a foreign connection as being available for reuse under a different
+ * DB name or prefix. This mechanism is reference-counted, and must be called
+ * the same number of times as getConnection() to work.
+ */
+ public function reuseConnection( $conn ) {
+ $serverIndex = $conn->getLBInfo('serverIndex');
+ $refCount = $conn->getLBInfo('foreignPoolRefCount');
+ $dbName = $conn->getDBname();
+ $prefix = $conn->tablePrefix();
+ if ( strval( $prefix ) !== '' ) {
+ $wiki = "$dbName-$prefix";
+ } else {
+ $wiki = $dbName;
+ }
+ if ( $serverIndex === null || $refCount === null ) {
+ wfDebug( __METHOD__.": this connection was not opened as a foreign connection\n" );
+ /**
+ * This can happen in code like:
+ * foreach ( $dbs as $db ) {
+ * $conn = $lb->getConnection( DB_SLAVE, array(), $db );
+ * ...
+ * $lb->reuseConnection( $conn );
+ * }
+ * When a connection to the local DB is opened in this way, reuseConnection()
+ * should be ignored
+ */
+ return;
+ }
+ if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
+ throw new MWException( __METHOD__.": connection not found, has the connection been freed already?" );
+ }
+ $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
+ if ( $refCount <= 0 ) {
+ $this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
+ unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
+ wfDebug( __METHOD__.": freed connection $serverIndex/$wiki\n" );
+ } else {
+ wfDebug( __METHOD__.": reference count for $serverIndex/$wiki reduced to $refCount\n" );
+ }
}
/**
* Open a connection to the server given by the specified index
- * Index must be an actual index into the array
- * Returns success
+ * Index must be an actual index into the array.
+ * If the server is already open, returns it.
+ *
+ * On error, returns false, and the connection which caused the
+ * error will be available via $this->mErrorConnection.
+ *
+ * @param integer $i Server index
+ * @param string $wiki Wiki ID to open
+ * @return Database
+ *
* @access private
*/
- function openConnection( $i, $fail = false ) {
- $fname = 'LoadBalancer::openConnection';
- wfProfileIn( $fname );
- $success = true;
+ function openConnection( $i, $wiki = false ) {
+ wfProfileIn( __METHOD__ );
- if ( !$this->isOpen( $i ) ) {
- $this->mConnections[$i] = $this->reallyOpenConnection( $this->mServers[$i] );
+ if ( $wiki !== false ) {
+ return $this->openForeignConnection( $i, $wiki );
}
-
- if ( !$this->isOpen( $i ) ) {
- wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
- if ( $fail ) {
- $this->reportConnectionError( $this->mConnections[$i] );
+ if ( isset( $this->mConns['local'][$i][0] ) ) {
+ $conn = $this->mConns['local'][$i][0];
+ } else {
+ $server = $this->mServers[$i];
+ $server['serverIndex'] = $i;
+ $conn = $this->reallyOpenConnection( $server );
+ if ( $conn->isOpen() ) {
+ $this->mConns['local'][$i][0] = $conn;
+ } else {
+ wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
+ $this->mErrorConnection = $conn;
+ $conn = false;
}
- $this->mErrorConnection = $this->mConnections[$i];
- $this->mConnections[$i] = false;
- $success = false;
}
$this->mLastIndex = $i;
- wfProfileOut( $fname );
- return $success;
+ wfProfileOut( __METHOD__ );
+ return $conn;
+ }
+
+ /**
+ * Open a connection to a foreign DB, or return one if it is already open.
+ *
+ * Increments a reference count on the returned connection which locks the
+ * connection to the requested wiki. This reference count can be
+ * decremented by calling reuseConnection().
+ *
+ * If a connection is open to the appropriate server already, but with the wrong
+ * database, it will be switched to the right database and returned, as long as
+ * it has been freed first with reuseConnection().
+ *
+ * On error, returns false, and the connection which caused the
+ * error will be available via $this->mErrorConnection.
+ *
+ * @param integer $i Server index
+ * @param string $wiki Wiki ID to open
+ * @return Database
+ */
+ function openForeignConnection( $i, $wiki ) {
+ list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
+
+ if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
+ // Reuse an already-used connection
+ $conn = $this->mConns['foreignUsed'][$i][$wiki];
+ wfDebug( __METHOD__.": reusing connection $i/$wiki\n" );
+ } elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
+ // Reuse a free connection for the same wiki
+ $conn = $this->mConns['foreignFree'][$i][$wiki];
+ unset( $this->mConns['foreignFree'][$i][$wiki] );
+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+ wfDebug( __METHOD__.": reusing free connection $i/$wiki\n" );
+ } elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
+ // Reuse a connection from another wiki
+ $conn = reset( $this->mConns['foreignFree'][$i] );
+ $oldWiki = key( $this->mConns['foreignFree'][$i] );
+
+ if ( !$conn->selectDB( $dbName ) ) {
+ global $wguname;
+ $this->mLastError = "Error selecting database $dbName on server " .
+ $conn->getServer() . " from client host {$wguname['nodename']}\n";
+ $this->mErrorConnection = $conn;
+ $conn = false;
+ } else {
+ $conn->tablePrefix( $prefix );
+ unset( $this->mConns['foreignFree'][$i][$oldWiki] );
+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+ wfDebug( __METHOD__.": reusing free connection from $oldWiki for $wiki\n" );
+ }
+ } else {
+ // Open a new connection
+ $server = $this->mServers[$i];
+ $server['serverIndex'] = $i;
+ $server['foreignPoolRefCount'] = 0;
+ $conn = $this->reallyOpenConnection( $server, $dbName );
+ if ( !$conn->isOpen() ) {
+ wfDebug( __METHOD__.": error opening connection for $i/$wiki\n" );
+ $this->mErrorConnection = $conn;
+ $conn = false;
+ } else {
+ $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+ wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" );
+ }
+ }
+
+ // Increment reference count
+ if ( $conn ) {
+ $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
+ $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
+ }
+ return $conn;
}
/**
@@ -398,37 +573,47 @@ class LoadBalancer {
if( !is_integer( $index ) ) {
return false;
}
- if ( array_key_exists( $index, $this->mConnections ) && is_object( $this->mConnections[$index] ) &&
- $this->mConnections[$index]->isOpen() )
- {
- return true;
- } else {
- return false;
- }
+ return (bool)$this->getAnyOpenConnection( $index );
}
/**
- * Really opens a connection
+ * Really opens a connection. Uncached.
+ * Returns a Database object whether or not the connection was successful.
* @access private
*/
- function reallyOpenConnection( &$server ) {
+ function reallyOpenConnection( $server, $dbNameOverride = false ) {
if( !is_array( $server ) ) {
throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
}
extract( $server );
+ if ( $dbNameOverride !== false ) {
+ $dbname = $dbNameOverride;
+ }
+
# Get class for this database type
$class = 'Database' . ucfirst( $type );
# Create object
+ wfDebug( "Connecting to $host...\n" );
$db = new $class( $host, $user, $password, $dbname, 1, $flags );
+ if ( $db->isOpen() ) {
+ wfDebug( "Connected\n" );
+ } else {
+ wfDebug( "Failed\n" );
+ }
$db->setLBInfo( $server );
+ if ( isset( $server['fakeSlaveLag'] ) ) {
+ $db->setFakeSlaveLag( $server['fakeSlaveLag'] );
+ }
+ if ( isset( $server['fakeMaster'] ) ) {
+ $db->setFakeMaster( true );
+ }
return $db;
}
function reportConnectionError( &$conn ) {
- $fname = 'LoadBalancer::reportConnectionError';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
# Prevent infinite recursion
static $reporting = false;
@@ -455,23 +640,13 @@ class LoadBalancer {
}
$reporting = false;
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
function getWriterIndex() {
return 0;
}
- /**
- * Force subsequent calls to getConnection(DB_SLAVE) to return the
- * given index. Set to -1 to restore the original load balancing
- * behaviour. I thought this was a good idea when I originally
- * wrote this class, but it has never been used.
- */
- function force( $i ) {
- $this->mForce = $i;
- }
-
/**
* Returns true if the specified index is a valid server index
*/
@@ -494,54 +669,88 @@ class LoadBalancer {
}
/**
- * Save master pos to the session and to memcached, if the session exists
+ * Get the host name or IP address of the server with the specified index
*/
- function saveMasterPos() {
- if ( session_id() != '' && count( $this->mServers ) > 1 ) {
- # If this entire request was served from a slave without opening a connection to the
- # master (however unlikely that may be), then we can fetch the position from the slave.
- if ( empty( $this->mConnections[0] ) ) {
- $conn =& $this->getConnection( DB_SLAVE );
- list( $file, $pos ) = $conn->getSlavePos();
- wfDebug( "Saving master pos fetched from slave: $file $pos\n" );
- } else {
- $conn =& $this->getConnection( 0 );
- list( $file, $pos ) = $conn->getMasterPos();
- wfDebug( "Saving master pos: $file $pos\n" );
- }
- if ( $file !== false ) {
- $_SESSION['master_log_file'] = $file;
- $_SESSION['master_pos'] = $pos;
- }
+ function getServerName( $i ) {
+ if ( isset( $this->mServers[$i]['hostName'] ) ) {
+ return $this->mServers[$i]['hostName'];
+ } elseif ( isset( $this->mServers[$i]['host'] ) ) {
+ return $this->mServers[$i]['host'];
+ } else {
+ return '';
}
}
/**
- * Loads the master pos from the session, waits for it if necessary
+ * Get the current master position for chronology control purposes
+ * @return mixed
*/
- function loadMasterPos() {
- if ( isset( $_SESSION['master_log_file'] ) && isset( $_SESSION['master_pos'] ) ) {
- $this->waitFor( $_SESSION['master_log_file'], $_SESSION['master_pos'] );
+ function getMasterPos() {
+ # If this entire request was served from a slave without opening a connection to the
+ # master (however unlikely that may be), then we can fetch the position from the slave.
+ $masterConn = $this->getAnyOpenConnection( 0 );
+ if ( !$masterConn ) {
+ $conn = $this->getConnection( DB_SLAVE );
+ $pos = $conn->getSlavePos();
+ wfDebug( "Master pos fetched from slave\n" );
+ } else {
+ $pos = $masterConn->getMasterPos();
+ wfDebug( "Master pos fetched from master\n" );
}
+ return $pos;
}
/**
* Close all open connections
*/
function closeAll() {
- foreach( $this->mConnections as $i => $conn ) {
- if ( $this->isOpen( $i ) ) {
- // Need to use this syntax because $conn is a copy not a reference
- $this->mConnections[$i]->close();
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ $conn->close();
+ }
+ }
+ }
+ $this->mConns = array(
+ 'local' => array(),
+ 'foreignFree' => array(),
+ 'foreignUsed' => array(),
+ );
+ }
+
+ /**
+ * Close a connection
+ * Using this function makes sure the LoadBalancer knows the connection is closed.
+ * If you use $conn->close() directly, the load balancer won't update its state.
+ */
+ function closeConnecton( $conn ) {
+ $done = false;
+ foreach ( $this->mConns as $i1 => $conns2 ) {
+ foreach ( $conns2 as $i2 => $conns3 ) {
+ foreach ( $conns3 as $i3 => $candidateConn ) {
+ if ( $conn === $candidateConn ) {
+ $conn->close();
+ unset( $this->mConns[$i1][$i2][$i3] );
+ $done = true;
+ break;
+ }
+ }
}
}
+ if ( !$done ) {
+ $conn->close();
+ }
}
+ /**
+ * Commit transactions on all open connections
+ */
function commitAll() {
- foreach( $this->mConnections as $i => $conn ) {
- if ( $this->isOpen( $i ) ) {
- // Need to use this syntax because $conn is a copy not a reference
- $this->mConnections[$i]->immediateCommit();
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ $conn->immediateCommit();
+ }
}
}
}
@@ -549,10 +758,15 @@ class LoadBalancer {
/* Issue COMMIT only on master, only if queries were done on connection */
function commitMasterChanges() {
// Always 0, but who knows.. :)
- $i = $this->getWriterIndex();
- if (array_key_exists($i,$this->mConnections)) {
- if ($this->mConnections[$i]->lastQuery() != '') {
- $this->mConnections[$i]->immediateCommit();
+ $masterIndex = $this->getWriterIndex();
+ foreach ( $this->mConns as $type => $conns2 ) {
+ if ( empty( $conns2[$masterIndex] ) ) {
+ continue;
+ }
+ foreach ( $conns2[$masterIndex] as $conn ) {
+ if ( $conn->lastQuery() != '' ) {
+ $conn->commit();
+ }
}
}
}
@@ -574,10 +788,12 @@ class LoadBalancer {
function pingAll() {
$success = true;
- foreach ( $this->mConnections as $i => $conn ) {
- if ( $this->isOpen( $i ) ) {
- if ( !$this->mConnections[$i]->ping() ) {
- $success = false;
+ foreach ( $this->mConns as $conns2 ) {
+ foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns3 as $conn ) {
+ if ( !$conn->ping() ) {
+ $success = false;
+ }
}
}
}
@@ -592,61 +808,74 @@ class LoadBalancer {
$maxLag = -1;
$host = '';
foreach ( $this->mServers as $i => $conn ) {
- if ( $this->openConnection( $i ) ) {
- $lag = $this->mConnections[$i]->getLag();
- if ( $lag > $maxLag ) {
- $maxLag = $lag;
- $host = $this->mServers[$i]['host'];
- }
+ $conn = $this->getAnyOpenConnection( $i );
+ if ( !$conn ) {
+ $conn = $this->openConnection( $i );
+ }
+ if ( !$conn ) {
+ continue;
+ }
+ $lag = $conn->getLag();
+ if ( $lag > $maxLag ) {
+ $maxLag = $lag;
+ $host = $this->mServers[$i]['host'];
}
}
return array( $host, $maxLag );
}
/**
- * Get lag time for each DB
- * Results are cached for a short time in memcached
+ * Get lag time for each server
+ * Results are cached for a short time in memcached, and indefinitely in the process cache
*/
function getLagTimes() {
wfProfileIn( __METHOD__ );
- $expiry = 5;
- $requestRate = 10;
-
- global $wgMemc;
- $times = $wgMemc->get( wfMemcKey( 'lag_times' ) );
- if ( $times ) {
- # Randomly recache with probability rising over $expiry
- $elapsed = time() - $times['timestamp'];
- $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
- if ( mt_rand( 0, $chance ) != 0 ) {
- unset( $times['timestamp'] );
- wfProfileOut( __METHOD__ );
- return $times;
+
+ if ( !isset( $this->mLagTimes ) ) {
+ $expiry = 5;
+ $requestRate = 10;
+
+ global $wgMemc;
+ $masterName = $this->getServerName( 0 );
+ $memcKey = wfMemcKey( 'lag_times', $masterName );
+ $times = $wgMemc->get( $memcKey );
+ if ( $times ) {
+ # Randomly recache with probability rising over $expiry
+ $elapsed = time() - $times['timestamp'];
+ $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
+ if ( mt_rand( 0, $chance ) != 0 ) {
+ unset( $times['timestamp'] );
+ wfProfileOut( __METHOD__ );
+ return $times;
+ }
+ wfIncrStats( 'lag_cache_miss_expired' );
+ } else {
+ wfIncrStats( 'lag_cache_miss_absent' );
}
- wfIncrStats( 'lag_cache_miss_expired' );
- } else {
- wfIncrStats( 'lag_cache_miss_absent' );
- }
- # Cache key missing or expired
+ # Cache key missing or expired
- $times = array();
- foreach ( $this->mServers as $i => $conn ) {
- if ($i==0) { # Master
- $times[$i] = 0;
- } elseif ( $this->openConnection( $i ) ) {
- $times[$i] = $this->mConnections[$i]->getLag();
+ $times = array();
+ foreach ( $this->mServers as $i => $conn ) {
+ if ($i == 0) { # Master
+ $times[$i] = 0;
+ } elseif ( false !== ( $conn = $this->getAnyOpenConnection( $i ) ) ) {
+ $times[$i] = $conn->getLag();
+ } elseif ( false !== ( $conn = $this->openConnection( $i ) ) ) {
+ $times[$i] = $conn->getLag();
+ }
}
- }
- # Add a timestamp key so we know when it was cached
- $times['timestamp'] = time();
- $wgMemc->set( wfMemcKey( 'lag_times' ), $times, $expiry );
+ # Add a timestamp key so we know when it was cached
+ $times['timestamp'] = time();
+ $wgMemc->set( $memcKey, $times, $expiry );
- # But don't give the timestamp to the caller
- unset($times['timestamp']);
+ # But don't give the timestamp to the caller
+ unset($times['timestamp']);
+ $this->mLagTimes = $times;
+ }
wfProfileOut( __METHOD__ );
- return $times;
+ return $this->mLagTimes;
}
}
diff --git a/includes/Setup.php b/includes/Setup.php
index 53e0b94963..44137410f3 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -213,20 +213,6 @@ if( !$wgCommandLineMode && ( $wgRequest->checkSessionCookie() || isset( $_COOKIE
wfProfileOut( $fname.'-SetupSession' );
wfProfileIn( $fname.'-globals' );
-if ( !$wgDBservers ) {
- $wgDBservers = array(array(
- 'host' => $wgDBserver,
- 'user' => $wgDBuser,
- 'password' => $wgDBpassword,
- 'dbname' => $wgDBname,
- 'type' => $wgDBtype,
- 'load' => 1,
- 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
- ));
-}
-
-$wgLoadBalancer = new StubObject( 'wgLoadBalancer', 'LoadBalancer',
- array( $wgDBservers, false, $wgMasterWaitTimeout, true ) );
$wgContLang = new StubContLang;
// Now that variant lists may be available...
diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php
index beeeaf152b..d9629a0dd3 100644
--- a/includes/SiteConfiguration.php
+++ b/includes/SiteConfiguration.php
@@ -126,7 +126,11 @@ class SiteConfiguration {
$site = NULL;
$lang = NULL;
foreach ( $this->suffixes as $suffix ) {
- if ( substr( $db, -strlen( $suffix ) ) == $suffix ) {
+ if ( $suffix === '' ) {
+ $site = '';
+ $lang = $db;
+ break;
+ } elseif ( substr( $db, -strlen( $suffix ) ) == $suffix ) {
$site = $suffix == 'wiki' ? 'wikipedia' : $suffix;
$lang = substr( $db, 0, strlen( $db ) - strlen( $suffix ) );
break;
diff --git a/includes/Skin.php b/includes/Skin.php
index 5ba8fdb1ab..4aaf86122f 100644
--- a/includes/Skin.php
+++ b/includes/Skin.php
@@ -1184,7 +1184,7 @@ END;
}
function lastModified() {
- global $wgLang, $wgArticle, $wgLoadBalancer;
+ global $wgLang, $wgArticle;
$timestamp = $wgArticle->getTimestamp();
if ( $timestamp ) {
@@ -1194,7 +1194,7 @@ END;
} else {
$s = '';
}
- if ( $wgLoadBalancer->getLaggedSlaveMode() ) {
+ if ( wfGetLB()->getLaggedSlaveMode() ) {
$s .= ' ' . wfMsg( 'laggedslavemode' ) . '';
}
return $s;
diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php
index 1e1684a095..9096744e0d 100644
--- a/includes/UserRightsProxy.php
+++ b/includes/UserRightsProxy.php
@@ -72,25 +72,12 @@ class UserRightsProxy {
// Hmm... this shouldn't happen though. :)
return wfGetDB( DB_MASTER );
} else {
- global $wgDBuser, $wgDBpassword;
- $server = self::getMaster( $database );
- return new Database( $server, $wgDBuser, $wgDBpassword, $database );
+ return wfGetDB( DB_MASTER, array(), $database );
}
}
return null;
}
- /**
- * Return the master server to connect to for the requested database.
- */
- private static function getMaster( $database ) {
- global $wgDBserver, $wgAlternateMaster;
- if( isset( $wgAlternateMaster[$database] ) ) {
- return $wgAlternateMaster[$database];
- }
- return $wgDBserver;
- }
-
public function getId() {
return $this->id;
}
@@ -158,4 +145,4 @@ class UserRightsProxy {
}
}
-?>
\ No newline at end of file
+?>
diff --git a/includes/Wiki.php b/includes/Wiki.php
index 33fc83b0d1..8dfe4dfed2 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -70,13 +70,12 @@ class MediaWiki {
* Check if the maximum lag of database slaves is higher that $maxLag, and
* if it's the case, output an error message
*
- * @param LoadBalancer $loadBalancer
* @param int $maxLag maximum lag allowed for the request, as supplied by
* the client
- * @return bool true if the requet can continue
+ * @return bool true if the request can continue
*/
- function checkMaxLag( $loadBalancer, $maxLag ) {
- list( $host, $lag ) = $loadBalancer->getMaxLag();
+ function checkMaxLag( $maxLag ) {
+ list( $host, $lag ) = wfGetLB()->getMaxLag();
if ( $lag > $maxLag ) {
wfMaxlagError( $host, $lag, $maxLag );
return false;
@@ -316,20 +315,18 @@ class MediaWiki {
}
/**
- * Cleaning up by doing deferred updates, calling loadbalancer and doing the
- * output
+ * Cleaning up by doing deferred updates, calling LBFactory and doing the output
*
* @param Array $deferredUpdates array of updates to do
- * @param LoadBalancer $loadBalancer
* @param OutputPage $output
*/
- function finalCleanup( &$deferredUpdates, &$loadBalancer, &$output ) {
+ function finalCleanup ( &$deferredUpdates, &$output ) {
wfProfileIn( __METHOD__ );
$this->doUpdates( $deferredUpdates );
$this->doJobs();
- $loadBalancer->saveMasterPos();
# Now commit any transactions, so that unreported errors after output() don't roll back the whole thing
- $loadBalancer->commitMasterChanges();
+ $factory = wfGetLBFactory();
+ $factory->shutdown();
$output->output();
wfProfileOut( __METHOD__ );
}
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index 9ea7036a6c..d93371096d 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -320,9 +320,9 @@ class ApiMain extends ApiBase {
if( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
// Check for maxlag
- global $wgLoadBalancer, $wgShowHostnames;
+ global $wgShowHostnames;
$maxLag = $params['maxlag'];
- list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
+ list( $host, $lag ) = wfGetLB()->getMaxLag();
if ( $lag > $maxLag ) {
if( $wgShowHostnames ) {
ApiBase :: dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index d070411527..c79e2fae92 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -185,7 +185,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
protected function appendDbReplLagInfo($property, $includeAll) {
- global $wgLoadBalancer, $wgShowHostnames;
+ global $wgShowHostnames;
$data = array();
@@ -194,14 +194,14 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$this->dieUsage('Cannot view all servers info unless $wgShowHostnames is true', 'includeAllDenied');
global $wgDBservers;
- $lags = $wgLoadBalancer->getLagTimes();
+ $lags = wfGetLB()->getLagTimes();
foreach( $lags as $i => $lag ) {
$data[] = array (
'host' => $wgDBservers[$i]['host'],
'lag' => $lag);
}
} else {
- list( $host, $lag ) = $wgLoadBalancer->getMaxLag();
+ list( $host, $lag ) = wfGetLB()->getMaxLag();
$data[] = array (
'host' => $wgShowHostnames ? $host : '',
'lag' => $lag);
diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php
new file mode 100644
index 0000000000..f10ac97e0f
--- /dev/null
+++ b/includes/filerepo/ForeignDBViaLBRepo.php
@@ -0,0 +1,37 @@
+wiki = $info['wiki'];
+ list( $this->dbName, $this->tablePrefix ) = wfSplitWikiID( $this->wiki );
+ $this->hasSharedCache = $info['hasSharedCache'];
+ }
+
+ function getMasterDB() {
+ return wfGetDB( DB_MASTER, array(), $this->wiki );
+ }
+
+ function getSlaveDB() {
+ return wfGetDB( DB_SLAVE, array(), $this->wiki );
+ }
+ function hasSharedCache() {
+ return $this->hasSharedCache;
+ }
+
+ function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+ function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+ function deleteBatch( $fileMap ) {
+ throw new MWException( get_class($this) . ': write operations are not supported' );
+ }
+}
diff --git a/index.php b/index.php
index c7432e42a7..d5a42a3e0f 100644
--- a/index.php
+++ b/index.php
@@ -47,7 +47,7 @@ OutputPage::setEncodings(); # Not really used yet
$maxLag = $wgRequest->getVal( 'maxlag' );
if ( !is_null( $maxLag ) ) {
- if ( !$mediaWiki->checkMaxLag( $wgLoadBalancer, $maxLag ) ) {
+ if ( !$mediaWiki->checkMaxLag( $maxLag ) ) {
exit;
}
}
@@ -69,7 +69,7 @@ if ( $wgUseAjax && $action == 'ajax' ) {
$dispatcher = new AjaxDispatcher();
$dispatcher->performAction();
- $mediaWiki->restInPeace( $wgLoadBalancer );
+ $mediaWiki->restInPeace();
exit;
}
@@ -90,7 +90,7 @@ $mediaWiki->setVal( 'UseExternalEditor', $wgUseExternalEditor );
$mediaWiki->setVal( 'UsePathInfo', $wgUsePathInfo );
$mediaWiki->initialize( $wgTitle, $wgArticle, $wgOut, $wgUser, $wgRequest );
-$mediaWiki->finalCleanup( $wgDeferredUpdateList, $wgLoadBalancer, $wgOut );
+$mediaWiki->finalCleanup ( $wgDeferredUpdateList, $wgOut );
# Not sure when $wgPostCommitUpdateList gets set, so I keep this separate from finalCleanup
$mediaWiki->doUpdates( $wgPostCommitUpdateList );
diff --git a/maintenance/eval.php b/maintenance/eval.php
index 519411df8c..605576e450 100644
--- a/maintenance/eval.php
+++ b/maintenance/eval.php
@@ -33,8 +33,9 @@ if ( isset( $options['d'] ) ) {
$wgDebugLogFile = '/dev/stdout';
}
if ( $d > 1 ) {
- foreach ( $wgLoadBalancer->mServers as $i => $server ) {
- $wgLoadBalancer->mServers[$i]['flags'] |= DBO_DEBUG;
+ $lb = wfGetLB();
+ foreach ( $lb->mServers as $i => $server ) {
+ $lb->mServers[$i]['flags'] |= DBO_DEBUG;
}
}
if ( $d > 2 ) {
diff --git a/maintenance/fixSlaveDesync.php b/maintenance/fixSlaveDesync.php
index daa5bbb47b..a3f22cea75 100644
--- a/maintenance/fixSlaveDesync.php
+++ b/maintenance/fixSlaveDesync.php
@@ -7,13 +7,13 @@ require_once( 'commandLine.inc' );
$slaveIndexes = array();
for ( $i = 1; $i < count( $wgDBservers ); $i++ ) {
- if ( $wgLoadBalancer->isNonZeroLoad( $i ) ) {
+ if ( wfGetLB()->isNonZeroLoad( $i ) ) {
$slaveIndexes[] = $i;
}
}
/*
-foreach ( $wgLoadBalancer->mServers as $i => $server ) {
- $wgLoadBalancer->mServers[$i]['flags'] |= DBO_DEBUG;
+foreach ( wfGetLB()->mServers as $i => $server ) {
+ wfGetLB()->mServers[$i]['flags'] |= DBO_DEBUG;
}*/
$reportingInterval = 1000;
diff --git a/maintenance/getLagTimes.php b/maintenance/getLagTimes.php
index d39345925a..a3268eec11 100644
--- a/maintenance/getLagTimes.php
+++ b/maintenance/getLagTimes.php
@@ -2,13 +2,15 @@
require 'commandLine.inc';
-if( empty( $wgDBservers ) ) {
+$lb = wfGetLB();
+
+if( $lb->getServerCount() == 1 ) {
echo "This script dumps replication lag times, but you don't seem to have\n";
echo "a multi-host db server configuration.\n";
} else {
- $lags = $wgLoadBalancer->getLagTimes();
+ $lags = $lb->getLagTimes();
foreach( $lags as $n => $lag ) {
- $host = $wgDBservers[$n]["host"];
+ $host = $lb->getServerName( $n );
if( IP::isValid( $host ) ) {
$ip = $host;
$host = gethostbyaddr( $host );
diff --git a/maintenance/getSlaveServer.php b/maintenance/getSlaveServer.php
index 9aca104300..97260591a5 100644
--- a/maintenance/getSlaveServer.php
+++ b/maintenance/getSlaveServer.php
@@ -4,10 +4,11 @@ require_once( dirname(__FILE__).'/commandLine.inc' );
if( isset( $options['group'] ) ) {
$db = wfGetDB( DB_SLAVE, $options['group'] );
- $host = $db->getProperty( 'mServer' );
+ $host = $db->getServer();
} else {
- $i = $wgLoadBalancer->getReaderIndex();
- $host = $wgDBservers[$i]['host'];
+ $lb = wfGetLB();
+ $i = $lb->getReaderIndex();
+ $host = $lb->getServerName( $i );
}
print "$host\n";
diff --git a/maintenance/nextJobDB.php b/maintenance/nextJobDB.php
index b2500caf78..b9e44939fe 100644
--- a/maintenance/nextJobDB.php
+++ b/maintenance/nextJobDB.php
@@ -21,19 +21,13 @@ if ( !$pendingDBs ) {
$pendingDBs = array();
# Cross-reference DBs by master DB server
$dbsByMaster = array();
- $defaultMaster = isset( $wgAlternateMaster['DEFAULT'] )
- ? $wgAlternateMaster['DEFAULT']
- : $wgDBserver;
foreach ( $wgLocalDatabases as $db ) {
- if ( isset( $wgAlternateMaster[$db] ) ) {
- $dbsByMaster[$wgAlternateMaster[$db]][] = $db;
- } else {
- $dbsByMaster[$defaultMaster][] = $db;
- }
+ $lb = wfGetLB( $db );
+ $dbsByMaster[$lb->getServerName(0)][] = $db;
}
foreach ( $dbsByMaster as $master => $dbs ) {
- $dbConn = new Database( $master, $wgDBuser, $wgDBpassword, $dbs[0] );
+ $dbConn = wfGetDB( DB_MASTER, array(), $dbs[0] );
$stype = $dbConn->addQuotes($type);
# Padding row for MySQL bug
diff --git a/maintenance/updateSpecialPages.php b/maintenance/updateSpecialPages.php
index 5e0f2cebe1..94b5f20598 100644
--- a/maintenance/updateSpecialPages.php
+++ b/maintenance/updateSpecialPages.php
@@ -73,12 +73,12 @@ foreach ( $wgQueryPages as $page ) {
}
# Reopen any connections that have closed
- if ( !$wgLoadBalancer->pingAll()) {
+ if ( !wfGetLB()->pingAll()) {
print "\n";
do {
print "Connection failed, reconnecting in 10 seconds...\n";
sleep(10);
- } while ( !$wgLoadBalancer->pingAll() );
+ } while ( !wfGetLB()->pingAll() );
print "Reconnected\n\n";
} else {
# Commit the results
diff --git a/maintenance/waitForSlave.php b/maintenance/waitForSlave.php
index 73a473649b..4e83b4bfad 100644
--- a/maintenance/waitForSlave.php
+++ b/maintenance/waitForSlave.php
@@ -1,12 +1,5 @@
mServers as $i => $server ) {
- if ( $server['host'] == '10.0.0.29' ) {
- unset($wgLoadBalancer->mServers[$i]);
- }
-}
if ( isset( $args[0] ) ) {
wfWaitForSlaves($args[0]);
} else {