Handle multiple warnings correctly in ApiBase::setWarning(). Calling this function...
[lhc/web/wiklou.git] / includes / Database.php
index 9d659fd..84b6b26 100644 (file)
@@ -1,5 +1,9 @@
 <?php
 /**
+ * @defgroup Database Database
+ *
+ * @file
+ * @ingroup Database
  * This file deals with MySQL interface functions
  * and query specifics/optimisations
  */
@@ -11,223 +15,9 @@ define( 'DEADLOCK_DELAY_MIN', 500000 );
 /** Maximum time to wait before retry */
 define( 'DEADLOCK_DELAY_MAX', 1500000 );
 
-/******************************************************************************
- * Utility classes
- *****************************************************************************/
-
-class DBObject {
-       public $mData;
-
-       function DBObject($data) {
-               $this->mData = $data;
-       }
-
-       function isLOB() {
-               return false;
-       }
-
-       function data() {
-               return $this->mData;
-       }
-};
-
-/******************************************************************************
- * Error classes
- *****************************************************************************/
-
-/**
- * Database error base class
- */
-class DBError extends MWException {
-       public $db;
-
-       /**
-        * 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 __construct( Database &$db, $error ) {
-               $this->db =& $db;
-               parent::__construct( $error );
-       }
-}
-
-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 );
-       }
-
-       function useOutputPage() {
-               // Not likely to work
-               return false;
-       }
-
-       function useMessageCache() {
-               // Not likely to work
-               return false;
-       }
-       
-       function getText() {
-               return $this->getMessage() . "\n";
-       }
-
-       function getLogMessage() {
-               # Don't send to the exception log
-               return false;
-       }
-
-       function getPageTitle() {
-               global $wgSitename;
-               return "$wgSitename has a problem";
-       }
-
-       function getHTML() {
-               global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
-               global $wgSitename, $wgServer, $wgMessageCache;
-
-               # 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;
-
-               $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. ";
-
-               # No database access
-               if ( is_object( $wgMessageCache ) ) {
-                       $wgMessageCache->disable();
-               }
-
-               if ( trim( $this->error ) == '' ) {
-                       $this->error = $this->db->getProperty('mServer');
-               }
-
-               $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 );
-                               }
-                       }
-
-                       $cache = new HTMLFileCache( $t );
-                       if( $cache->isFileCached() ) {
-                               // 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 $text;
-       }
-}
-
-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;
-       }
-
-       function getText() {
-               if ( $this->useMessageCache() ) {
-                       return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
-                         htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
-               } else {
-                       return $this->getMessage();
-               }
-       }
-       
-       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;
-       }
-
-       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() ) );
-               }
-       }
-}
-
-class DBUnexpectedError extends DBError {}
-
-/******************************************************************************/
-
 /**
  * Database abstraction object
+ * @ingroup Database
  */
 class Database {
 
@@ -246,9 +36,7 @@ class Database {
        protected $mTrxLevel = 0;
        protected $mErrorCount = 0;
        protected $mLBInfo = array();
-       protected $mCascadingDeletes = false;
-       protected $mCleanupTriggers = false;
-       protected $mStrictIPs = false;
+       protected $mFakeSlaveLag = null, $mFakeMaster = false;
 
 #------------------------------------------------------------------------------
 # Accessors
@@ -316,6 +104,10 @@ class Database {
                return wfSetVar( $this->mErrorCount, $count );
        }
 
+       function tablePrefix( $prefix = null ) {
+               return wfSetVar( $this->mTablePrefix, $prefix );
+       }
+
        /**
         * Properties passed down from the server info array of the load balancer
         */
@@ -339,18 +131,32 @@ class Database {
                }
        }
 
+       /**
+        * Set lag time in seconds for a fake slave
+        */
+       function setFakeSlaveLag( $lag ) {
+               $this->mFakeSlaveLag = $lag;
+       }
+
+       /**
+        * Make this connection a fake master
+        */
+       function setFakeMaster( $enabled = true ) {
+               $this->mFakeMaster = $enabled;
+       }
+
        /**
         * Returns true if this database supports (and uses) cascading deletes
         */
        function cascadingDeletes() {
-               return $this->mCascadingDeletes;
+               return false;
        }
 
        /**
         * Returns true if this database supports (and uses) triggers (e.g. on the page table)
         */
        function cleanupTriggers() {
-               return $this->mCleanupTriggers;
+               return false;
        }
 
        /**
@@ -358,7 +164,7 @@ class Database {
         * Specifically, it uses a NULL value instead of an empty string.
         */
        function strictIPs() {
-               return $this->mStrictIPs;
+               return false;
        }
 
        /**
@@ -375,6 +181,14 @@ class Database {
                return true;
        }
 
+       /**
+        * 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;
+       }
+
        /**
         * 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';
@@ -383,6 +197,13 @@ class Database {
                return false;
        }
 
+       /**
+        * Returns true if this database can use functional indexes
+        */
+       function functionalIndexes() {
+               return false;
+       }
+
        /**#@+
         * Get function
         */
@@ -409,18 +230,24 @@ class Database {
                return $this->$name;
        }
 
+       function getWikiID() {
+               if( $this->mTablePrefix ) {
+                       return "{$this->mDBname}-{$this->mTablePrefix}";
+               } else {
+                       return $this->mDBname;
+               }
+       }
+
 #------------------------------------------------------------------------------
 # 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
@@ -470,8 +297,7 @@ class Database {
         * @param failFunction
         * @param $flags
         */
-       static function newFromParams( $server, $user, $password, $dbName,
-               $failFunction = false, $flags = 0 )
+       static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
        {
                return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
        }
@@ -481,7 +307,7 @@ class Database {
         * If the failFunction is set to a non-zero integer, returns success
         */
        function open( $server, $user, $password, $dbName ) {
-               global $wguname;
+               global $wguname, $wgAllDBsAreLocalhost;
                wfProfileIn( __METHOD__ );
 
                # Test for missing mysql.so
@@ -496,6 +322,12 @@ class Database {
                        throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
                }
 
+               # Debugging hack -- fake cluster
+               if ( $wgAllDBsAreLocalhost ) {
+                       $realServer = 'localhost';
+               } else {
+                       $realServer = $server;
+               }
                $this->close();
                $this->mServer = $server;
                $this->mUser = $user;
@@ -505,8 +337,10 @@ class Database {
                $success = false;
 
                wfProfileIn("dbconnect-$server");
-               
-               # LIVE PATCH by Tim, ask Domas for why: retry loop
+
+               # 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++ ) {
@@ -514,13 +348,13 @@ class Database {
                                usleep( 1000 );
                        }
                        if ( $this->mFlags & DBO_PERSISTENT ) {
-                               @/**/$this->mConn = mysql_pconnect( $server, $user, $password );
+                               @/**/$this->mConn = mysql_pconnect( $realServer, $user, $password );
                        } else {
                                # Create a new connection...
-                               @/**/$this->mConn = mysql_connect( $server, $user, $password, true );
+                               @/**/$this->mConn = mysql_connect( $realServer, $user, $password, true );
                        }
                        if ($this->mConn === false) {
-                               $iplus = $i + 1;
+                               #$iplus = $i + 1;
                                #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); 
                        }
                }
@@ -548,12 +382,19 @@ class Database {
                }
 
                if ( $success ) {
-                       global $wgDBmysql5;
-                       if( $wgDBmysql5 ) {
+                       $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.
-                               $this->query( 'SET NAMES utf8' );
+                               global $wgDBmysql5;
+                               if( $wgDBmysql5 ) {
+                                       $this->query( 'SET NAMES utf8', __METHOD__ );
+                               }
+                               // Turn off strict mode
+                               $this->query( "SET sql_mode = ''", __METHOD__ );
                        }
+
+                       // Turn off strict mode if it is on
                } else {
                        $this->reportConnectionError();
                }
@@ -606,25 +447,34 @@ class Database {
        }
 
        /**
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns success
-        */
-       function query( $sql, $fname = '', $tempIgnore = false ) {
-               global $wgProfiling;
-
-               if ( $wgProfiling ) {
+        * 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
+        */
+       public function query( $sql, $fname = '', $tempIgnore = false ) {
+               global $wgProfiler;
+
+               $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+               if ( isset( $wgProfiler ) ) {
                        # 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 );
 
-                       if ( is_null( $this->getLBInfo( 'master' ) ) ) {
-                               $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
-                               $totalProf = 'Database::query';
-                       } else {
+                       if ( $isMaster ) {
                                $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
                                $totalProf = 'Database::query-master';
+                       } else {
+                               $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+                               $totalProf = 'Database::query';
                        }
                        wfProfileIn( $totalProf );
                        wfProfileIn( $queryProf );
@@ -633,23 +483,41 @@ class Database {
                $this->mLastQuery = $sql;
 
                # Add a comment for easy SHOW PROCESSLIST interpretation
-               if ( $fname ) {
-                       $commentedSql = preg_replace('/\s/', " /* $fname */ ", $sql, 1);
-               } else {
-                       $commentedSql = $sql;
-               }
+               #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' 
-               ) {
-                       $this->begin();
+                       $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(); 
                }
 
                if ( $this->debug() ) {
                        $sqlx = substr( $commentedSql, 0, 500 );
                        $sqlx = strtr( $sqlx, "\t\n", '  ' );
-                       wfDebug( "SQL: $sqlx\n" );
+                       if ( $isMaster ) {
+                               wfDebug( "SQL-master: $sqlx\n" );
+                       } else {
+                               wfDebug( "SQL: $sqlx\n" );
+                       }
                }
 
                # Do the query and handle errors
@@ -677,18 +545,20 @@ class Database {
                        $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
                }
 
-               if ( $wgProfiling ) {
+               if ( isset( $wgProfiler ) ) {
                        wfProfileOut( $queryProf );
                        wfProfileOut( $totalProf );
                }
-               return $ret;
+               return $this->resultObject( $ret );
        }
 
        /**
         * The DBMS-dependent part of query()
-        * @param string $sql SQL query.
+        * @param  $sql String: SQL query.
+        * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
+        * @access private
         */
-       function doQuery( $sql ) {
+       /*private*/ function doQuery( $sql ) {
                if( $this->bufferResults() ) {
                        $ret = mysql_query( $sql, $this->mConn );
                } else {
@@ -823,15 +693,27 @@ class Database {
         * Free a result object
         */
        function freeResult( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
                if ( !@/**/mysql_free_result( $res ) ) {
                        throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
                }
        }
 
        /**
-        * Fetch the next row from the given result object, in object form
+        * 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 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() ) );
@@ -840,10 +722,17 @@ class Database {
        }
 
        /**
-        * Fetch the next row from the given result object
-        * Returns an array
+        * 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;
+               }
                @/**/$row = mysql_fetch_array( $res );
                if ( $this->lastErrno() ) {
                        throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
@@ -855,6 +744,9 @@ class Database {
         * Get the number of rows in a result object
         */
        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() ) );
@@ -866,14 +758,24 @@ class Database {
         * Get the number of fields in a result object
         * See documentation for mysql_num_fields()
         */
-       function numFields( $res ) { return mysql_num_fields( $res ); }
+       function numFields( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_num_fields( $res );
+       }
 
        /**
         * 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 ) { return mysql_field_name( $res, $n ); }
+       function fieldName( $res, $n ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_field_name( $res, $n );
+       }
 
        /**
         * Get the inserted value of an auto-increment row
@@ -891,7 +793,12 @@ class Database {
         * Change the position of the cursor in a result object
         * See mysql_data_seek()
         */
-       function dataSeek( $res, $row ) { return mysql_data_seek( $res, $row ); }
+       function dataSeek( $res, $row ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_data_seek( $res, $row );
+       }
 
        /**
         * Get the last error number
@@ -995,6 +902,7 @@ class Database {
                }
 
                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'])) {
@@ -1005,7 +913,7 @@ class Database {
 
                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 ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
 
                # Various MySQL extensions
                if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
@@ -1035,10 +943,30 @@ class Database {
         * @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
+        * @param array $join_conds Associative array of table join conditions (optional)
+        *                        (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
         * @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() )
+       function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
        {
+               $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+               return $this->query( $sql, $fname );
+       }
+       
+       /**
+        * 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
+        * @param array $join_conds Associative array of table join conditions (optional)
+        *                        (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+        * @return string, the SQL text
+        */
+       function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
                if( is_array( $vars ) ) {
                        $vars = implode( ',', $vars );
                }
@@ -1046,8 +974,8 @@ class Database {
                        $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'] );
+                       if ( !empty($join_conds) || is_array( @$options['USE INDEX'] ) )
+                               $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
                        else
                                $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
                } elseif ($table!='') {
@@ -1075,8 +1003,11 @@ class Database {
                        $sql = $this->limitResult($sql, $options['LIMIT'],
                                isset($options['OFFSET']) ? $options['OFFSET'] : false);
                $sql = "$sql $postLimitTail";
-
-               return $this->query( $sql, $fname );
+               
+               if (isset($options['EXPLAIN'])) {
+                       $sql = 'EXPLAIN ' . $sql;
+               }
+               return $sql;
        }
 
        /**
@@ -1093,9 +1024,9 @@ class Database {
         *
         * @todo migrate documentation to phpdocumentor format
         */
-       function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array() ) {
+       function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
                $options['LIMIT'] = 1;
-               $res = $this->select( $table, $vars, $conds, $fname, $options );
+               $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
                if ( $res === false )
                        return false;
                if ( !$this->numRows($res) ) {
@@ -1107,6 +1038,33 @@ class Database {
                return $obj;
 
        }
+       
+       /**
+        * Estimate rows in dataset
+        * Returns estimated count, based on EXPLAIN output
+        * Takes same arguments as Database::select()
+        */
+       
+       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;           
+       }
+       
 
        /**
         * Removes most variables from an SQL query and replaces them with X or N for numbers.
@@ -1225,11 +1183,11 @@ class Database {
        function fieldInfo( $table, $field ) {
                $table = $this->tableName( $table );
                $res = $this->query( "SELECT * FROM $table LIMIT 1" );
-               $n = mysql_num_fields( $res );
+               $n = mysql_num_fields( $res->result );
                for( $i = 0; $i < $n; $i++ ) {
-                       $meta = mysql_fetch_field( $res, $i );
+                       $meta = mysql_fetch_field( $res->result, $i );
                        if( $field == $meta->name ) {
-                               return $meta;
+                               return new MySQLField($meta);
                        }
                }
                return false;
@@ -1239,6 +1197,9 @@ class Database {
         * mysql_field_type() wrapper
         */
        function fieldType( $res, $index ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
                return mysql_field_type( $res, $index );
        }
 
@@ -1328,6 +1289,7 @@ class Database {
         *                       (for the log)
         * @param array  $options An array of UPDATE options, can be one or
         *                        more of IGNORE, LOW_PRIORITY
+        * @return bool
         */
        function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
                $table = $this->tableName( $table );
@@ -1336,7 +1298,7 @@ class Database {
                if ( $conds != '*' ) {
                        $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
                }
-               $this->query( $sql, $fname );
+               return $this->query( $sql, $fname );
        }
 
        /**
@@ -1371,8 +1333,25 @@ class Database {
                                $list .= "($value)";
                        } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
                                $list .= "$value";
-                       } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array ($value) ) {
-                               $list .= $field." IN (".$this->makeList($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 = ";
@@ -1391,33 +1370,81 @@ class Database {
                return mysql_select_db( $db, $this->mConn );
        }
 
+       /**
+        * Get the current DB name
+        */
+       function getDBname() {
+               return $this->mDBname;
+       }
+
+       /**
+        * Get the server hostname or IP address
+        */
+       function getServer() {
+               return $this->mServer;
+       }
+
        /**
         * Format a table name ready for use in constructing an SQL query
         *
-        * This does two important things: it quotes table names which as necessary,
-        * and it adds a table prefix if there is one.
+        * This does two important things: it quotes the table names to clean them up,
+        * and it adds a table prefix if only given a table name with no quotes.
         *
         * 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 string $name database table name
+        * @return string full database name
         */
        function tableName( $name ) {
-               global $wgSharedDB;
-               # Skip quoted literals
-               if ( $name{0} != '`' ) {
-                       if ( $this->mTablePrefix !== '' &&  strpos( '.', $name ) === false ) {
-                               $name = "{$this->mTablePrefix}$name";
-                       }
-                       if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
-                               $name = "`$wgSharedDB`.`$name`";
-                       } else {
-                               # Standard quoting
-                               $name = "`$name`";
-                       }
+               global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
+               # Skip the entire process when we have a string quoted on both ends.
+               # Note that we check the end so that we will still quote any use of
+               # use of `database`.table. But won't break things if someone wants
+               # to query a database table with a dot in the name.
+               if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
+               
+               # Lets test for any bits of text that should never show up in a table
+               # name. Basically anything like JOIN or ON which are actually part of
+               # SQL queries, but may end up inside of the table value to combine
+               # sql. Such as how the API is doing.
+               # Note that we use a whitespace test rather than a \b test to avoid
+               # any remote case where a word like on may be inside of a table name
+               # surrounded by symbols which may be considered word breaks.
+               if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
+               
+               # Split database and table into proper variables.
+               # We reverse the explode so that database.table and table both output
+               # the correct table.
+               @list( $table, $database ) = array_reverse( explode( '.', $name, 2 ) );
+               $prefix = $this->mTablePrefix; # Default prefix
+               
+               # A database name has been specified in input. Quote the table name
+               # because we don't want any prefixes added.
+               if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
+               
+               # Note that we use the long format because php will complain in in_array if
+               # the input is not an array, and will complain in is_array if it is not set.
+               if( !isset( $database ) # Don't use shared database if pre selected.
+                && isset( $wgSharedDB ) # We have a shared database
+                && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
+                && isset( $wgSharedTables )
+                && is_array( $wgSharedTables )
+                && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
+                       $database = $wgSharedDB;
+                       $prefix   = isset( $wgSharedprefix ) ? $wgSharedprefix : $prefix;
                }
-               return $name;
+               
+               # Quote the $database and $table and apply the prefix if not quoted.
+               if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
+               $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
+               
+               # Merge our database and table into our final table name.
+               $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
+               
+               # We're finished, return.
+               return $tableName;
        }
 
        /**
@@ -1439,11 +1466,11 @@ class Database {
        }
        
        /**
-        * @desc: Fetch a number of table names into an zero-indexed numerical array
+        * 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->tableNames('user','watchlist');
+        * 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";
         */
@@ -1459,16 +1486,38 @@ class Database {
        /**
         * @private
         */
-       function tableNamesWithUseIndex( $tables, $use_index ) {
+       function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
                $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 );
+               $retJOIN = array();
+               $use_index_safe = is_array($use_index) ? $use_index : array();
+               $join_conds_safe = is_array($join_conds) ? $join_conds : array();
+               foreach ( $tables as $table ) {
+                       // Is there a JOIN and INDEX clause for this table?
+                       if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
+                               $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+                               $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+                               $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+                               $retJOIN[] = $tableClause;
+                       // Is there an INDEX clause?
+                       } else if ( isset($use_index_safe[$table]) ) {
+                               $tableClause = $this->tableName( $table );
+                               $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+                               $ret[] = $tableClause;
+                       // Is there a JOIN clause?
+                       } else if ( isset($join_conds_safe[$table]) ) {
+                               $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+                               $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+                               $retJOIN[] = $tableClause;
+                       } else {
+                               $tableClause = $this->tableName( $table );
+                               $ret[] = $tableClause;
+                       }
+               }
+               // We can't separate explicit JOIN clauses with ',', use ' ' for those
+               $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
+               $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
+               // Compile our final table clause
+               return implode(' ',array($straightJoins,$otherJoins) );
        }
 
        /**
@@ -1673,7 +1722,7 @@ class Database {
                if( !is_numeric($limit) ) {
                        throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
                }
-               return " $sql LIMIT "
+               return "$sql LIMIT "
                                . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
                                . "{$limit} ";
        }
@@ -1694,6 +1743,18 @@ class Database {
                return " IF($cond, $trueVal, $falseVal) ";
        }
 
+       /**
+        * Returns a comand for str_replace function in SQL query.
+        * Uses REPLACE() in MySQL
+        *
+        * @param string $orig String or column to modify
+        * @param string $old String or column to seek
+        * @param string $new String or column to replace with
+        */
+       function strreplace( $orig, $old, $new ) {
+               return "REPLACE({$orig}, {$old}, {$new})";
+       }
+
        /**
         * Determines if the last failure was due to a deadlock
         */
@@ -1763,17 +1824,37 @@ class Database {
         * @param string $pos the binlog position
         * @param integer $timeout the maximum number of seconds to wait for synchronisation
         */
-       function masterPosWait( $file, $pos, $timeout ) {
+       function masterPosWait( MySQLMasterPos $pos, $timeout ) {
                $fname = 'Database::masterPosWait';
                wfProfileIn( $fname );
 
-
                # Commit any open transactions
-               $this->immediateCommit();
+               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 {
+                               wfDebug( "Fake slave up to date ($wait us)\n" );
+                               wfProfileOut( $fname );
+                               return 0;
+                       }
+               }
 
                # 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)";
+               $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 );
@@ -1789,12 +1870,17 @@ class Database {
         * Get the position of the master from SHOW SLAVE STATUS
         */
        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 array( $row->Master_Log_File, $row->Read_Master_Log_Pos );
+                       return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
                } else {
-                       return array( false, false );
+                       return false;
                }
        }
 
@@ -1802,12 +1888,15 @@ class Database {
         * Get the position of the master from SHOW MASTER STATUS
         */
        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 array( $row->File, $row->Position );
+                       return new MySQLMasterPos( $row->File, $row->Position );
                } else {
-                       return array( false, false );
+                       return false;
                }
        }
 
@@ -1828,10 +1917,11 @@ class Database {
        }
 
        /**
-        * Rollback a transaction
+        * Rollback a transaction.
+        * No-op on non-transactional databases.
         */
        function rollback( $fname = 'Database::rollback' ) {
-               $this->query( 'ROLLBACK', $fname );
+               $this->query( 'ROLLBACK', $fname, true );
                $this->mTrxLevel = 0;
        }
 
@@ -1874,7 +1964,12 @@ class Database {
         */
        function resultObject( $result ) {
                if( empty( $result ) ) {
-                       return NULL;
+                       return false;
+               } elseif ( $result instanceof ResultWrapper ) {
+                       return $result;
+               } elseif ( $result === true ) {
+                       // Successful write query
+                       return $result;
                } else {
                        return new ResultWrapper( $this, $result );
                }
@@ -1905,12 +2000,24 @@ class Database {
         * Ping the server and try to reconnect if it there is no connection
         */
        function ping() {
-               if( function_exists( 'mysql_ping' ) ) {
-                       return mysql_ping( $this->mConn );
-               } else {
+               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;
        }
 
        /**
@@ -1918,9 +2025,12 @@ class Database {
         * At the moment, this will only work if the DB user has the PROCESS privilege
         */
        function getLag() {
+               if ( !is_null( $this->mFakeSlaveLag ) ) {
+                       wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
+                       return $this->mFakeSlaveLag;
+               }
                $res = $this->query( 'SHOW PROCESSLIST' );
-               # Find slave SQL thread. Assumed to be the second one running, which is a bit
-               # dubious, but unfortunately there's no easy rigorous way
+               # 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, 
@@ -1988,18 +2098,36 @@ class Database {
        /**
         * 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 ) {
+       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;
+       }
 
+       /**
+        * 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;
 
                while ( ! feof( $fp ) ) {
+                       if ( $lineCallback ) {
+                               call_user_func( $lineCallback );
+                       }
                        $line = trim( fgets( $fp, 1024 ) );
                        $sl = strlen( $line ) - 1;
 
@@ -2029,7 +2157,10 @@ class Database {
                        if ( $done ) {
                                $cmd = str_replace(';;', ";", $cmd);
                                $cmd = $this->replaceVars( $cmd );
-                               $res = $this->query( $cmd, 'dbsource', true );
+                               $res = $this->query( $cmd, __METHOD__, true );
+                               if ( $resultCallback ) {
+                                       call_user_func( $resultCallback, $res );
+                               }
 
                                if ( false === $res ) {
                                        $err = $this->lastError();
@@ -2040,10 +2171,10 @@ class Database {
                                $done = false;
                        }
                }
-               fclose( $fp );
                return true;
        }
 
+
        /**
         * Replace variables in sourced SQL
         */
@@ -2051,7 +2182,7 @@ class Database {
                $varnames = array(
                        'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
                        'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
-                       'wgDBadminuser', 'wgDBadminpassword',
+                       'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
                );
 
                // Ordinary variables
@@ -2078,57 +2209,376 @@ class Database {
                return $this->tableName( $matches[1] );
        }
 
+       /*
+        * Build a concatenation list to feed into a SQL query
+       */
+       function buildConcat( $stringList ) {
+               return 'CONCAT(' . implode( ',', $stringList ) . ')';
+       }
+
 }
 
 /**
  * Database abstraction object for mySQL
  * Inherit all methods and properties of Database::Database()
  *
+ * @ingroup Database
  * @see Database
  */
 class DatabaseMysql extends Database {
        # Inherit all
 }
 
+/******************************************************************************
+ * Utility classes
+ *****************************************************************************/
 
 /**
- * Result wrapper for grabbing data queried by someone else
+ * Utility class.
+ * @ingroup Database
+ */
+class DBObject {
+       public $mData;
+
+       function DBObject($data) {
+               $this->mData = $data;
+       }
+
+       function isLOB() {
+               return false;
+       }
+
+       function data() {
+               return $this->mData;
+       }
+}
+
+/**
+ * Utility class
+ * @ingroup Database
  *
+ * This allows us to distinguish a blob from a normal string and an array of strings
  */
-class ResultWrapper {
-       var $db, $result;
+class Blob {
+       private $mData;
+       function __construct($data) {
+               $this->mData = $data;
+       }
+       function fetch() {
+               return $this->mData;
+       }
+}
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class MySQLField {
+       private $name, $tablename, $default, $max_length, $nullable,
+               $is_pk, $is_unique, $is_multiple, $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;
+       }
+
+       function name() {
+               return $this->name;
+       }
+
+       function tableName() {
+               return $this->tableName;
+       }
+
+       function defaultValue() {
+               return $this->default;
+       }
+
+       function maxLength() {
+               return $this->max_length;
+       }
+
+       function nullable() {
+               return $this->nullable;
+       }
+
+       function isKey() {
+               return $this->is_key;
+       }
+
+       function isMultipleKey() {
+               return $this->is_multiple;
+       }
+
+       function type() {
+               return $this->type;
+       }
+}
+
+/******************************************************************************
+ * Error classes
+ *****************************************************************************/
+
+/**
+ * Database error base class
+ * @ingroup Database
+ */
+class DBError extends MWException {
+       public $db;
 
        /**
-        * @todo document
+        * 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 __construct( Database &$db, $error ) {
+               $this->db =& $db;
+               parent::__construct( $error );
+       }
+}
+
+/**
+ * @ingroup 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 );
+       }
+
+       function useOutputPage() {
+               // Not likely to work
+               return false;
+       }
+
+       function useMessageCache() {
+               // Not likely to work
+               return false;
+       }
+       
+       function getText() {
+               return $this->getMessage() . "\n";
+       }
+
+       function getLogMessage() {
+               # Don't send to the exception log
+               return false;
+       }
+
+       function getPageTitle() {
+               global $wgSitename;
+               return "$wgSitename has a problem";
+       }
+
+       function getHTML() {
+               global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
+               global $wgSitename, $wgServer, $wgMessageCache;
+
+               # 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;
+
+               $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. ";
+
+               # No database access
+               if ( is_object( $wgMessageCache ) ) {
+                       $wgMessageCache->disable();
+               }
+
+               if ( trim( $this->error ) == '' ) {
+                       $this->error = $this->db->getProperty('mServer');
+               }
+
+               $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 );
+                               }
+                       }
+
+                       $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 $text;
+       }
+}
+
+/**
+ * @ingroup 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;
+       }
+
+       function getText() {
+               if ( $this->useMessageCache() ) {
+                       return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
+                         htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+               } else {
+                       return $this->getMessage();
+               }
+       }
+       
+       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;
+       }
+
+       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() ) );
+               }
+       }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBUnexpectedError extends DBError {}
+
+
+/**
+ * Result wrapper for grabbing data queried by someone else
+ * @ingroup Database
+ */
+class ResultWrapper implements Iterator {
+       var $db, $result, $pos = 0, $currentRow = null;
+
+       /**
+        * Create a new result object from a result resource and a Database object
         */
-       function ResultWrapper( &$database, $result ) {
-               $this->db =& $database;
-               $this->result =& $result;
+       function ResultWrapper( $database, $result ) {
+               $this->db = $database;
+               if ( $result instanceof ResultWrapper ) {
+                       $this->result = $result->result;
+               } else {
+                       $this->result = $result;
+               }
        }
 
        /**
-        * @todo document
+        * Get the number of rows in a result object
         */
        function numRows() {
                return $this->db->numRows( $this->result );
        }
 
        /**
-        * @todo document
+        * 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 fetchObject() {
                return $this->db->fetchObject( $this->result );
        }
 
        /**
-        * @todo document
+        * 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() {
                return $this->db->fetchRow( $this->result );
        }
 
        /**
-        * @todo document
+        * Free a result object
         */
        function free() {
                $this->db->freeResult( $this->result );
@@ -2136,16 +2586,59 @@ class ResultWrapper {
                unset( $this->db );
        }
 
+       /**
+        * Change the position of the cursor in a result object
+        * See mysql_data_seek()
+        */
        function seek( $row ) {
                $this->db->dataSeek( $this->result, $row );
        }
-       
+
+       /*********************
+        * Iterator functions
+        * Note that using these in combination with the non-iterator functions
+        * above may cause rows to be skipped or repeated.
+        */
+
        function rewind() {
                if ($this->numRows()) {
                        $this->db->dataSeek($this->result, 0);
                }
+               $this->pos = 0;
+               $this->currentRow = null;
        }
 
+       function current() {
+               if ( is_null( $this->currentRow ) ) {
+                       $this->next();
+               }
+               return $this->currentRow;
+       }
+
+       function key() {
+               return $this->pos;
+       }
+
+       function next() {
+               $this->pos++;
+               $this->currentRow = $this->fetchObject();
+               return $this->currentRow;
+       }
+
+       function valid() {
+               return $this->current() !== false;
+       }
 }
 
-?>
+class MySQLMasterPos {
+       var $file, $pos;
+
+       function __construct( $file, $pos ) {
+               $this->file = $file;
+               $this->pos = $pos;
+       }
+
+       function __toString() {
+               return "{$this->file}/{$this->pos}";
+       }
+}