(bug 20275) Fixed LIKE queries on SQLite backend
[lhc/web/wiklou.git] / includes / db / Database.php
index da85801..e3d3906 100644 (file)
@@ -19,13 +19,14 @@ define( 'DEADLOCK_DELAY_MAX', 1500000 );
  * Database abstraction object
  * @ingroup Database
  */
-class Database {
+abstract class DatabaseBase {
 
 #------------------------------------------------------------------------------
 # Variables
 #------------------------------------------------------------------------------
 
        protected $mLastQuery = '';
+       protected $mDoneWrites = false;
        protected $mPHPError = false;
 
        protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
@@ -38,6 +39,7 @@ class Database {
        protected $mErrorCount = 0;
        protected $mLBInfo = array();
        protected $mFakeSlaveLag = null, $mFakeMaster = false;
+       protected $mDefaultBigSelects = null;
 
 #------------------------------------------------------------------------------
 # Accessors
@@ -190,6 +192,14 @@ class Database {
                return true;
        }
 
+       /**
+        * Returns true if this database requires that SELECT DISTINCT queries require that all 
+       ORDER BY expressions occur in the SELECT list per the SQL92 standard
+        */
+       function standardSelectDistinct() {
+               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';
@@ -207,24 +217,54 @@ class Database {
 
        /**
         * Return the last query that went through Database::query()
-        * @return string
+        * @return String
         */
        function lastQuery() { return $this->mLastQuery; }
-       
+
+
+       /**
+        * Returns true if the connection may have been used for write queries.
+        * Should return true if unsure.
+        */
+       function doneWrites() { return $this->mDoneWrites; }
+
        /**
         * Is a connection to the database open?
-        * @return bool
+        * @return Boolean
         */
        function isOpen() { return $this->mOpened; }
 
+       /**
+        * Set a flag for this connection
+        *
+        * @param $flag Integer: DBO_* constants from Defines.php:
+        *   - DBO_DEBUG: output some debug info (same as debug())
+        *   - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
+        *   - DBO_IGNORE: ignore errors (same as ignoreErrors())
+        *   - DBO_TRX: automatically start transactions
+        *   - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode
+        *       and removes it in command line mode
+        *   - DBO_PERSISTENT: use persistant database connection 
+        */
        function setFlag( $flag ) {
                $this->mFlags |= $flag;
        }
 
+       /**
+        * Clear a flag for this connection
+        *
+        * @param $flag: same as setFlag()'s $flag param
+        */
        function clearFlag( $flag ) {
                $this->mFlags &= ~$flag;
        }
 
+       /**
+        * Returns a boolean whether the flag $flag is set for this connection
+        *
+        * @param $flag: same as setFlag()'s $flag param
+        * @return Boolean
+        */
        function getFlag( $flag ) {
                return !!($this->mFlags & $flag);
        }
@@ -248,13 +288,13 @@ class Database {
 # 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 $server String: database server host
+        * @param $user String: database user name
+        * @param $password String: database user password
+        * @param $dbName String: database name
+        * @param $failFunction
         * @param $flags
         * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
         */
@@ -298,128 +338,28 @@ class Database {
        }
 
        /**
-        * @static
+        * Same as new DatabaseMysql( ... ), kept for backward compatibility
+        * @param $server String: database server host
+        * @param $user String: database user name
+        * @param $password String: database user password
+        * @param $dbName String: database name
         * @param failFunction
         * @param $flags
         */
        static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
        {
-               return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
+               return new DatabaseMysql( $server, $user, $password, $dbName, $failFunction, $flags );
        }
 
        /**
         * Usually aborts on failure
         * If the failFunction is set to a non-zero integer, returns success
+        * @param $server String: database server host
+        * @param $user String: database user name
+        * @param $password String: database user password
+        * @param $dbName String: database name
         */
-       function open( $server, $user, $password, $dbName ) {
-               global $wgAllDBsAreLocalhost;
-               wfProfileIn( __METHOD__ );
-
-               # Test for missing mysql.so
-               # First try to load it
-               if (!@extension_loaded('mysql')) {
-                       @dl('mysql.so');
-               }
-
-               # 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" );
-               }
-
-               # Debugging hack -- fake cluster
-               if ( $wgAllDBsAreLocalhost ) {
-                       $realServer = 'localhost';
-               } else {
-                       $realServer = $server;
-               }
-               $this->close();
-               $this->mServer = $server;
-               $this->mUser = $user;
-               $this->mPassword = $password;
-               $this->mDBname = $dbName;
-
-               $success = false;
-
-               wfProfileIn("dbconnect-$server");
-
-               # The kernel's default SYN retransmission period is far too slow for us,
-               # so we use a short timeout plus a manual retry. Retrying means that a small
-               # but finite rate of SYN packet loss won't cause user-visible errors.
-               $this->mConn = false;
-               if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
-                       $numAttempts = 2;
-               } else {
-                       $numAttempts = 1;
-               }
-               $this->installErrorHandler();
-               for ( $i = 0; $i < $numAttempts && !$this->mConn; $i++ ) {
-                       if ( $i > 1 ) {
-                               usleep( 1000 );
-                       }
-                       if ( $this->mFlags & DBO_PERSISTENT ) {
-                               $this->mConn = mysql_pconnect( $realServer, $user, $password );
-                       } else {
-                               # Create a new connection...
-                               $this->mConn = mysql_connect( $realServer, $user, $password, true );
-                       }
-                       if ($this->mConn === false) {
-                               #$iplus = $i + 1;
-                               #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); 
-                       }
-               }
-               $phpError = $this->restoreErrorHandler();
-               # Always log connection errors
-               if ( !$this->mConn ) {
-                       $error = $this->lastError();
-                       if ( !$error ) {
-                               $error = $phpError;
-                       }
-                       wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
-                       wfDebug( "DB connection error\n" );
-                       wfDebug( "Server: $server, User: $user, Password: " .
-                               substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
-                       $success = false;
-               }
-               
-               wfProfileOut("dbconnect-$server");
-
-               if ( $dbName != '' && $this->mConn !== false ) {
-                       $success = @/**/mysql_select_db( $dbName, $this->mConn );
-                       if ( !$success ) {
-                               $error = "Error selecting database $dbName on server {$this->mServer} " .
-                                       "from client host " . wfHostname() . "\n";
-                               wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
-                               wfDebug( $error );
-                       }
-               } else {
-                       # Delay USE query
-                       $success = (bool)$this->mConn;
-               }
-
-               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__ );
-                       }
-
-                       // Turn off strict mode if it is on
-               } else {
-                       $this->reportConnectionError( $phpError );
-               }
-
-               $this->mOpened = $success;
-               wfProfileOut( __METHOD__ );
-               return $success;
-       }
-       /**@}}*/
+       abstract function open( $server, $user, $password, $dbName );
 
        protected function installErrorHandler() {
                $this->mPHPError = false;
@@ -449,23 +389,15 @@ class Database {
         * Closes a database connection.
         * if it is open : commits any open transactions
         *
-        * @return bool operation success. true if already closed.
+        * @return Bool operation success. true if already closed.
         */
-       function close()
-       {
-               $this->mOpened = false;
-               if ( $this->mConn ) {
-                       if ( $this->trxLevel() ) {
-                               $this->immediateCommit();
-                       }
-                       return mysql_close( $this->mConn );
-               } else {
-                       return true;
-               }
+       function close() {
+               # Stub, should probably be overridden
+               return true;
        }
 
        /**
-        * @param string $error fallback error message, used if none is given by MySQL
+        * @param $error String: fallback error message, used if none is given by MySQL
         */
        function reportConnectionError( $error = 'Unknown error' ) {
                $myError = $this->lastError();
@@ -485,13 +417,21 @@ class Database {
                }
        }
 
+       /**
+        * Determine whether a query writes to the DB.
+        * Should return true if unsure.
+        */
+       function isWriteQuery( $sql ) {
+               return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW)\b/i', $sql );
+       }
+
        /**
         * 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... 
+        * @param  $tempIgnore Boolean:   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
@@ -520,6 +460,11 @@ class Database {
                }
 
                $this->mLastQuery = $sql;
+               if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) {
+                       // Set a flag indicating that writes have been done
+                       wfDebug( __METHOD__.": Writes done: $sql\n" );
+                       $this->mDoneWrites = true;
+               }
 
                # Add a comment for easy SHOW PROCESSLIST interpretation
                #if ( $fname ) {
@@ -559,11 +504,15 @@ class Database {
                        }
                }
 
+               if ( istainted( $sql ) & TC_MYSQL ) {
+                       throw new MWException( 'Tainted query found' );
+               }
+
                # 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 ) ) {
+               if ( false === $ret && $this->wasErrorReissuable() ) {
                        # Transaction is gone, like it or not
                        $this->mTrxLevel = 0;
                        wfDebug( "Connection lost, reconnecting...\n" );
@@ -595,23 +544,16 @@ class Database {
         * 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
+        * @private
         */
-       /*private*/ function doQuery( $sql ) {
-               if( $this->bufferResults() ) {
-                       $ret = mysql_query( $sql, $this->mConn );
-               } else {
-                       $ret = mysql_unbuffered_query( $sql, $this->mConn );
-               }
-               return $ret;
-       }
+       /*private*/ abstract function doQuery( $sql );
 
        /**
-        * @param $error
-        * @param $errno
-        * @param $sql
-        * @param string $fname
-        * @param bool $tempIgnore
+        * @param $error String
+        * @param $errno Integer
+        * @param $sql String
+        * @param $fname String
+        * @param $tempIgnore Boolean
         */
        function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
                global $wgCommandLineMode;
@@ -653,8 +595,8 @@ class Database {
 
        /**
         * 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
+        * @param $prepared String: the prepared sql
+        * @param $args Mixed: Either an array here, or put scalars as varargs
         */
        function execute( $prepared, $args = null ) {
                if( !is_array( $args ) ) {
@@ -669,8 +611,8 @@ class Database {
        /**
         * Prepare & execute an SQL statement, quoting and inserting arguments
         * in the appropriate places.
-        * @param string $query
-        * @param string $args ...
+        * @param $query String
+        * @param $args ...
         */
        function safeQuery( $query, $args = null ) {
                $prepared = $this->prepare( $query, 'Database::safeQuery' );
@@ -687,8 +629,8 @@ class Database {
        /**
         * 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
+        * @param $preparedQuery String: a 'preparable' SQL statement
+        * @param $args Array of arguments to fill it with
         * @return string executable SQL
         */
        function fillPrepared( $preparedQuery, $args ) {
@@ -703,8 +645,8 @@ class Database {
         * The arguments should be in $this->preparedArgs and must not be touched
         * while we're doing this.
         *
-        * @param array $matches
-        * @return string
+        * @param $matches Array
+        * @return String
         * @private
         */
        function fillPreparedArg( $matches ) {
@@ -725,19 +667,13 @@ class Database {
                }
        }
 
-       /**#@+
-        * @param mixed $res A SQL result
-        */
        /**
         * Free a result object
+        * @param $res Mixed: A SQL result
         */
        function freeResult( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               if ( !@/**/mysql_free_result( $res ) ) {
-                       throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
-               }
+               # Stub.  Might not really need to be overridden, since results should
+               # be freed by PHP when the variable goes out of scope anyway.
        }
 
        /**
@@ -749,16 +685,7 @@ class Database {
         * @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() ) );
-               }
-               return $row;
-       }
+       abstract function fetchObject( $res );
 
        /**
         * Fetch the next row from the given result object, in associative array
@@ -768,53 +695,29 @@ class Database {
         * @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() ) );
-               }
-               return $row;
-       }
+       abstract function fetchRow( $res );
 
        /**
         * Get the number of rows in a result object
+        * @param $res Mixed: A SQL result
         */
-       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;
-       }
+       abstract function numRows( $res );
 
        /**
         * Get the number of fields in a result object
         * See documentation for mysql_num_fields()
+        * @param $res Mixed: A SQL result
         */
-       function numFields( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mysql_num_fields( $res );
-       }
+       abstract function numFields( $res );
 
        /**
         * Get a field name in a result object
         * See documentation for mysql_field_name():
         * http://www.php.net/mysql_field_name
+        * @param $res Mixed: A SQL result
+        * @param $n Integer
         */
-       function fieldName( $res, $n ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mysql_field_name( $res, $n );
-       }
+       abstract function fieldName( $res, $n );
 
        /**
         * Get the inserted value of an auto-increment row
@@ -826,58 +729,33 @@ class Database {
         * $dbw->insert('page',array('page_id' => $id));
         * $id = $dbw->insertId();
         */
-       function insertId() { return mysql_insert_id( $this->mConn ); }
+       abstract function insertId();
 
        /**
         * Change the position of the cursor in a result object
         * See mysql_data_seek()
+        * @param $res Mixed: A SQL result
+        * @param $row Mixed: Either MySQL row or ResultWrapper
         */
-       function dataSeek( $res, $row ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mysql_data_seek( $res, $row );
-       }
+       abstract function dataSeek( $res, $row );
 
        /**
         * Get the last error number
         * See mysql_errno()
         */
-       function lastErrno() {
-               if ( $this->mConn ) {
-                       return mysql_errno( $this->mConn );
-               } else {
-                       return mysql_errno();
-               }
-       }
+       abstract function lastErrno();
 
        /**
         * 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();
-                       }
-                       wfRestoreWarnings();
-               } else {
-                       $error = mysql_error();
-               }
-               if( $error ) {
-                       $error .= ' (' . $this->mServer . ')';
-               }
-               return $error;
-       }
+       abstract function lastError();
+
        /**
         * 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
+       abstract function affectedRows();
 
        /**
         * Simple UPDATE wrapper
@@ -887,8 +765,7 @@ class Database {
         * This function exists for historical reasons, Database::update() has a more standard
         * calling convention and feature set
         */
-       function set( $table, $var, $value, $cond, $fname = 'Database::set' )
-       {
+       function set( $table, $var, $value, $cond, $fname = 'Database::set' ) {
                $table = $this->tableName( $table );
                $sql = "UPDATE $table SET $var = '" .
                  $this->strencode( $value ) . "' WHERE ($cond)";
@@ -913,7 +790,7 @@ class Database {
                $row = $this->fetchRow( $res );
                if ( $row !== false ) {
                        $this->freeResult( $res );
-                       return $row[0];
+                       return reset( $row );
                } else {
                        return false;
                }
@@ -925,9 +802,9 @@ class Database {
         *
         * @private
         *
-        * @param array $options an associative array of options to be turned into
+        * @param $options Array: associative array of options to be turned into
         *              an SQL query, valid keys are listed in the function.
-        * @return array
+        * @return Array
         */
        function makeSelectOptions( $options ) {
                $preLimitTail = $postLimitTail = '';
@@ -976,14 +853,14 @@ class Database {
        /**
         * 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') )
+        * @param $table   Mixed:  Array or string, table name(s) (prefix auto-added)
+        * @param $vars    Mixed:  Array or string, field name(s) to be retrieved
+        * @param $conds   Mixed:  Array or string, condition(s) for WHERE
+        * @param $fname   String: Calling function name (use __METHOD__) for logs/profiling
+        * @param $options Array:  Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+        *                         see Database::makeSelectOptions code for list of supported stuff
+        * @param $join_conds Array: 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(), $join_conds = array() )
@@ -995,14 +872,14 @@ class Database {
        /**
         * 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') )
+        * @param $table   Mixed:  Array or string, table name(s) (prefix auto-added)
+        * @param $vars    Mixed:  Array or string, field name(s) to be retrieved
+        * @param $conds   Mixed:  Array or string, condition(s) for WHERE
+        * @param $fname   String: Calling function name (use __METHOD__) for logs/profiling
+        * @param $options Array:  Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+        *                         see Database::makeSelectOptions code for list of supported stuff
+        * @param $join_conds Array: 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() ) {
@@ -1053,13 +930,17 @@ class Database {
         * Single row SELECT wrapper
         * Aborts or returns FALSE on error
         *
-        * $vars: the selected variables
-        * $conds: a condition map, terms are ANDed together.
+        * @param $table String: table name
+        * @param $vars String: the selected variables
+        * @param $conds Array: 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
+        * @param $fname String: Calling function name
+        * @param $options Array
+        * @param $join_conds Array
         *
         * @todo migrate documentation to phpdocumentor format
         */
@@ -1080,37 +961,33 @@ class Database {
        
        /**
         * Estimate rows in dataset
-        * Returns estimated count, based on EXPLAIN output
+        * Returns estimated count - not necessarily an accurate estimate across different databases,
+        * so use sparingly
         * 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
+        *
+        * @param string $table table name
+        * @param array $vars unused
+        * @param array $conds filters on the table
+        * @param string $fname function name for profiling
+        * @param array $options options for select
+        * @return int row count
+        */
+       public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+               $rows = 0;
+               $res = $this->select ( $table, 'COUNT(*) AS rowcount', $conds, $fname, $options );
+               if ( $res ) {
+                       $row = $this->fetchRow( $res );
+                       $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0;
                }
-               
-               $this->freeResult($res);
-               return $rows;           
+               $this->freeResult( $res );
+               return $rows;
        }
-       
 
        /**
         * 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
+        * @param $sql String: A SQL Query
         */
        static function generalizeSQL( $sql ) {
                # This does the same as the regexp below would do, but in such a way
@@ -1179,6 +1056,7 @@ class Database {
                # SHOW INDEX should work for 3.x and up:
                # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
                $table = $this->tableName( $table );
+               $index = $this->indexName( $index );
                $sql = 'SHOW INDEX FROM '.$table;
                $res = $this->query( $sql, $fname );
                if ( !$res ) {
@@ -1219,18 +1097,7 @@ class Database {
         * @param $table
         * @param $field
         */
-       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;
-       }
+       abstract function fieldInfo( $table, $field );
 
        /**
         * mysql_field_type() wrapper
@@ -1303,7 +1170,7 @@ class Database {
         * Make UPDATE options for the Database::update function
         *
         * @private
-        * @param array $options The options passed to Database::update
+        * @param $options Array: The options passed to Database::update
         * @return string
         */
        function makeUpdateOptions( $options ) {
@@ -1321,14 +1188,14 @@ class Database {
        /**
         * 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
+        * @param $table  String: The table to UPDATE
+        * @param $values Array:  An array of values to SET
+        * @param $conds  Array:  An array of conditions (WHERE). Use '*' to update all rows.
+        * @param $fname  String: The Class::Function calling this function
+        *                        (for the log)
+        * @param $options Array: An array of UPDATE options, can be one or
         *                        more of IGNORE, LOW_PRIORITY
-        * @return bool
+        * @return Boolean
         */
        function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
                $table = $this->tableName( $table );
@@ -1384,7 +1251,7 @@ class Database {
                                } else {
                                        $list .= $field." IN (".$this->makeList($value).") ";
                                }
-                       } elseif( is_null($value) ) {
+                       } elseif( $value === null ) {
                                if ( $mode == LIST_AND || $mode == LIST_OR ) {
                                        $list .= "$field IS ";
                                } elseif ( $mode == LIST_SET ) {
@@ -1401,12 +1268,33 @@ class Database {
                return $list;
        }
 
+       /**
+        * Bitwise operations
+        */
+
+       function bitNot($field) {
+               return "(~$bitField)";
+       }
+
+       function bitAnd($fieldLeft, $fieldRight) {
+               return "($fieldLeft & $fieldRight)";
+       }
+
+       function bitOr($fieldLeft, $fieldRight) {
+               return "($fieldLeft | $fieldRight)";
+       }
+
        /**
         * Change the current database
+        *
+        * @return bool Success or failure
         */
        function selectDB( $db ) {
-               $this->mDBname = $db;
-               return mysql_select_db( $db, $this->mConn );
+               # Stub.  Shouldn't cause serious problems if it's not overridden, but
+               # if your database engine supports a concept similar to MySQL's
+               # databases you may as well.  TODO: explain what exactly will fail if
+               # this is not overridden.
+               return true;
        }
 
        /**
@@ -1433,8 +1321,8 @@ class Database {
         * 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
+        * @param $name String: database table name
+        * @return String: full database name
         */
        function tableName( $name ) {
                global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
@@ -1562,20 +1450,35 @@ class Database {
        }
 
        /**
-        * Wrapper for addslashes()
-        * @param string $s String to be slashed.
-        * @return string slashed string.
+        * Get the name of an index in a given table
         */
-       function strencode( $s ) {
-               return mysql_real_escape_string( $s, $this->mConn );
+       function indexName( $index ) {
+               // Backwards-compatibility hack
+               $renamed = array(
+                       'ar_usertext_timestamp' => 'usertext_timestamp',
+                       'un_user_id'            => 'user_id',
+                       'un_user_ip'            => 'user_ip',
+               );
+               if( isset( $renamed[$index] ) ) {
+                       return $renamed[$index];
+               } else {
+                       return $index;
+               }
        }
 
+       /**
+        * Wrapper for addslashes()
+        * @param $s String: to be slashed.
+        * @return String: slashed string.
+        */
+       abstract function strencode( $s );
+
        /**
         * If it's a string, adds quotes and backslashes
         * Otherwise returns as-is
         */
        function addQuotes( $s ) {
-               if ( is_null( $s ) ) {
+               if ( $s === null ) {
                        return 'NULL';
                } else {
                        # This will also quote numeric values. This should be harmless,
@@ -1587,14 +1490,59 @@ class Database {
        }
 
        /**
-        * Escape string for safe LIKE usage
+        * Escape string for safe LIKE usage.
+        * WARNING: you should almost never use this function directly,
+        * instead use buildLike() that escapes everything automatically
         */
        function escapeLike( $s ) {
-               $s=$this->strencode( $s );
-               $s=str_replace(array('%','_'),array('\%','\_'),$s);
+               $s = str_replace( '\\', '\\\\', $s );
+               $s = $this->strencode( $s );
+               $s = str_replace( array( '%', '_' ), array( '\%', '\_' ), $s );
                return $s;
        }
 
+       /**
+        * LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match
+        * containing either string literals that will be escaped or tokens returned by anyChar() or anyString().
+        * Alternatively, the function could be provided with an array of aforementioned parameters.
+        * 
+        * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns a LIKE clause that searches
+        * for subpages of 'My page title'.
+        * Alternatively: $pattern = array( 'My_page_title/', $dbr->anyString() ); $query .= $dbr->buildLike( $pattern );
+        *
+        * @ return String: fully built LIKE statement
+        */
+       function buildLike() {
+               $params = func_get_args();
+               if (count($params) > 0 && is_array($params[0])) {
+                       $params = $params[0];
+               }
+
+               $s = '';
+               foreach( $params as $value) {
+                       if( $value instanceof LikeMatch ) {
+                               $s .= $value->toString();
+                       } else {
+                               $s .= $this->escapeLike( $value );
+                       }
+               }
+               return " LIKE '" . $s . "' ";
+       }
+
+       /**
+        * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query
+        */
+       function anyChar() {
+               return new LikeMatch( '_' );
+       }
+
+       /**
+        * Returns a token for buildLike() that denotes a '%' to be used in a LIKE query
+        */
+       function anyString() {
+               return new LikeMatch( '%' );
+       }
+
        /**
         * Returns an appropriately quoted sequence value for inserting a new row.
         * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
@@ -1605,11 +1553,15 @@ class Database {
        }
 
        /**
-        * USE INDEX clause
-        * PostgreSQL doesn't have them and returns ""
+        * USE INDEX clause.  Unlikely to be useful for anything but MySQL.  This
+        * is only needed because a) MySQL must be as efficient as possible due to
+        * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
+        * which index to pick.  Anyway, other databases might have different
+        * indexes on a given table.  So don't bother overriding this unless you're
+        * MySQL.
         */
        function useIndexClause( $index ) {
-               return "FORCE INDEX ($index)";
+               return '';
        }
 
        /**
@@ -1655,11 +1607,12 @@ class Database {
         *
         * 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
+        * @param $delTable String: The table to delete from.
+        * @param $joinTable String: The other table.
+        * @param $delVar String: The variable to join on, in the first table.
+        * @param $joinVar String: The variable to join on, in the second table.
+        * @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause
+        * @param $fname String: Calling function name (use __METHOD__) for logs/profiling
         */
        function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
                if ( !$conds ) {
@@ -1696,10 +1649,14 @@ class Database {
        }
 
        /**
+        * A string to insert into queries to show that they're low-priority, like
+        * MySQL's LOW_PRIORITY.  If no such feature exists, return an empty
+        * string and nothing bad should happen.
+        *
         * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
         */
        function lowPriorityOption() {
-               return 'LOW_PRIORITY';
+               return '';
        }
 
        /**
@@ -1753,44 +1710,77 @@ class Database {
        }
 
        /**
-        * 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)
+        * Construct a LIMIT query with optional offset.  This is used for query
+        * pages.  The SQL should be adjusted so that only the first $limit rows
+        * are returned.  If $offset is provided as well, then the first $offset
+        * rows should be discarded, and the next $limit rows should be returned.
+        * If the result of the query is not ordered, then the rows to be returned
+        * are theoretically arbitrary.
+        *
+        * $sql is expected to be a SELECT, if that makes a difference.  For
+        * UPDATE, limitResultForUpdate should be used.
+        *
+        * The version provided by default works in MySQL and SQLite.  It will very
+        * likely need to be overridden for most other DBMSes.
+        *
+        * @param $sql String: SQL query we will append the limit too
+        * @param $limit Integer: the SQL limit
+        * @param $offset Integer the SQL offset (default false)
         */
-       function limitResult($sql, $limit, $offset=false) {
-               if( !is_numeric($limit) ) {
+       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);
+       function limitResultForUpdate( $sql, $num ) {
+               return $this->limitResult( $sql, $num, 0 );
+       }
+
+       /**
+        * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries 
+        * within the UNION construct.
+        * @return Boolean
+        */
+       function unionSupportsOrderAndLimit() {
+               return true; // True for almost every DB supported
+       }
+
+       /**
+        * Construct a UNION query
+        * This is used for providing overload point for other DB abstractions
+        * not compatible with the MySQL syntax.
+        * @param $sqls Array: SQL statements to combine
+        * @param $all Boolean: use UNION ALL
+        * @return String: SQL fragment
+        */
+       function unionQueries($sqls, $all) {
+               $glue = $all ? ') UNION ALL (' : ') UNION (';
+               return '('.implode( $glue, $sqls ) . ')';
        }
 
        /**
-        * Returns an SQL expression for a simple conditional.
-        * Uses IF on MySQL.
+        * Returns an SQL expression for a simple conditional.  This doesn't need
+        * to be overridden unless CASE isn't supported in your DBMS.
         *
-        * @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
+        * @param $cond String: SQL expression which will result in a boolean value
+        * @param $trueVal String: SQL expression to return if true
+        * @param $falseVal String: SQL expression to return if false
+        * @return String: SQL fragment
         */
        function conditional( $cond, $trueVal, $falseVal ) {
-               return " IF($cond, $trueVal, $falseVal) ";
+               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
        }
 
        /**
         * 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
+        * @param $orig String: column to modify
+        * @param $old String: column to seek
+        * @param $new String: column to replace with
         */
        function strreplace( $orig, $old, $new ) {
                return "REPLACE({$orig}, {$old}, {$new})";
@@ -1798,9 +1788,27 @@ class Database {
 
        /**
         * Determines if the last failure was due to a deadlock
+        * STUB
         */
        function wasDeadlock() {
-               return $this->lastErrno() == 1213;
+               return false;
+       }
+
+       /**
+        * Determines if the last query error was something that should be dealt 
+        * with by pinging the connection and reissuing the query.
+        * STUB
+        */
+       function wasErrorReissuable() {
+               return false;
+       }
+
+       /**
+        * Determines if the last failure was due to the database being read-only.
+        * STUB
+        */
+       function wasReadOnlyError() {
+               return false;
        }
 
        /**
@@ -1861,9 +1869,8 @@ class Database {
        /**
         * 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
+        * @param $pos MySQLMasterPos object
+        * @param $timeout Integer: the maximum number of seconds to wait for synchronisation
         */
        function masterPosWait( MySQLMasterPos $pos, $timeout ) {
                $fname = 'Database::masterPosWait';
@@ -1919,7 +1926,8 @@ class Database {
                $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 );
+                       $pos = isset($row->Exec_master_log_pos) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos;
+                       return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
                } else {
                        return false;
                }
@@ -2024,41 +2032,31 @@ class Database {
        }
 
        /**
-        * @return string wikitext of a link to the server software's web site
+        * Returns a wikitext link to the DB's website, e.g.,
+        *     return "[http://www.mysql.com/ MySQL]";
+        * Should at least contain plain text, if for some reason
+        * your database has no website.
+        *
+        * @return String: wikitext of a link to the server software's web site
         */
-       function getSoftwareLink() {
-               return "[http://www.mysql.com/ MySQL]";
-       }
+       abstract function getSoftwareLink();
 
        /**
-        * @return string Version information from the database
+        * A string describing the current software version, like from
+        * mysql_get_server_info().  Will be listed on Special:Version, etc.
+        * 
+        * @return String: Version information from the database
         */
-       function getServerVersion() {
-               return mysql_get_server_info( $this->mConn );
-       }
+       abstract function getServerVersion();
 
        /**
         * Ping the server and try to reconnect if it there is no connection
+        *
+        * @return bool Success or failure
         */
        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;
+               # Stub.  Not essential to override.
+               return true;
        }
 
        /**
@@ -2070,7 +2068,7 @@ class Database {
                        wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
                        return $this->mFakeSlaveLag;
                }
-               $res = $this->query( 'SHOW PROCESSLIST' );
+               $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ );
                # Find slave SQL thread
                while ( $row = $this->fetchObject( $res ) ) {
                        /* This should work for most situations - when default db 
@@ -2084,7 +2082,10 @@ class Database {
                                $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'
+                               $row->State != 'Requesting binlog dump' && 
+                               $row->State != 'Waiting to reconnect after a failed master event read' &&
+                               $row->State != 'Reconnecting after a failed master event read' &&
+                               $row->State != 'Registering slave on master'
                                ) {
                                # This is it, return the time (except -ve)
                                if ( $row->Time > 0x7fffffff ) {
@@ -2125,23 +2126,21 @@ class Database {
        }
 
        /**
-        * 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
+        * 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.  May or may not be necessary for non-MySQL
+        * databases.  For most purposes, leaving it as a no-op should be fine.
+        *
+        * @param $timeout Integer in seconds
         */
-       public function setTimeout( $timeout ) {
-               $this->query( "SET net_read_timeout=$timeout" );
-               $this->query( "SET net_write_timeout=$timeout" );
-       }
+       public function setTimeout( $timeout ) {}
 
        /**
         * Read and execute SQL commands from a file.
         * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
-        * @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
+        * @param $filename String: File name to open
+        * @param $lineCallback Callback: Optional function called before reading each line
+        * @param $resultCallback Callback: Optional function called for each MySQL result
         */
        function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
                $fp = fopen( $filename, 'r' );
@@ -2153,12 +2152,29 @@ class Database {
                return $error;
        }
 
+       /**
+        * Get the full path of a patch file. Originally based on archive()
+        * from updaters.inc. Keep in mind this always returns a patch, as 
+        * it fails back to MySQL if no DB-specific patch can be found
+        *
+        * @param $patch String The name of the patch, like patch-something.sql
+        * @return String Full path to patch file
+        */
+       public static function patchPath( $patch ) {
+               global $wgDBtype, $IP;
+               if ( file_exists( "$IP/maintenance/$wgDBtype/archives/$name" ) ) {
+                       return "$IP/maintenance/$wgDBtype/archives/$name";
+               } else {
+                       return "$IP/maintenance/archives/$name";
+               }
+       }
+
        /**
         * Read and execute commands from an open file handle
         * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
-        * @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
+        * @param $fp String: File handle
+        * @param $lineCallback Callback: Optional function called before reading each line
+        * @param $resultCallback Callback: Optional function called for each MySQL result
         */
        function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
                $cmd = "";
@@ -2237,8 +2253,12 @@ class Database {
                }
 
                // Table prefixes
-               $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-zA-Z_0-9]*)/',
-                       array( &$this, 'tableNameCallback' ), $ins );
+               $ins = preg_replace_callback( '!/\*(?:\$wgDBprefix|_)\*/([a-zA-Z_0-9]*)!',
+                       array( $this, 'tableNameCallback' ), $ins );
+
+               // Index names
+               $ins = preg_replace_callback( '!/\*i\*/([a-zA-Z_0-9]*)!', 
+                       array( $this, 'indexNameCallback' ), $ins );
                return $ins;
        }
 
@@ -2250,73 +2270,96 @@ class Database {
                return $this->tableName( $matches[1] );
        }
 
-       /*
+       /**
+        * Index name callback
+        */
+       protected function indexNameCallback( $matches ) {
+               return $this->indexName( $matches[1] );
+       }
+
+       /**
         * Build a concatenation list to feed into a SQL query
-       */
+        * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting
+        * @return String
+        */
        function buildConcat( $stringList ) {
                return 'CONCAT(' . implode( ',', $stringList ) . ')';
        }
        
        /**
-        * Acquire a lock
+        * Acquire a named lock
         * 
         * Abstracted from Filestore::lock() so child classes can implement for
         * their own needs.
         * 
-        * @param string $lockName Name of lock to aquire
-        * @param string $method Name of method calling us
+        * @param $lockName String: Name of lock to aquire
+        * @param $method String: Name of method calling us
         * @return bool
         */
-       public function lock( $lockName, $method ) {
-               $lockName = $this->addQuotes( $lockName );
-               $result = $this->query( "SELECT GET_LOCK($lockName, 5) AS lockstatus", $method );
-               $row = $this->fetchObject( $result );
-               $this->freeResult( $result );
-
-               if( $row->lockstatus == 1 ) {
-                       return true;
-               } else {
-                       wfDebug( __METHOD__." failed to acquire lock\n" );
-                       return false;
-               }
+       public function lock( $lockName, $method, $timeout = 5 ) {
+               return true;
        }
+
        /**
         * Release a lock.
         * 
-        * @todo fixme - Figure out a way to return a bool
-        * based on successful lock release.
-        * 
-        * @param string $lockName Name of lock to release
-        * @param string $method Name of method calling us
+        * @param $lockName String: Name of lock to release
+        * @param $method String: Name of method calling us
+        *
+        * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
+        * @return Returns 1 if the lock was released, 0 if the lock was not established
+        * by this thread (in which case the lock is not released), and NULL if the named 
+        * lock did not exist
         */
        public function unlock( $lockName, $method ) {
-               $lockName = $this->addQuotes( $lockName );
-               $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
-               $this->freeResult( $result );
+               return true;
+       }
+
+       /**
+        * Lock specific tables
+        *
+        * @param $read Array of tables to lock for read access
+        * @param $write Array of tables to lock for write access
+        * @param $method String name of caller
+        * @param $lowPriority bool Whether to indicate writes to be LOW PRIORITY
+        */
+       public function lockTables( $read, $write, $method, $lowPriority = true ) {
+               return true;
+       }
+       
+       /**
+        * Unlock specific tables
+        *
+        * @param $method String the caller
+        */
+       public function unlockTables( $method ) {
+               return true;
        }
        
        /**
         * Get search engine class. All subclasses of this
         * need to implement this if they wish to use searching.
         * 
-        * @return string
+        * @return String
         */
        public function getSearchEngine() {
                return "SearchMySQL";
        }
-}
 
-/**
- * Database abstraction object for mySQL
- * Inherit all methods and properties of Database::Database()
- *
- * @ingroup Database
- * @see Database
- */
-class DatabaseMysql extends Database {
-       # Inherit all
+       /**
+        * Allow or deny "big selects" for this session only. This is done by setting 
+        * the sql_big_selects session variable.
+        *
+        * This is a MySQL-specific feature. 
+        *
+        * @param mixed $value true for allow, false for deny, or "default" to restore the initial value
+        */
+       public function setBigSelects( $value = true ) {
+               // no-op
+       }
 }
 
+
 /******************************************************************************
  * Utility classes
  *****************************************************************************/
@@ -2423,13 +2466,22 @@ class DBError extends MWException {
 
        /**
         * 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
+        * @param $db Database object which threw the error
+        * @param $error A simple error message to be used for debugging
         */
-       function __construct( Database &$db, $error ) {
+       function __construct( DatabaseBase &$db, $error ) {
                $this->db =& $db;
                parent::__construct( $error );
        }
+
+       function getText() {
+               global $wgShowDBErrorBacktrace;
+               $s = $this->getMessage() . "\n";
+               if ( $wgShowDBErrorBacktrace ) {
+                       $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
+               }
+               return $s;
+       }
 }
 
 /**
@@ -2438,7 +2490,7 @@ class DBError extends MWException {
 class DBConnectionError extends DBError {
        public $error;
        
-       function __construct( Database &$db, $error = 'unknown error' ) {
+       function __construct( DatabaseBase &$db, $error = 'unknown error' ) {
                $msg = 'DB connection error';
                if ( trim( $error ) != '' ) {
                        $msg .= ": $error";
@@ -2457,54 +2509,33 @@ class DBConnectionError extends DBError {
                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";
+               global $wgSitename, $wgLang;
+               $header = "$wgSitename has a problem";
+               if ( $wgLang instanceof Language ) {
+                       $header = htmlspecialchars( $wgLang->getMessage( 'dberr-header' ) );
+               }
+               
+               return $header;
        }
 
        function getHTML() {
-               global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
-               global $wgSitename, $wgServer, $wgMessageCache;
+               global $wgLang, $wgMessageCache, $wgUseFileCache, $wgShowDBErrorBacktrace;
 
-               # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
-               # Hard coding strings instead.
+               $sorry = 'Sorry! This site is experiencing technical difficulties.';
+               $again = 'Try waiting a few minutes and reloading.';
+               $info  = '(Can\'t contact the database server: $1)';
 
-               $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. ";
+               if ( $wgLang instanceof Language ) {
+                       $sorry = htmlspecialchars( $wgLang->getMessage( 'dberr-problems' ) );
+                       $again = htmlspecialchars( $wgLang->getMessage( 'dberr-again' ) );
+                       $info  = htmlspecialchars( $wgLang->getMessage( 'dberr-info' ) );
+               }
 
                # No database access
                if ( is_object( $wgMessageCache ) ) {
@@ -2515,47 +2546,106 @@ border=\"0\" ALT=\"Google\"></A>
                        $this->error = $this->db->getProperty('mServer');
                }
 
+               $noconnect = "<p><strong>$sorry</strong><br />$again</p><p><small>$info</small></p>";
                $text = str_replace( '$1', $this->error, $noconnect );
 
-               /*
-               if ( $GLOBALS['wgShowExceptionDetails'] ) {
-                       $text .= '</p><p>Backtrace:</p><p>' . 
-                               nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . 
-                               "</p>\n";
-               }*/
-
-               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 );
+               if ( $wgShowDBErrorBacktrace ) {
+                       $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+               }
+
+               $extra = $this->searchForm();
+
+               if( $wgUseFileCache ) {
+                       try {
+                               $cache = $this->fileCachedPage();
+                               # Cached version on file system?
+                               if( $cache !== null ) {
+                                       # Hack: extend the body for error messages
+                                       $cache = str_replace( array('</html>','</body>'), '', $cache );
+                                       # Add cache notice...
+                                       $cachederror = "This is a cached copy of the requested page, and may not be up to date. ";
+                                       # Localize it if possible...
+                                       if( $wgLang instanceof Language ) {
+                                               $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) );
+                                       }
+                                       $warning = "<div style='color:red;font-size:150%;font-weight:bold;'>$cachederror</div>";
+                                       # Output cached page with notices on bottom and re-close body
+                                       return "{$cache}{$warning}<hr />$text<hr />$extra</body></html>";
                                }
+                       } catch( MWException $e ) {
+                               // Do nothing, just use the default page
                        }
+               }
+               # Headers needed here - output is just the error message
+               return $this->htmlHeader()."$text<hr />$extra".$this->htmlFooter();
+       }
 
-                       $cache = new HTMLFileCache( $t );
-                       if( $cache->isFileCached() ) {
-                               // @todo, FIXME: $msg is not defined on the next line.
-                               $msg = '<p style="color: red"><b>'.$text."<br />\n" .
-                                       $cachederror . "</b></p>\n";
-
-                               $tag = '<div id="article">';
-                               $text = str_replace(
-                                       $tag,
-                                       $tag . $text,
-                                       $cache->fetchPageText() );
-                       }
+       function searchForm() {
+               global $wgSitename, $wgServer, $wgLang, $wgInputEncoding;
+               $usegoogle = "You can try searching via Google in the meantime.";
+               $outofdate = "Note that their indexes of our content may be out of date.";
+               $googlesearch = "Search";
+
+               if ( $wgLang instanceof Language ) {
+                       $usegoogle = htmlspecialchars( $wgLang->getMessage( 'dberr-usegoogle' ) );
+                       $outofdate = htmlspecialchars( $wgLang->getMessage( 'dberr-outofdate' ) );
+                       $googlesearch  = htmlspecialchars( $wgLang->getMessage( 'searchbutton' ) );
+               }
+
+               $search = htmlspecialchars(@$_REQUEST['search']);
+
+               $trygoogle = <<<EOT
+<div style="margin: 1.5em">$usegoogle<br />
+<small>$outofdate</small></div>
+<!-- SiteSearch Google -->
+<form method="get" action="http://www.google.com/search" id="googlesearch">
+    <input type="hidden" name="domains" value="$wgServer" />
+    <input type="hidden" name="num" value="50" />
+    <input type="hidden" name="ie" value="$wgInputEncoding" />
+    <input type="hidden" name="oe" value="$wgInputEncoding" />
+
+    <img src="http://www.google.com/logos/Logo_40wht.gif" alt="" style="float:left; margin-left: 1.5em; margin-right: 1.5em;" />
+
+    <input type="text" name="q" size="31" maxlength="255" value="$search" />
+    <input type="submit" name="btnG" value="$googlesearch" />
+  <div>
+    <input type="radio" name="sitesearch" id="gwiki" value="$wgServer" checked="checked" /><label for="gwiki">$wgSitename</label>
+    <input type="radio" name="sitesearch" id="gWWW" value="" /><label for="gWWW">WWW</label>
+  </div>
+</form>
+<!-- SiteSearch Google -->
+EOT;
+               return $trygoogle;
+       }
+
+       function fileCachedPage() {
+               global $wgTitle, $title, $wgLang, $wgOut;
+               if( $wgOut->isDisabled() ) return; // Done already?
+               $mainpage = 'Main Page';
+               if ( $wgLang instanceof Language ) {
+                       $mainpage    = htmlspecialchars( $wgLang->getMessage( 'mainpage' ) );
+               }
+
+               if( $wgTitle ) {
+                       $t =& $wgTitle;
+               } elseif( $title ) {
+                       $t = Title::newFromURL( $title );
+               } else {
+                       $t = Title::newFromText( $mainpage );
                }
 
-               return $text;
+               $cache = new HTMLFileCache( $t );
+               if( $cache->isFileCached() ) {
+                       return $cache->fetchPageText();
+               } else {
+                       return '';
+               }
        }
+       
+       function htmlBodyOnly() {
+               return true;
+       }
+
 }
 
 /**
@@ -2564,7 +2654,7 @@ border=\"0\" ALT=\"Google\"></A>
 class DBQueryError extends DBError {
        public $error, $errno, $sql, $fname;
        
-       function __construct( Database &$db, $error, $errno, $sql, $fname ) {
+       function __construct( DatabaseBase &$db, $error, $errno, $sql, $fname ) {
                $message = "A database error has occurred\n" .
                  "Query: $sql\n" .
                  "Function: $fname\n" .
@@ -2578,11 +2668,16 @@ class DBQueryError extends DBError {
        }
 
        function getText() {
+               global $wgShowDBErrorBacktrace;
                if ( $this->useMessageCache() ) {
-                       return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
-                         htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+                       $s = wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
+                               htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+                       if ( $wgShowDBErrorBacktrace ) {
+                               $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
+                       }
+                       return $s;
                } else {
-                       return $this->getMessage();
+                       return parent::getText();
                }
        }
        
@@ -2605,12 +2700,17 @@ class DBQueryError extends DBError {
        }
 
        function getHTML() {
+               global $wgShowDBErrorBacktrace;
                if ( $this->useMessageCache() ) {
-                       return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
+                       $s = wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
                          htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
                } else {
-                       return nl2br( htmlspecialchars( $this->getMessage() ) );
+                       $s = nl2br( htmlspecialchars( $this->getMessage() ) );
+               }
+               if ( $wgShowDBErrorBacktrace ) {
+                       $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
                }
+               return $s;
        }
 }
 
@@ -2643,7 +2743,7 @@ class ResultWrapper implements Iterator {
         * Get the number of rows in a result object
         */
        function numRows() {
-               return $this->db->numRows( $this->result );
+               return $this->db->numRows( $this );
        }
 
        /**
@@ -2656,7 +2756,7 @@ class ResultWrapper implements Iterator {
         * @throws DBUnexpectedError Thrown if the database returns an error
         */
        function fetchObject() {
-               return $this->db->fetchObject( $this->result );
+               return $this->db->fetchObject( $this );
        }
 
        /**
@@ -2668,14 +2768,14 @@ class ResultWrapper implements Iterator {
         * @throws DBUnexpectedError Thrown if the database returns an error
         */
        function fetchRow() {
-               return $this->db->fetchRow( $this->result );
+               return $this->db->fetchRow( $this );
        }
 
        /**
         * Free a result object
         */
        function free() {
-               $this->db->freeResult( $this->result );
+               $this->db->freeResult( $this );
                unset( $this->result );
                unset( $this->db );
        }
@@ -2685,7 +2785,7 @@ class ResultWrapper implements Iterator {
         * See mysql_data_seek()
         */
        function seek( $row ) {
-               $this->db->dataSeek( $this->result, $row );
+               $this->db->dataSeek( $this, $row );
        }
 
        /*********************
@@ -2696,7 +2796,7 @@ class ResultWrapper implements Iterator {
 
        function rewind() {
                if ($this->numRows()) {
-                       $this->db->dataSeek($this->result, 0);
+                       $this->db->dataSeek($this, 0);
                }
                $this->pos = 0;
                $this->currentRow = null;
@@ -2724,15 +2824,18 @@ class ResultWrapper implements Iterator {
        }
 }
 
-class MySQLMasterPos {
-       var $file, $pos;
+/**
+ * Used by DatabaseBase::buildLike() to represent characters that have special meaning in SQL LIKE clauses
+ * and thus need no escaping. Don't instantiate it manually, use Database::anyChar() and anyString() instead.
+ */
+class LikeMatch {
+       private $str;
 
-       function __construct( $file, $pos ) {
-               $this->file = $file;
-               $this->pos = $pos;
+       public function __construct( $s ) {
+               $this->str = $s;
        }
 
-       function __toString() {
-               return "{$this->file}/{$this->pos}";
+       public function toString() {
+               return $this->str;
        }
-}
+}
\ No newline at end of file