fix synxtax
[lhc/web/wiklou.git] / includes / Database.php
index 21fa09d..6966430 100644 (file)
@@ -15,6 +15,10 @@ define( 'DEADLOCK_DELAY_MAX', 1500000 );
  * Utility classes
  *****************************************************************************/
 
+/**
+ * Utility class.
+ * @addtogroup Database
+ */
 class DBObject {
        public $mData;
 
@@ -31,12 +35,82 @@ class DBObject {
        }
 };
 
+/**
+ * Utility class
+ * @addtogroup Database
+ *
+ * This allows us to distinguish a blob from a normal string and an array of strings
+ */
+class Blob {
+       private $mData;
+       function __construct($data) {
+               $this->mData = $data;
+       }
+       function fetch() {
+               return $this->mData;
+       }
+};
+
+/**
+ * Utility class.
+ * @addtogroup Database
+ */
+class MySQLField {
+       private $name, $tablename, $default, $max_length, $nullable,
+               $is_pk, $is_unique, $is_key, $type;
+       function __construct ($info) {
+               $this->name = $info->name;
+               $this->tablename = $info->table;
+               $this->default = $info->def;
+               $this->max_length = $info->max_length;
+               $this->nullable = !$info->not_null;
+               $this->is_pk = $info->primary_key;
+               $this->is_unique = $info->unique_key;
+               $this->is_multiple = $info->multiple_key;
+               $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
+               $this->type = $info->type;
+       }
+
+       function name() {
+               return $this->name;
+       }
+
+       function tableName() {
+               return $this->tableName;
+       }
+
+       function defaultValue() {
+               return $this->default;
+       }
+
+       function maxLength() {
+               return $this->max_length;
+       }
+
+       function nullable() {
+               return $this->nullable;
+       }
+
+       function isKey() {
+               return $this->is_key;
+       }
+
+       function isMultipleKey() {
+               return $this->is_multiple;
+       }
+
+       function type() {
+               return $this->type;
+       }
+}
+
 /******************************************************************************
  * Error classes
  *****************************************************************************/
 
 /**
  * Database error base class
+ * @addtogroup Database
  */
 class DBError extends MWException {
        public $db;
@@ -52,6 +126,9 @@ class DBError extends MWException {
        }
 }
 
+/**
+ * @addtogroup Database
+ */
 class DBConnectionError extends DBError {
        public $error;
        
@@ -153,7 +230,7 @@ border=\"0\" ALT=\"Google\"></A>
 
                        $cache = new HTMLFileCache( $t );
                        if( $cache->isFileCached() ) {
-                               // FIXME: $msg is not defined on the next line.
+                               // @todo, FIXME: $msg is not defined on the next line.
                                $msg = '<p style="color: red"><b>'.$msg."<br />\n" .
                                        $cachederror . "</b></p>\n";
 
@@ -169,6 +246,9 @@ border=\"0\" ALT=\"Google\"></A>
        }
 }
 
+/**
+ * @addtogroup Database
+ */
 class DBQueryError extends DBError {
        public $error, $errno, $sql, $fname;
        
@@ -222,12 +302,16 @@ class DBQueryError extends DBError {
        }
 }
 
+/**
+ * @addtogroup Database
+ */
 class DBUnexpectedError extends DBError {}
 
 /******************************************************************************/
 
 /**
  * Database abstraction object
+ * @addtogroup Database
  */
 class Database {
 
@@ -246,9 +330,6 @@ class Database {
        protected $mTrxLevel = 0;
        protected $mErrorCount = 0;
        protected $mLBInfo = array();
-       protected $mCascadingDeletes = false;
-       protected $mCleanupTriggers = false;
-       protected $mStrictIPs = false;
 
 #------------------------------------------------------------------------------
 # Accessors
@@ -343,14 +424,14 @@ class Database {
         * Returns true if this database supports (and uses) cascading deletes
         */
        function cascadingDeletes() {
-               return $this->mCascadingDeletes;
+               return false;
        }
 
        /**
         * Returns true if this database supports (and uses) triggers (e.g. on the page table)
         */
        function cleanupTriggers() {
-               return $this->mCleanupTriggers;
+               return false;
        }
 
        /**
@@ -358,7 +439,7 @@ class Database {
         * Specifically, it uses a NULL value instead of an empty string.
         */
        function strictIPs() {
-               return $this->mStrictIPs;
+               return false;
        }
 
        /**
@@ -375,6 +456,14 @@ class Database {
                return true;
        }
 
+       /**
+        * Returns true if this database does an implicit order by when the column has an index
+        * For example: SELECT page_title FROM page LIMIT 1
+        */
+       function implicitOrderby() {
+               return true;
+       }
+
        /**
         * Returns true if this database can do a native search on IP columns
         * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
@@ -383,6 +472,13 @@ class Database {
                return false;
        }
 
+       /**
+        * Returns true if this database can use functional indexes
+        */
+       function functionalIndexes() {
+               return false;
+       }
+
        /**#@+
         * Get function
         */
@@ -414,13 +510,11 @@ class Database {
 #------------------------------------------------------------------------------
 
        /**@{{
+        * Constructor.
         * @param string $server database server host
         * @param string $user database user name
         * @param string $password database user password
         * @param string $dbname database name
-        */
-
-       /**
         * @param failFunction
         * @param $flags
         * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
@@ -470,8 +564,7 @@ class Database {
         * @param failFunction
         * @param $flags
         */
-       static function newFromParams( $server, $user, $password, $dbName,
-               $failFunction = false, $flags = 0 )
+       static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
        {
                return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
        }
@@ -520,7 +613,7 @@ class Database {
                                @/**/$this->mConn = mysql_connect( $server, $user, $password, true );
                        }
                        if ($this->mConn === false) {
-                               $iplus = $i + 1;
+                               #$iplus = $i + 1;
                                #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); 
                        }
                }
@@ -548,12 +641,19 @@ class Database {
                }
 
                if ( $success ) {
-                       global $wgDBmysql5;
-                       if( $wgDBmysql5 ) {
+                       $version = $this->getServerVersion();
+                       if ( version_compare( $version, '4.1' ) >= 0 ) {
                                // Tell the server we're communicating with it in UTF-8.
                                // This may engage various charset conversions.
-                               $this->query( 'SET NAMES utf8' );
+                               global $wgDBmysql5;
+                               if( $wgDBmysql5 ) {
+                                       $this->query( 'SET NAMES utf8', __METHOD__ );
+                               }
+                               // Turn off strict mode
+                               $this->query( "SET sql_mode = ''", __METHOD__ );
                        }
+
+                       // Turn off strict mode if it is on
                } else {
                        $this->reportConnectionError();
                }
@@ -606,10 +706,18 @@ class Database {
        }
 
        /**
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns success
-        */
-       function query( $sql, $fname = '', $tempIgnore = false ) {
+        * Usually aborts on failure.  If errors are explicitly ignored, returns success.
+        *
+        * @param  $sql        String: SQL query
+        * @param  $fname      String: Name of the calling function, for profiling/SHOW PROCESSLIST 
+        *     comment (you can use __METHOD__ or add some extra info)
+        * @param  $tempIgnore Bool:   Whether to avoid throwing an exception on errors... 
+        *     maybe best to catch the exception instead?
+        * @return true for a successful write query, ResultWrapper object for a successful read query, 
+        *     or false on failure if $tempIgnore set
+        * @throws DBQueryError Thrown when the database returns an error of any kind
+        */
+       public function query( $sql, $fname = '', $tempIgnore = false ) {
                global $wgProfiling;
 
                if ( $wgProfiling ) {
@@ -633,17 +741,31 @@ class Database {
                $this->mLastQuery = $sql;
 
                # Add a comment for easy SHOW PROCESSLIST interpretation
-               if ( $fname ) {
-                       $commentedSql = preg_replace('/\s/', " /* $fname */ ", $sql, 1);
-               } else {
-                       $commentedSql = $sql;
-               }
+               #if ( $fname ) {
+                       global $wgUser;
+                       if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
+                               $userName = $wgUser->getName();
+                               if ( mb_strlen( $userName ) > 15 ) {
+                                       $userName = mb_substr( $userName, 0, 15 ) . '...';
+                               }
+                               $userName = str_replace( '/', '', $userName );
+                       } else {
+                               $userName = '';
+                       }
+                       $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
+               #} else {
+               #       $commentedSql = $sql;
+               #}
 
                # If DBO_TRX is set, start a transaction
                if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && 
-                       $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' 
-               ) {
-                       $this->begin();
+                       $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
+                       // avoid establishing transactions for SHOW and SET statements too -
+                       // that would delay transaction initializations to once connection 
+                       // is really used by application
+                       $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
+                       if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0) 
+                               $this->begin(); 
                }
 
                if ( $this->debug() ) {
@@ -681,14 +803,16 @@ class Database {
                        wfProfileOut( $queryProf );
                        wfProfileOut( $totalProf );
                }
-               return $ret;
+               return $this->resultObject( $ret );
        }
 
        /**
         * The DBMS-dependent part of query()
-        * @param string $sql SQL query.
+        * @param  $sql String: SQL query.
+        * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
+        * @access private
         */
-       function doQuery( $sql ) {
+       /*private*/ function doQuery( $sql ) {
                if( $this->bufferResults() ) {
                        $ret = mysql_query( $sql, $this->mConn );
                } else {
@@ -823,15 +947,27 @@ class Database {
         * Free a result object
         */
        function freeResult( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
                if ( !@/**/mysql_free_result( $res ) ) {
                        throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
                }
        }
 
        /**
-        * Fetch the next row from the given result object, in object form
+        * Fetch the next row from the given result object, in object form.
+        * Fields can be retrieved with $row->fieldname, with fields acting like
+        * member variables.
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
         */
        function fetchObject( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
                @/**/$row = mysql_fetch_object( $res );
                if( $this->lastErrno() ) {
                        throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
@@ -840,10 +976,17 @@ class Database {
        }
 
        /**
-        * Fetch the next row from the given result object
-        * Returns an array
+        * Fetch the next row from the given result object, in associative array
+        * form.  Fields are retrieved with $row['fieldname'].
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
         */
        function fetchRow( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
                @/**/$row = mysql_fetch_array( $res );
                if ( $this->lastErrno() ) {
                        throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
@@ -855,6 +998,9 @@ class Database {
         * Get the number of rows in a result object
         */
        function numRows( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
                @/**/$n = mysql_num_rows( $res );
                if( $this->lastErrno() ) {
                        throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
@@ -866,14 +1012,24 @@ class Database {
         * Get the number of fields in a result object
         * See documentation for mysql_num_fields()
         */
-       function numFields( $res ) { return mysql_num_fields( $res ); }
+       function numFields( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_num_fields( $res );
+       }
 
        /**
         * Get a field name in a result object
         * See documentation for mysql_field_name():
         * http://www.php.net/mysql_field_name
         */
-       function fieldName( $res, $n ) { return mysql_field_name( $res, $n ); }
+       function fieldName( $res, $n ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_field_name( $res, $n );
+       }
 
        /**
         * Get the inserted value of an auto-increment row
@@ -891,7 +1047,12 @@ class Database {
         * Change the position of the cursor in a result object
         * See mysql_data_seek()
         */
-       function dataSeek( $res, $row ) { return mysql_data_seek( $res, $row ); }
+       function dataSeek( $res, $row ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_data_seek( $res, $row );
+       }
 
        /**
         * Get the last error number
@@ -984,7 +1145,7 @@ class Database {
         * @return array
         */
        function makeSelectOptions( $options ) {
-               $tailOpts = '';
+               $preLimitTail = $postLimitTail = '';
                $startOpts = '';
 
                $noKeyOptions = array();
@@ -994,17 +1155,19 @@ class Database {
                        }
                }
 
-               if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}";
-               if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}";
+               if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+               if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+               if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
                
-               if (isset($options['LIMIT'])) {
-                       $tailOpts .= $this->limitResult('', $options['LIMIT'],
-                               isset($options['OFFSET']) ? $options['OFFSET'] : false);
-               }
+               //if (isset($options['LIMIT'])) {
+               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
+               //              isset($options['OFFSET']) ? $options['OFFSET'] 
+               //              : false);
+               //}
 
-               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
-               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
-               if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
 
                # Various MySQL extensions
                if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
@@ -1022,7 +1185,7 @@ class Database {
                        $useIndex = '';
                }
                
-               return array( $startOpts, $useIndex, $tailOpts );
+               return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
        }
 
        /**
@@ -1059,17 +1222,25 @@ class Database {
                        $from = '';
                }
 
-               list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $options );
+               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
 
                if( !empty( $conds ) ) {
                        if ( is_array( $conds ) ) {
                                $conds = $this->makeList( $conds, LIST_AND );
                        }
-                       $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $tailOpts";
+                       $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
                } else {
-                       $sql = "SELECT $startOpts $vars $from $useIndex $tailOpts";
+                       $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
                }
 
+               if (isset($options['LIMIT']))
+                       $sql = $this->limitResult($sql, $options['LIMIT'],
+                               isset($options['OFFSET']) ? $options['OFFSET'] : false);
+               $sql = "$sql $postLimitTail";
+               
+               if (isset($options['EXPLAIN'])) {
+                       $sql = 'EXPLAIN ' . $sql;
+               }
                return $this->query( $sql, $fname );
        }
 
@@ -1101,6 +1272,33 @@ class Database {
                return $obj;
 
        }
+       
+       /**
+        * Estimate rows in dataset
+        * Returns estimated count, based on EXPLAIN output
+        * Takes same arguments as Database::select()
+        */
+       
+       function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+               $options['EXPLAIN']=true;
+               $res = $this->select ($table, $vars, $conds, $fname, $options );
+               if ( $res === false )
+                       return false;
+               if (!$this->numRows($res)) {
+                       $this->freeResult($res);
+                       return 0;
+               }
+               
+               $rows=1;
+       
+               while( $plan = $this->fetchObject( $res ) ) {
+                       $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
+               }
+               
+               $this->freeResult($res);
+               return $rows;           
+       }
+       
 
        /**
         * Removes most variables from an SQL query and replaces them with X or N for numbers.
@@ -1219,11 +1417,11 @@ class Database {
        function fieldInfo( $table, $field ) {
                $table = $this->tableName( $table );
                $res = $this->query( "SELECT * FROM $table LIMIT 1" );
-               $n = mysql_num_fields( $res );
+               $n = mysql_num_fields( $res->result );
                for( $i = 0; $i < $n; $i++ ) {
-                       $meta = mysql_fetch_field( $res, $i );
+                       $meta = mysql_fetch_field( $res->result, $i );
                        if( $field == $meta->name ) {
-                               return $meta;
+                               return new MySQLField($meta);
                        }
                }
                return false;
@@ -1233,6 +1431,9 @@ class Database {
         * mysql_field_type() wrapper
         */
        function fieldType( $res, $index ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
                return mysql_field_type( $res, $index );
        }
 
@@ -1322,6 +1523,7 @@ class Database {
         *                       (for the log)
         * @param array  $options An array of UPDATE options, can be one or
         *                        more of IGNORE, LOW_PRIORITY
+        * @return bool
         */
        function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
                $table = $this->tableName( $table );
@@ -1330,7 +1532,7 @@ class Database {
                if ( $conds != '*' ) {
                        $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
                }
-               $this->query( $sql, $fname );
+               return $this->query( $sql, $fname );
        }
 
        /**
@@ -1365,8 +1567,23 @@ class Database {
                                $list .= "($value)";
                        } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
                                $list .= "$value";
-                       } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array ($value) ) {
-                               $list .= $field." IN (".$this->makeList($value).") ";
+                       } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
+                               if( count( $value ) == 0 ) {
+                                       // Empty input... or should this throw an error?
+                                       $list .= '0';
+                               } elseif( count( $value ) == 1 ) {
+                                       // Special-case single values, as IN isn't terribly efficient
+                                       $list .= $field." = ".$this->addQuotes( $value[0] );
+                               } else {
+                                       $list .= $field." IN (".$this->makeList($value).") ";
+                               }
+                       } elseif( is_null($value) ) {
+                               if ( $mode == LIST_AND || $mode == LIST_OR ) {
+                                       $list .= "$field IS ";
+                               } elseif ( $mode == LIST_SET ) {
+                                       $list .= "$field = ";
+                               }
+                               $list .= 'NULL';
                        } else {
                                if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
                                        $list .= "$field = ";
@@ -1401,7 +1618,7 @@ class Database {
                global $wgSharedDB;
                # Skip quoted literals
                if ( $name{0} != '`' ) {
-                       if ( $this->mTablePrefix !== '' &&  strpos( '.', $name ) === false ) {
+                       if ( $this->mTablePrefix !== '' &&  strpos( $name, '.' ) === false ) {
                                $name = "{$this->mTablePrefix}$name";
                        }
                        if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
@@ -1433,11 +1650,11 @@ class Database {
        }
        
        /**
-        * @desc: Fetch a number of table names into an zero-indexed numerical array
+        * Fetch a number of table names into an zero-indexed numerical array
         * This is handy when you need to construct SQL for joins
         *
         * Example:
-        * list( $user, $watchlist ) = $dbr->tableNames('user','watchlist');
+        * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
         * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
         *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
         */
@@ -1868,7 +2085,12 @@ class Database {
         */
        function resultObject( $result ) {
                if( empty( $result ) ) {
-                       return NULL;
+                       return false;
+               } elseif ( $result instanceof ResultWrapper ) {
+                       return $result;
+               } elseif ( $result === true ) {
+                       // Successful write query
+                       return $result;
                } else {
                        return new ResultWrapper( $this, $result );
                }
@@ -1913,8 +2135,7 @@ class Database {
         */
        function getLag() {
                $res = $this->query( 'SHOW PROCESSLIST' );
-               # Find slave SQL thread. Assumed to be the second one running, which is a bit
-               # dubious, but unfortunately there's no easy rigorous way
+               # Find slave SQL thread
                while ( $row = $this->fetchObject( $res ) ) {
                        /* This should work for most situations - when default db 
                         * for thread is not specified, it had no events executed, 
@@ -1982,18 +2203,36 @@ class Database {
        /**
         * Read and execute SQL commands from a file.
         * Returns true on success, error string on failure
+        * @param string $filename File name to open
+        * @param callback $lineCallback Optional function called before reading each line
+        * @param callback $resultCallback Optional function called for each MySQL result
         */
-       function sourceFile( $filename ) {
+       function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
                $fp = fopen( $filename, 'r' );
                if ( false === $fp ) {
                        return "Could not open \"{$filename}\".\n";
                }
+               $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
+               fclose( $fp );
+               return $error;
+       }
 
+       /**
+        * Read and execute commands from an open file handle
+        * Returns true on success, error string on failure
+        * @param string $fp File handle
+        * @param callback $lineCallback Optional function called before reading each line
+        * @param callback $resultCallback Optional function called for each MySQL result
+        */
+       function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
                $cmd = "";
                $done = false;
                $dollarquote = false;
 
                while ( ! feof( $fp ) ) {
+                       if ( $lineCallback ) {
+                               call_user_func( $lineCallback );
+                       }
                        $line = trim( fgets( $fp, 1024 ) );
                        $sl = strlen( $line ) - 1;
 
@@ -2023,7 +2262,10 @@ class Database {
                        if ( $done ) {
                                $cmd = str_replace(';;', ";", $cmd);
                                $cmd = $this->replaceVars( $cmd );
-                               $res = $this->query( $cmd, 'dbsource', true );
+                               $res = $this->query( $cmd, __METHOD__, true );
+                               if ( $resultCallback ) {
+                                       call_user_func( $resultCallback, $res );
+                               }
 
                                if ( false === $res ) {
                                        $err = $this->lastError();
@@ -2034,10 +2276,10 @@ class Database {
                                $done = false;
                        }
                }
-               fclose( $fp );
                return true;
        }
 
+
        /**
         * Replace variables in sourced SQL
         */
@@ -2045,7 +2287,7 @@ class Database {
                $varnames = array(
                        'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
                        'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
-                       'wgDBadminuser', 'wgDBadminpassword',
+                       'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
                );
 
                // Ordinary variables
@@ -2072,12 +2314,20 @@ class Database {
                return $this->tableName( $matches[1] );
        }
 
+       /*
+        * Build a concatenation list to feed into a SQL query
+       */
+       function buildConcat( $stringList ) {
+               return 'CONCAT(' . implode( ',', $stringList ) . ')';
+       }
+
 }
 
 /**
  * Database abstraction object for mySQL
  * Inherit all methods and properties of Database::Database()
  *
+ * @addtogroup Database
  * @see Database
  */
 class DatabaseMysql extends Database {
@@ -2087,42 +2337,57 @@ class DatabaseMysql extends Database {
 
 /**
  * Result wrapper for grabbing data queried by someone else
- *
+ * @addtogroup Database
  */
-class ResultWrapper {
-       var $db, $result;
+class ResultWrapper implements Iterator {
+       var $db, $result, $pos = 0, $currentRow = null;
 
        /**
-        * @todo document
+        * Create a new result object from a result resource and a Database object
         */
-       function ResultWrapper( &$database, $result ) {
-               $this->db =& $database;
-               $this->result =& $result;
+       function ResultWrapper( $database, $result ) {
+               $this->db = $database;
+               if ( $result instanceof ResultWrapper ) {
+                       $this->result = $result->result;
+               } else {
+                       $this->result = $result;
+               }
        }
 
        /**
-        * @todo document
+        * Get the number of rows in a result object
         */
        function numRows() {
                return $this->db->numRows( $this->result );
        }
 
        /**
-        * @todo document
+        * Fetch the next row from the given result object, in object form.
+        * Fields can be retrieved with $row->fieldname, with fields acting like
+        * member variables.
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
         */
        function fetchObject() {
                return $this->db->fetchObject( $this->result );
        }
 
        /**
-        * @todo document
+        * Fetch the next row from the given result object, in associative array
+        * form.  Fields are retrieved with $row['fieldname'].
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
         */
        function fetchRow() {
                return $this->db->fetchRow( $this->result );
        }
 
        /**
-        * @todo document
+        * Free a result object
         */
        function free() {
                $this->db->freeResult( $this->result );
@@ -2130,16 +2395,48 @@ class ResultWrapper {
                unset( $this->db );
        }
 
+       /**
+        * Change the position of the cursor in a result object
+        * See mysql_data_seek()
+        */
        function seek( $row ) {
                $this->db->dataSeek( $this->result, $row );
        }
-       
+
+       /*********************
+        * Iterator functions
+        * Note that using these in combination with the non-iterator functions
+        * above may cause rows to be skipped or repeated.
+        */
+
        function rewind() {
                if ($this->numRows()) {
                        $this->db->dataSeek($this->result, 0);
                }
+               $this->pos = 0;
+               $this->currentRow = null;
        }
 
+       function current() {
+               if ( is_null( $this->currentRow ) ) {
+                       $this->next();
+               }
+               return $this->currentRow;
+       }
+
+       function key() {
+               return $this->pos;
+       }
+
+       function next() {
+               $this->pos++;
+               $this->currentRow = $this->fetchObject();
+               return $this->currentRow;
+       }
+
+       function valid() {
+               return $this->current() !== false;
+       }
 }
 
-?>
+