(bug 20275) Fixed LIKE queries on SQLite backend
[lhc/web/wiklou.git] / includes / db / Database.php
index 8d3d25a..e3d3906 100644 (file)
@@ -19,7 +19,7 @@ define( 'DEADLOCK_DELAY_MAX', 1500000 );
  * Database abstraction object
  * @ingroup Database
  */
-class Database {
+abstract class DatabaseBase {
 
 #------------------------------------------------------------------------------
 # Variables
@@ -39,6 +39,7 @@ class Database {
        protected $mErrorCount = 0;
        protected $mLBInfo = array();
        protected $mFakeSlaveLag = null, $mFakeMaster = false;
+       protected $mDefaultBigSelects = null;
 
 #------------------------------------------------------------------------------
 # Accessors
@@ -191,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';
@@ -225,14 +234,37 @@ class Database {
         */
        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);
        }
@@ -306,7 +338,7 @@ class Database {
        }
 
        /**
-        * Same as new Database( ... ), kept for backward compatibility
+        * 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
@@ -316,7 +348,7 @@ class Database {
         */
        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 );
        }
 
        /**
@@ -327,114 +359,7 @@ class Database {
         * @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;
@@ -466,17 +391,9 @@ class Database {
         *
         * @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;
        }
 
        /**
@@ -629,14 +546,7 @@ class Database {
         * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
         * @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 String
@@ -762,12 +672,8 @@ class Database {
         * @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.
        }
 
        /**
@@ -779,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
@@ -798,43 +695,20 @@ 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
@@ -843,12 +717,7 @@ class Database {
         * @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
@@ -860,7 +729,7 @@ 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
@@ -868,51 +737,25 @@ class Database {
         * @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 ); }
+       abstract function affectedRows();
 
        /**
         * Simple UPDATE wrapper
@@ -1095,7 +938,7 @@ class Database {
         * 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 functio name
+        * @param $fname String: Calling function name
         * @param $options Array
         * @param $join_conds Array
         *
@@ -1118,30 +961,27 @@ 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.
@@ -1257,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
@@ -1439,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;
        }
 
        /**
@@ -1621,9 +1471,7 @@ class Database {
         * @param $s String: to be slashed.
         * @return String: slashed string.
         */
-       function strencode( $s ) {
-               return mysql_real_escape_string( $s, $this->mConn );
-       }
+       abstract function strencode( $s );
 
        /**
         * If it's a string, adds quotes and backslashes
@@ -1642,15 +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=str_replace('\\','\\\\',$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
@@ -1661,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 (" . $this->indexName( $index ) . ")";
+               return '';
        }
 
        /**
@@ -1753,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 '';
        }
 
        /**
@@ -1810,27 +1710,60 @@ class Database {
        }
 
        /**
-        * Construct a LIMIT query with optional offset
-        * This is used for query pages
+        * 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 an SQL expression for a simple conditional.
-        * Uses IF on MySQL.
+        * 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.  This doesn't need
+        * to be overridden unless CASE isn't supported in your DBMS.
         *
         * @param $cond String: SQL expression which will result in a boolean value
         * @param $trueVal String: SQL expression to return if true
@@ -1838,7 +1771,7 @@ class Database {
         * @return String: SQL fragment
         */
        function conditional( $cond, $trueVal, $falseVal ) {
-               return " IF($cond, $trueVal, $falseVal) ";
+               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
        }
 
        /**
@@ -1855,17 +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
+        * with by pinging the connection and reissuing the query.
+        * STUB
         */
        function wasErrorReissuable() {
-               return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
+               return false;
+       }
+
+       /**
+        * Determines if the last failure was due to the database being read-only.
+        * STUB
+        */
+       function wasReadOnlyError() {
+               return false;
        }
 
        /**
@@ -2089,41 +2032,31 @@ class Database {
        }
 
        /**
+        * 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();
 
        /**
+        * 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;
        }
 
        /**
@@ -2149,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 ) {
@@ -2190,16 +2126,14 @@ 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.
+        * 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.
@@ -2218,6 +2152,23 @@ 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)
@@ -2326,15 +2277,17 @@ class Database {
                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.
@@ -2343,32 +2296,44 @@ class Database {
         * @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 $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;
        }
        
        /**
@@ -2380,19 +2345,21 @@ class Database {
        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
  *****************************************************************************/
@@ -2502,10 +2469,19 @@ class DBError extends MWException {
         * @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;
+       }
 }
 
 /**
@@ -2514,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";
@@ -2533,10 +2509,6 @@ class DBConnectionError extends DBError {
                return false;
        }
        
-       function getText() {
-               return $this->getMessage() . "\n";
-       }
-
        function getLogMessage() {
                # Don't send to the exception log
                return false;
@@ -2553,7 +2525,7 @@ class DBConnectionError extends DBError {
        }
 
        function getHTML() {
-               global $wgLang, $wgMessageCache, $wgUseFileCache;
+               global $wgLang, $wgMessageCache, $wgUseFileCache, $wgShowDBErrorBacktrace;
 
                $sorry = 'Sorry! This site is experiencing technical difficulties.';
                $again = 'Try waiting a few minutes and reloading.';
@@ -2577,12 +2549,9 @@ class DBConnectionError extends DBError {
                $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 ( $wgShowDBErrorBacktrace ) {
+                       $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+               }
 
                $extra = $this->searchForm();
 
@@ -2685,7 +2654,7 @@ EOT;
 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" .
@@ -2699,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();
                }
        }
        
@@ -2726,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;
        }
 }
 
@@ -2845,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