* Introduced LBFactory -- an abstract class for configuring database load balancers...
authorTim Starling <tstarling@users.mediawiki.org>
Sun, 30 Mar 2008 09:48:15 +0000 (09:48 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Sun, 30 Mar 2008 09:48:15 +0000 (09:48 +0000)
* Wrote two concrete implementations. LBFactory_Simple is for general installations. LBFactory_Multi will replace the runtime configuration used on Wikimedia and allow load-balanced connections to any DB.
* Ported Special:Userrights, CentralAuth and OAI audit to the LBFactory system.
* Added ForeignDBViaLBRepo, a file repository which uses LBFactory.
* Removed $wgLoadBalancer and $wgAlternateMaster
* Improved the query group concept to allow failover and lag control
* Improved getReaderIndex(), it will now try all servers before waiting, instead of waiting after each.
* Removed the $fail parameter to getConnection(), obsolete.
* Removed the useless force() function.
* Abstracted the replication position interface to allow for future non-MySQL support.
* Rearranged Database.php. Added a few debugging features.
* Removed ancient benet-specific hack from waitForSlave.php

27 files changed:
includes/AutoLoader.php
includes/BagOStuff.php
includes/Database.php
includes/DatabaseOracle.php
includes/DatabasePostgres.php
includes/DefaultSettings.php
includes/ExternalStoreDB.php
includes/GlobalFunctions.php
includes/LBFactory.php [new file with mode: 0644]
includes/LBFactory_Multi.php [new file with mode: 0644]
includes/LoadBalancer.php
includes/Setup.php
includes/SiteConfiguration.php
includes/Skin.php
includes/UserRightsProxy.php
includes/Wiki.php
includes/api/ApiMain.php
includes/api/ApiQuerySiteinfo.php
includes/filerepo/ForeignDBViaLBRepo.php [new file with mode: 0644]
index.php
maintenance/eval.php
maintenance/fixSlaveDesync.php
maintenance/getLagTimes.php
maintenance/getSlaveServer.php
maintenance/nextJobDB.php
maintenance/updateSpecialPages.php
maintenance/waitForSlave.php

index be0759b..9e4e94a 100644 (file)
@@ -32,6 +32,7 @@ function __autoload($className) {
                'CategoryViewer' => 'includes/CategoryPage.php',
                'ChangesList' => 'includes/ChangesList.php',
                'ChannelFeed' => 'includes/Feed.php',
+               'ChronologyProtector' => 'includes/LBFactory.php',
                'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php',
                'ContributionsPage' => 'includes/SpecialContributions.php',
                'CoreParserFunctions' => 'includes/CoreParserFunctions.php',
@@ -120,6 +121,9 @@ function __autoload($className) {
                'IP' => 'includes/IP.php',
                'IPUnblockForm' => 'includes/SpecialIpblocklist.php',
                'Job' => 'includes/JobQueue.php',
+               'LBFactory' => 'includes/LBFactory.php',
+               'LBFactory_Multi' => 'includes/LBFactory_Multi.php',
+               'LBFactory_Simple' => 'includes/LBFactory.php',
                'License' => 'includes/Licenses.php',
                'Licenses' => 'includes/Licenses.php',
                'LinkBatch' => 'includes/LinkBatch.php',
@@ -159,6 +163,7 @@ function __autoload($className) {
                'MWException' => 'includes/Exception.php',
                'MWNamespace' => 'includes/Namespace.php',
                'MySQLSearchResultSet' => 'includes/SearchMySQL.php',
+               'MySQLMasterPos' => 'includes/Database.php',
                'Namespace' => 'includes/NamespaceCompat.php', // Compat
                'NewbieContributionsPage' => 'includes/SpecialNewbieContributions.php',
                'NewPagesPage' => 'includes/SpecialNewpages.php',
@@ -289,6 +294,7 @@ function __autoload($className) {
                'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
                'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
                'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
+               'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php',
                'FSRepo' => 'includes/filerepo/FSRepo.php',
                'Image' => 'includes/filerepo/LocalFile.php',
                'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
index 226abb3..1cc7ce3 100644 (file)
@@ -645,6 +645,7 @@ class DBABagOStuff extends BagOStuff {
                }
                $this->mFile = "$dir/mw-cache-" . wfWikiID();
                $this->mFile .= '.db';
+               wfDebug( __CLASS__.": using cache file {$this->mFile}\n" );
                $this->mHandler = $handler;
        }
 
index e2f15b2..3581abf 100644 (file)
@@ -11,2343 +11,2415 @@ define( 'DEADLOCK_DELAY_MIN', 500000 );
 /** Maximum time to wait before retry */
 define( 'DEADLOCK_DELAY_MAX', 1500000 );
 
-/******************************************************************************
- * Utility classes
- *****************************************************************************/
-
 /**
- * Utility class.
+ * Database abstraction object
  * @addtogroup Database
  */
-class DBObject {
-       public $mData;
+class Database {
 
-       function DBObject($data) {
-               $this->mData = $data;
+#------------------------------------------------------------------------------
+# Variables
+#------------------------------------------------------------------------------
+
+       protected $mLastQuery = '';
+
+       protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
+       protected $mOut, $mOpened = false;
+
+       protected $mFailFunction;
+       protected $mTablePrefix;
+       protected $mFlags;
+       protected $mTrxLevel = 0;
+       protected $mErrorCount = 0;
+       protected $mLBInfo = array();
+       protected $mFakeSlaveLag = null, $mFakeMaster = false;
+
+#------------------------------------------------------------------------------
+# Accessors
+#------------------------------------------------------------------------------
+       # These optionally set a variable and return the previous state
+
+       /**
+        * 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 );
        }
 
-       function isLOB() {
-               return false;
+       /**
+        * Output page, used for reporting errors
+        * FALSE means discard output
+        */
+       function setOutputPage( $out ) {
+               $this->mOut = $out;
        }
 
-       function data() {
-               return $this->mData;
+       /**
+        * Boolean, controls output of large amounts of debug information
+        */
+       function debug( $debug = NULL ) {
+               return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
        }
-};
 
-/**
- * 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;
+       /**
+        * 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 );
+               } else {
+                       return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
+               }
        }
-       function fetch() {
-               return $this->mData;
+
+       /**
+        * 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.
+        */
+       function ignoreErrors( $ignoreErrors = NULL ) {
+               return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
        }
-};
 
-/**
- * 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;
+       /**
+        * The current depth of nested transactions
+        * @param $level Integer: , default NULL.
+        */
+       function trxLevel( $level = NULL ) {
+               return wfSetVar( $this->mTrxLevel, $level );
        }
 
-       function name() {
-               return $this->name;
+       /**
+        * Number of errors logged, only useful when errors are ignored
+        */
+       function errorCount( $count = NULL ) {
+               return wfSetVar( $this->mErrorCount, $count );
        }
 
-       function tableName() {
-               return $this->tableName;
+       function tablePrefix( $prefix = null ) {
+               return wfSetVar( $this->mTablePrefix, $prefix );
        }
 
-       function defaultValue() {
-               return $this->default;
+       /**
+        * 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];
+                       } else {
+                               return NULL;
+                       }
+               }
        }
 
-       function maxLength() {
-               return $this->max_length;
+       function setLBInfo( $name, $value = NULL ) {
+               if ( is_null( $value ) ) {
+                       $this->mLBInfo = $name;
+               } else {
+                       $this->mLBInfo[$name] = $value;
+               }
        }
 
-       function nullable() {
-               return $this->nullable;
+       /**
+        * Set lag time in seconds for a fake slave
+        */
+       function setFakeSlaveLag( $lag ) {
+               $this->mFakeSlaveLag = $lag;
        }
 
-       function isKey() {
-               return $this->is_key;
+       /**
+        * Make this connection a fake master
+        */
+       function setFakeMaster( $enabled = true ) {
+               $this->mFakeMaster = $enabled;
        }
 
-       function isMultipleKey() {
-               return $this->is_multiple;
+       /**
+        * Returns true if this database supports (and uses) cascading deletes
+        */
+       function cascadingDeletes() {
+               return false;
        }
 
-       function type() {
-               return $this->type;
+       /**
+        * Returns true if this database supports (and uses) triggers (e.g. on the page table)
+        */
+       function cleanupTriggers() {
+               return false;
        }
-}
 
-/******************************************************************************
- * Error classes
- *****************************************************************************/
+       /**
+        * 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.
+        */
+       function strictIPs() {
+               return false;
+       }
 
-/**
* Database error base class
- * @addtogroup Database
- */
-class DBError extends MWException {
-       public $db;
+       /**
       * Returns true if this database uses timestamps rather than integers
+       */
+       function realTimestamps() {
+               return false;
+       }
 
        /**
-        * 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
+        * Returns true if this database does an implicit sort when doing GROUP BY
         */
-       function __construct( Database &$db, $error ) {
-               $this->db =& $db;
-               parent::__construct( $error );
+       function implicitGroupby() {
+               return true;
        }
-}
 
-/**
- * @addtogroup Database
- */
-class DBConnectionError extends DBError {
-       public $error;
-       
-       function __construct( Database &$db, $error = 'unknown error' ) {
-               $msg = 'DB connection error';
-               if ( trim( $error ) != '' ) {
-                       $msg .= ": $error";
-               }
-               $this->error = $error;
-               parent::__construct( $db, $msg );
+       /**
+        * 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
+        */
+       function implicitOrderby() {
+               return true;
        }
 
-       function useOutputPage() {
-               // Not likely to work
+       /**
+        * 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';
+        */
+       function searchableIPs() {
                return false;
        }
 
-       function useMessageCache() {
-               // Not likely to work
+       /**
+        * Returns true if this database can use functional indexes
+        */
+       function functionalIndexes() {
                return false;
        }
-       
-       function getText() {
-               return $this->getMessage() . "\n";
-       }
 
-       function getLogMessage() {
-               # Don't send to the exception log
-               return false;
+       /**#@+
+        * Get function
+        */
+       function lastQuery() { return $this->mLastQuery; }
+       function isOpen() { return $this->mOpened; }
+       /**#@-*/
+
+       function setFlag( $flag ) {
+               $this->mFlags |= $flag;
        }
 
-       function getPageTitle() {
-               global $wgSitename;
-               return "$wgSitename has a problem";
+       function clearFlag( $flag ) {
+               $this->mFlags &= ~$flag;
        }
 
-       function getHTML() {
-               global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
-               global $wgSitename, $wgServer, $wgMessageCache;
+       function getFlag( $flag ) {
+               return !!($this->mFlags & $flag);
+       }
 
-               # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
-               # Hard coding strings instead.
-
-               $noconnect = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
-               $mainpage = 'Main Page';
-               $searchdisabled = <<<EOT
-<p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
-<span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
-EOT;
+       /**
+        * General read-only accessor
+        */
+       function getProperty( $name ) {
+               return $this->$name;
+       }
 
-               $googlesearch = "
-<!-- SiteSearch Google -->
-<FORM method=GET action=\"http://www.google.com/search\">
-<TABLE bgcolor=\"#FFFFFF\"><tr><td>
-<A HREF=\"http://www.google.com/\">
-<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
-border=\"0\" ALT=\"Google\"></A>
-</td>
-<td>
-<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
-<INPUT type=submit name=btnG VALUE=\"Google Search\">
-<font size=-1>
-<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
-<input type='hidden' name='ie' value='$2'>
-<input type='hidden' name='oe' value='$2'>
-</font>
-</td></tr></TABLE>
-</FORM>
-<!-- SiteSearch Google -->";
-               $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
+#------------------------------------------------------------------------------
+# Other functions
+#------------------------------------------------------------------------------
 
-               # No database access
-               if ( is_object( $wgMessageCache ) ) {
-                       $wgMessageCache->disable();
-               }
+       /**@{{
+        * 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
+        */
+       function __construct( $server = false, $user = false, $password = false, $dbName = false,
+               $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
 
-               if ( trim( $this->error ) == '' ) {
-                       $this->error = $this->db->getProperty('mServer');
+               global $wgOut, $wgDBprefix, $wgCommandLineMode;
+               # Can't get a reference if it hasn't been set yet
+               if ( !isset( $wgOut ) ) {
+                       $wgOut = NULL;
                }
+               $this->mOut =& $wgOut;
 
-               $text = str_replace( '$1', $this->error, $noconnect );
-               $text .= wfGetSiteNotice();
+               $this->mFailFunction = $failFunction;
+               $this->mFlags = $flags;
 
-               if($wgUseFileCache) {
-                       if($wgTitle) {
-                               $t =& $wgTitle;
+               if ( $this->mFlags & DBO_DEFAULT ) {
+                       if ( $wgCommandLineMode ) {
+                               $this->mFlags &= ~DBO_TRX;
                        } 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 );
-                               }
-                       }
-
-                       $cache = new HTMLFileCache( $t );
-                       if( $cache->isFileCached() ) {
-                               // @todo, FIXME: $msg is not defined on the next line.
-                               $msg = '<p style="color: red"><b>'.$msg."<br />\n" .
-                                       $cachederror . "</b></p>\n";
-
-                               $tag = '<div id="article">';
-                               $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 = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
+               $mainpage = 'Main Page';
+               $searchdisabled = <<<EOT
+<p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
+<span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
+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 = "
+<!-- SiteSearch Google -->
+<FORM method=GET action=\"http://www.google.com/search\">
+<TABLE bgcolor=\"#FFFFFF\"><tr><td>
+<A HREF=\"http://www.google.com/\">
+<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
+border=\"0\" ALT=\"Google\"></A>
+</td>
+<td>
+<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
+<INPUT type=submit name=btnG VALUE=\"Google Search\">
+<font size=-1>
+<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
+<input type='hidden' name='ie' value='$2'>
+<input type='hidden' name='oe' value='$2'>
+</font>
+</td></tr></TABLE>
+</FORM>
+<!-- SiteSearch Google -->";
+               $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 = '<p style="color: red"><b>'.$msg."<br />\n" .
+                                       $cachederror . "</b></p>\n";
+
+                               $tag = '<div id="article">';
+                               $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}";
+       }
+}
index 3848548..06fab29 100644 (file)
@@ -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
 
 
index 0121371..2cdc342 100644 (file)
@@ -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 );
        }
index 7be844a..6977b8b 100644 (file)
@@ -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
index f9046f7..ab9cc8d 100644 (file)
@@ -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.*/
index 7acd3e8..17dd351 100644 (file)
@@ -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 (file)
index 0000000..ba509f7
--- /dev/null
@@ -0,0 +1,211 @@
+<?php
+
+/**
+ * An interface for generating database load balancers
+ */
+abstract class LBFactory {
+       static $instance;
+
+       /**
+        * Get an LBFactory instance
+        */
+       static function &singleton() {
+               if ( is_null( self::$instance ) ) {
+                       global $wgLBFactoryConf;
+                       $class = $wgLBFactoryConf['class'];
+                       self::$instance = new $class( $wgLBFactoryConf );
+               }
+               return self::$instance;
+       }
+
+       /**
+        * Construct a factory based on a configuration array (typically from $wgLBFactoryConf) 
+        */
+       abstract function __construct( $conf );
+
+       /**
+        * Get a load balancer object.
+        *
+        * @param string $wiki Wiki ID, or false for the current wiki
+        * @return LoadBalancer
+        */
+       abstract function getMainLB( $wiki = false );
+
+       /*
+        * Get a load balancer for external storage
+        *
+        * @param string $cluster External storage cluster, or false for core
+        * @param string $wiki Wiki ID, or false for the current wiki
+        */
+       abstract function getExternalLB( $cluster, $wiki = false );
+
+       /**
+        * 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.
+        */
+       abstract function forEachLB( $callback, $params = array() );
+
+       /**
+        * Prepare all load balancers for shutdown
+        * STUB
+        */
+       function shutdown() {}
+
+       /**
+        * Call a method of each load balancer
+        */
+       function forEachLBCallMethod( $methodName, $args = array() ) {
+               $this->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 (file)
index 0000000..aee033a
--- /dev/null
@@ -0,0 +1,221 @@
+<?php
+
+/**
+ * A multi-wiki, multi-master factory for Wikimedia and similar installations.
+ * Ignores the old configuration globals
+ *
+ * Configuration: 
+ *     sectionsByDB                       A map of database names to section names
+ *
+ *     sectionLoads                A 2-d map. For each section, gives a map of server names to load ratios.
+ *                                 For example: array( 'section1' => 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();
+       }
+}
+
index 0cdadd1..d6f3d76 100644 (file)
@@ -1,32 +1,29 @@
 <?php
-/**
- *
- */
-
-
 /**
  * Database load balancing object
  *
  * @todo document
  */
 class LoadBalancer {
-       /* private */ var $mServers, $mConnections, $mLoads, $mGroupLoads;
+       /* private */ var $mServers, $mConns, $mLoads, $mGroupLoads;
        /* private */ var $mFailFunction, $mErrorConnection;
-       /* private */ var $mForce, $mReadIndex, $mLastIndex, $mAllowLagged;
-       /* private */ var $mWaitForFile, $mWaitForPos, $mWaitTimeout;
+       /* private */ var $mReadIndex, $mLastIndex, $mAllowLagged;
+       /* private */ var $mWaitForPos, $mWaitTimeout;
        /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
+       /* private */ var $mParentInfo, $mLagTimes;
 
-       function __construct( $servers, $failFunction = false, $waitTimeout = 10, $waitForMasterNow = false )
+       function __construct( $servers, $failFunction = false, $waitTimeout = 10, $unused = false )
        {
                $this->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;
        }
 }
 
index 53e0b94..4413741 100644 (file)
@@ -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...
index beeeaf1..d9629a0 100644 (file)
@@ -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;
index 5ba8fdb..4aaf861 100644 (file)
@@ -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 .= ' <strong>' . wfMsg( 'laggedslavemode' ) . '</strong>';
                }
                return $s;
index 1e1684a..9096744 100644 (file)
@@ -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
+?>
index 33fc83b..8dfe4df 100644 (file)
@@ -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__ );
        }
index 9ea7036..d933710 100644 (file)
@@ -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' );
index d070411..c79e2fa 100644 (file)
@@ -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 (file)
index 0000000..f10ac97
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * A foreign repository with a MediaWiki database accessible via the configured LBFactory
+ */
+class ForeignDBViaLBRepo extends LocalRepo {
+       var $wiki, $dbName, $tablePrefix;
+       var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' );
+
+       function __construct( $info ) {
+               parent::__construct( $info );
+               $this->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' );
+       }
+}
index c7432e4..d5a42a3 100644 (file)
--- 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 );
index 519411d..605576e 100644 (file)
@@ -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 ) {
index daa5bbb..a3f22ce 100644 (file)
@@ -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;
 
index d393459..a3268ee 100644 (file)
@@ -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 );
index 9aca104..9726059 100644 (file)
@@ -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";
index b2500ca..b9e4493 100644 (file)
@@ -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
index 5e0f2ce..94b5f20 100644 (file)
@@ -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
index 73a4736..4e83b4b 100644 (file)
@@ -1,12 +1,5 @@
 <?php
 require_once( "commandLine.inc" );
-
-# Don't wait for benet
-foreach ( $wgLoadBalancer->mServers as $i => $server ) {
-       if ( $server['host'] == '10.0.0.29' ) {
-               unset($wgLoadBalancer->mServers[$i]);
-       }
-}
 if ( isset( $args[0] ) ) {
        wfWaitForSlaves($args[0]);
 } else {