<?php
/**
+ * @defgroup Database Database
+ *
+ * @file
+ * @ingroup Database
* This file deals with MySQL interface functions
* and query specifics/optimisations
*/
/** Maximum time to wait before retry */
define( 'DEADLOCK_DELAY_MAX', 1500000 );
-/******************************************************************************
- * Utility classes
- *****************************************************************************/
-
-class DBObject {
- public $mData;
-
- function DBObject($data) {
- $this->mData = $data;
- }
-
- function isLOB() {
- return false;
- }
-
- function data() {
- return $this->mData;
- }
-};
-
-/******************************************************************************
- * Error classes
- *****************************************************************************/
-
-/**
- * Database error base class
- */
-class DBError extends MWException {
- public $db;
-
- /**
- * Construct a database error
- * @param Database $db The database object which threw the error
- * @param string $error A simple error message to be used for debugging
- */
- function __construct( Database &$db, $error ) {
- $this->db =& $db;
- parent::__construct( $error );
- }
-}
-
-class DBConnectionError extends DBError {
- public $error;
-
- function __construct( Database &$db, $error = 'unknown error' ) {
- $msg = 'DB connection error';
- if ( trim( $error ) != '' ) {
- $msg .= ": $error";
- }
- $this->error = $error;
- parent::__construct( $db, $msg );
- }
-
- function useOutputPage() {
- // Not likely to work
- return false;
- }
-
- function useMessageCache() {
- // Not likely to work
- return false;
- }
-
- function getText() {
- return $this->getMessage() . "\n";
- }
-
- function getLogMessage() {
- # Don't send to the exception log
- return false;
- }
-
- function getPageTitle() {
- global $wgSitename;
- return "$wgSitename has a problem";
- }
-
- function getHTML() {
- global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
- global $wgSitename, $wgServer, $wgMessageCache;
-
- # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
- # Hard coding strings instead.
-
- $noconnect = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
- $mainpage = 'Main Page';
- $searchdisabled = <<<EOT
-<p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
-<span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
-EOT;
-
- $googlesearch = "
-<!-- SiteSearch Google -->
-<FORM method=GET action=\"http://www.google.com/search\">
-<TABLE bgcolor=\"#FFFFFF\"><tr><td>
-<A HREF=\"http://www.google.com/\">
-<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
-border=\"0\" ALT=\"Google\"></A>
-</td>
-<td>
-<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
-<INPUT type=submit name=btnG VALUE=\"Google Search\">
-<font size=-1>
-<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
-<input type='hidden' name='ie' value='$2'>
-<input type='hidden' name='oe' value='$2'>
-</font>
-</td></tr></TABLE>
-</FORM>
-<!-- SiteSearch Google -->";
- $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
-
- # No database access
- if ( is_object( $wgMessageCache ) ) {
- $wgMessageCache->disable();
- }
-
- if ( trim( $this->error ) == '' ) {
- $this->error = $this->db->getProperty('mServer');
- }
-
- $text = str_replace( '$1', $this->error, $noconnect );
- $text .= wfGetSiteNotice();
-
- if($wgUseFileCache) {
- if($wgTitle) {
- $t =& $wgTitle;
- } else {
- if($title) {
- $t = Title::newFromURL( $title );
- } elseif (@/**/$_REQUEST['search']) {
- $search = $_REQUEST['search'];
- return $searchdisabled .
- str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
- $wgInputEncoding ), $googlesearch );
- } else {
- $t = Title::newFromText( $mainpage );
- }
- }
-
- $cache = new HTMLFileCache( $t );
- if( $cache->isFileCached() ) {
- // FIXME: $msg is not defined on the next line.
- $msg = '<p style="color: red"><b>'.$msg."<br />\n" .
- $cachederror . "</b></p>\n";
-
- $tag = '<div id="article">';
- $text = str_replace(
- $tag,
- $tag . $msg,
- $cache->fetchPageText() );
- }
- }
-
- return $text;
- }
-}
-
-class DBQueryError extends DBError {
- public $error, $errno, $sql, $fname;
-
- function __construct( Database &$db, $error, $errno, $sql, $fname ) {
- $message = "A database error has occurred\n" .
- "Query: $sql\n" .
- "Function: $fname\n" .
- "Error: $errno $error\n";
-
- parent::__construct( $db, $message );
- $this->error = $error;
- $this->errno = $errno;
- $this->sql = $sql;
- $this->fname = $fname;
- }
-
- function getText() {
- if ( $this->useMessageCache() ) {
- return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
- htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
- } else {
- return $this->getMessage();
- }
- }
-
- function getSQL() {
- global $wgShowSQLErrors;
- if( !$wgShowSQLErrors ) {
- return $this->msg( 'sqlhidden', 'SQL hidden' );
- } else {
- return $this->sql;
- }
- }
-
- function getLogMessage() {
- # Don't send to the exception log
- return false;
- }
-
- function getPageTitle() {
- return $this->msg( 'databaseerror', 'Database error' );
- }
-
- function getHTML() {
- if ( $this->useMessageCache() ) {
- return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
- htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
- } else {
- return nl2br( htmlspecialchars( $this->getMessage() ) );
- }
- }
-}
-
-class DBUnexpectedError extends DBError {}
-
-/******************************************************************************/
-
/**
* Database abstraction object
+ * @ingroup Database
*/
class Database {
protected $mTrxLevel = 0;
protected $mErrorCount = 0;
protected $mLBInfo = array();
- protected $mCascadingDeletes = false;
- protected $mCleanupTriggers = false;
- protected $mStrictIPs = false;
+ protected $mFakeSlaveLag = null, $mFakeMaster = false;
#------------------------------------------------------------------------------
# Accessors
return wfSetVar( $this->mErrorCount, $count );
}
+ function tablePrefix( $prefix = null ) {
+ return wfSetVar( $this->mTablePrefix, $prefix );
+ }
+
/**
* Properties passed down from the server info array of the load balancer
*/
}
}
+ /**
+ * Set lag time in seconds for a fake slave
+ */
+ function setFakeSlaveLag( $lag ) {
+ $this->mFakeSlaveLag = $lag;
+ }
+
+ /**
+ * Make this connection a fake master
+ */
+ function setFakeMaster( $enabled = true ) {
+ $this->mFakeMaster = $enabled;
+ }
+
/**
* Returns true if this database supports (and uses) cascading deletes
*/
function cascadingDeletes() {
- return $this->mCascadingDeletes;
+ return false;
}
/**
* Returns true if this database supports (and uses) triggers (e.g. on the page table)
*/
function cleanupTriggers() {
- return $this->mCleanupTriggers;
+ return false;
}
/**
* Specifically, it uses a NULL value instead of an empty string.
*/
function strictIPs() {
- return $this->mStrictIPs;
+ return false;
}
/**
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';
return false;
}
+ /**
+ * Returns true if this database can use functional indexes
+ */
+ function functionalIndexes() {
+ return false;
+ }
+
/**#@+
* Get function
*/
return $this->$name;
}
+ function getWikiID() {
+ if( $this->mTablePrefix ) {
+ return "{$this->mDBname}-{$this->mTablePrefix}";
+ } else {
+ return $this->mDBname;
+ }
+ }
+
#------------------------------------------------------------------------------
# Other functions
#------------------------------------------------------------------------------
/**@{{
+ * Constructor.
* @param string $server database server host
* @param string $user database user name
* @param string $password database user password
* @param string $dbname database name
- */
-
- /**
* @param failFunction
* @param $flags
* @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
* @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 );
}
* If the failFunction is set to a non-zero integer, returns success
*/
function open( $server, $user, $password, $dbName ) {
- global $wguname;
+ global $wguname, $wgAllDBsAreLocalhost;
wfProfileIn( __METHOD__ );
# Test for missing mysql.so
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;
$success = false;
wfProfileIn("dbconnect-$server");
-
- # LIVE PATCH by Tim, ask Domas for why: retry loop
+
+ # Try to connect up to three times
+ # The kernel's default SYN retransmission period is far too slow for us,
+ # so we use a short timeout plus a manual retry.
$this->mConn = false;
$max = 3;
for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
usleep( 1000 );
}
if ( $this->mFlags & DBO_PERSISTENT ) {
- @/**/$this->mConn = mysql_pconnect( $server, $user, $password );
+ @/**/$this->mConn = mysql_pconnect( $realServer, $user, $password );
} else {
# Create a new connection...
- @/**/$this->mConn = mysql_connect( $server, $user, $password, true );
+ @/**/$this->mConn = mysql_connect( $realServer, $user, $password, true );
}
if ($this->mConn === false) {
- $iplus = $i + 1;
+ #$iplus = $i + 1;
#wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
}
}
}
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();
}
}
/**
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
- */
- function query( $sql, $fname = '', $tempIgnore = false ) {
- global $wgProfiling;
-
- if ( $wgProfiling ) {
+ * Usually aborts on failure. If errors are explicitly ignored, returns success.
+ *
+ * @param $sql String: SQL query
+ * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
+ * comment (you can use __METHOD__ or add some extra info)
+ * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors...
+ * maybe best to catch the exception instead?
+ * @return true for a successful write query, ResultWrapper object for a successful read query,
+ * or false on failure if $tempIgnore set
+ * @throws DBQueryError Thrown when the database returns an error of any kind
+ */
+ public function query( $sql, $fname = '', $tempIgnore = false ) {
+ global $wgProfiler;
+
+ $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+ if ( isset( $wgProfiler ) ) {
# generalizeSQL will probably cut down the query to reasonable
# logging size most of the time. The substr is really just a sanity check.
# Who's been wasting my precious column space? -- TS
#$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
- if ( is_null( $this->getLBInfo( 'master' ) ) ) {
- $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'Database::query';
- } else {
+ if ( $isMaster ) {
$queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
$totalProf = 'Database::query-master';
+ } else {
+ $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'Database::query';
}
wfProfileIn( $totalProf );
wfProfileIn( $queryProf );
$this->mLastQuery = $sql;
# Add a comment for easy SHOW PROCESSLIST interpretation
- if ( $fname ) {
- $commentedSql = preg_replace('/\s/', " /* $fname */ ", $sql, 1);
- } else {
- $commentedSql = $sql;
- }
+ #if ( $fname ) {
+ global $wgUser;
+ if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
+ $userName = $wgUser->getName();
+ if ( mb_strlen( $userName ) > 15 ) {
+ $userName = mb_substr( $userName, 0, 15 ) . '...';
+ }
+ $userName = str_replace( '/', '', $userName );
+ } else {
+ $userName = '';
+ }
+ $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
+ #} else {
+ # $commentedSql = $sql;
+ #}
# If DBO_TRX is set, start a transaction
if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
- $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK'
- ) {
- $this->begin();
+ $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
+ // avoid establishing transactions for SHOW and SET statements too -
+ // that would delay transaction initializations to once connection
+ // is really used by application
+ $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
+ if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
+ $this->begin();
}
if ( $this->debug() ) {
$sqlx = substr( $commentedSql, 0, 500 );
$sqlx = strtr( $sqlx, "\t\n", ' ' );
- wfDebug( "SQL: $sqlx\n" );
+ if ( $isMaster ) {
+ wfDebug( "SQL-master: $sqlx\n" );
+ } else {
+ wfDebug( "SQL: $sqlx\n" );
+ }
}
# Do the query and handle errors
$this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
}
- if ( $wgProfiling ) {
+ if ( isset( $wgProfiler ) ) {
wfProfileOut( $queryProf );
wfProfileOut( $totalProf );
}
- return $ret;
+ return $this->resultObject( $ret );
}
/**
* The DBMS-dependent part of query()
- * @param string $sql SQL query.
+ * @param $sql String: SQL query.
+ * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
+ * @access private
*/
- function doQuery( $sql ) {
+ /*private*/ function doQuery( $sql ) {
if( $this->bufferResults() ) {
$ret = mysql_query( $sql, $this->mConn );
} else {
* 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() ) );
}
/**
- * 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() ) );
* 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() ) );
* 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
* 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
}
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'])) {
if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
# Various MySQL extensions
if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
* @param string $fname Calling function name (use __METHOD__) for logs/profiling
* @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
* see Database::makeSelectOptions code for list of supported stuff
+ * @param array $join_conds Associative array of table join conditions (optional)
+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
* @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
*/
- function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() )
+ function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
{
+ $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * SELECT wrapper
+ *
+ * @param mixed $table Array or string, table name(s) (prefix auto-added)
+ * @param mixed $vars Array or string, field name(s) to be retrieved
+ * @param mixed $conds Array or string, condition(s) for WHERE
+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
+ * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * see Database::makeSelectOptions code for list of supported stuff
+ * @param array $join_conds Associative array of table join conditions (optional)
+ * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+ * @return string, the SQL text
+ */
+ function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
if( is_array( $vars ) ) {
$vars = implode( ',', $vars );
}
$options = array( $options );
}
if( is_array( $table ) ) {
- if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
- $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
+ if ( !empty($join_conds) || is_array( @$options['USE INDEX'] ) )
+ $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
else
$from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
} elseif ($table!='') {
$sql = $this->limitResult($sql, $options['LIMIT'],
isset($options['OFFSET']) ? $options['OFFSET'] : false);
$sql = "$sql $postLimitTail";
-
- return $this->query( $sql, $fname );
+
+ if (isset($options['EXPLAIN'])) {
+ $sql = 'EXPLAIN ' . $sql;
+ }
+ return $sql;
}
/**
*
* @todo migrate documentation to phpdocumentor format
*/
- function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array() ) {
+ function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
$options['LIMIT'] = 1;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
+ $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
if ( $res === false )
return false;
if ( !$this->numRows($res) ) {
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.
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;
* mysql_field_type() wrapper
*/
function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
return mysql_field_type( $res, $index );
}
* (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 );
if ( $conds != '*' ) {
$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
}
- $this->query( $sql, $fname );
+ return $this->query( $sql, $fname );
}
/**
$list .= "($value)";
} elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
$list .= "$value";
- } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array ($value) ) {
- $list .= $field." IN (".$this->makeList($value).") ";
+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
+ if( count( $value ) == 0 ) {
+ throw new MWException( __METHOD__.': empty input' );
+ } elseif( count( $value ) == 1 ) {
+ // Special-case single values, as IN isn't terribly efficient
+ // Don't necessarily assume the single key is 0; we don't
+ // enforce linear numeric ordering on other arrays here.
+ $value = array_values( $value );
+ $list .= $field." = ".$this->addQuotes( $value[0] );
+ } else {
+ $list .= $field." IN (".$this->makeList($value).") ";
+ }
+ } elseif( is_null($value) ) {
+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
+ $list .= "$field IS ";
+ } elseif ( $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= 'NULL';
} else {
if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
$list .= "$field = ";
return mysql_select_db( $db, $this->mConn );
}
+ /**
+ * Get the current DB name
+ */
+ function getDBname() {
+ return $this->mDBname;
+ }
+
+ /**
+ * Get the server hostname or IP address
+ */
+ function getServer() {
+ return $this->mServer;
+ }
+
/**
* Format a table name ready for use in constructing an SQL query
*
- * This does two important things: it quotes table names which as necessary,
- * and it adds a table prefix if there is one.
+ * This does two important things: it quotes the table names to clean them up,
+ * and it adds a table prefix if only given a table name with no quotes.
*
* All functions of this object which require a table name call this function
* themselves. Pass the canonical name to such functions. This is only needed
* when calling query() directly.
*
* @param string $name database table name
+ * @return string full database name
*/
function tableName( $name ) {
- global $wgSharedDB;
- # Skip quoted literals
- if ( $name{0} != '`' ) {
- if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) {
- $name = "{$this->mTablePrefix}$name";
- }
- if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
- $name = "`$wgSharedDB`.`$name`";
- } else {
- # Standard quoting
- $name = "`$name`";
- }
+ global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
+ # Skip the entire process when we have a string quoted on both ends.
+ # Note that we check the end so that we will still quote any use of
+ # use of `database`.table. But won't break things if someone wants
+ # to query a database table with a dot in the name.
+ if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
+
+ # Lets test for any bits of text that should never show up in a table
+ # name. Basically anything like JOIN or ON which are actually part of
+ # SQL queries, but may end up inside of the table value to combine
+ # sql. Such as how the API is doing.
+ # Note that we use a whitespace test rather than a \b test to avoid
+ # any remote case where a word like on may be inside of a table name
+ # surrounded by symbols which may be considered word breaks.
+ if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
+
+ # Split database and table into proper variables.
+ # We reverse the explode so that database.table and table both output
+ # the correct table.
+ @list( $table, $database ) = array_reverse( explode( '.', $name, 2 ) );
+ $prefix = $this->mTablePrefix; # Default prefix
+
+ # A database name has been specified in input. Quote the table name
+ # because we don't want any prefixes added.
+ if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
+
+ # Note that we use the long format because php will complain in in_array if
+ # the input is not an array, and will complain in is_array if it is not set.
+ if( !isset( $database ) # Don't use shared database if pre selected.
+ && isset( $wgSharedDB ) # We have a shared database
+ && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
+ && isset( $wgSharedTables )
+ && is_array( $wgSharedTables )
+ && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
+ $database = $wgSharedDB;
+ $prefix = isset( $wgSharedprefix ) ? $wgSharedprefix : $prefix;
}
- return $name;
+
+ # Quote the $database and $table and apply the prefix if not quoted.
+ if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
+ $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
+
+ # Merge our database and table into our final table name.
+ $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
+
+ # We're finished, return.
+ return $tableName;
}
/**
}
/**
- * @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";
*/
/**
* @private
*/
- function tableNamesWithUseIndex( $tables, $use_index ) {
+ function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
$ret = array();
-
- foreach ( $tables as $table )
- if ( @$use_index[$table] !== null )
- $ret[] = $this->tableName( $table ) . ' ' . $this->useIndexClause( implode( ',', (array)$use_index[$table] ) );
- else
- $ret[] = $this->tableName( $table );
-
- return implode( ',', $ret );
+ $retJOIN = array();
+ $use_index_safe = is_array($use_index) ? $use_index : array();
+ $join_conds_safe = is_array($join_conds) ? $join_conds : array();
+ foreach ( $tables as $table ) {
+ // Is there a JOIN and INDEX clause for this table?
+ if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
+ $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+ $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+ $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+ $retJOIN[] = $tableClause;
+ // Is there an INDEX clause?
+ } else if ( isset($use_index_safe[$table]) ) {
+ $tableClause = $this->tableName( $table );
+ $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+ $ret[] = $tableClause;
+ // Is there a JOIN clause?
+ } else if ( isset($join_conds_safe[$table]) ) {
+ $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+ $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+ $retJOIN[] = $tableClause;
+ } else {
+ $tableClause = $this->tableName( $table );
+ $ret[] = $tableClause;
+ }
+ }
+ // We can't separate explicit JOIN clauses with ',', use ' ' for those
+ $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
+ $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
+ // Compile our final table clause
+ return implode(' ',array($straightJoins,$otherJoins) );
}
/**
if( !is_numeric($limit) ) {
throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
}
- return " $sql LIMIT "
+ return "$sql LIMIT "
. ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
. "{$limit} ";
}
return " IF($cond, $trueVal, $falseVal) ";
}
+ /**
+ * Returns a comand for str_replace function in SQL query.
+ * Uses REPLACE() in MySQL
+ *
+ * @param string $orig String or column to modify
+ * @param string $old String or column to seek
+ * @param string $new String or column to replace with
+ */
+ function strreplace( $orig, $old, $new ) {
+ return "REPLACE({$orig}, {$old}, {$new})";
+ }
+
/**
* Determines if the last failure was due to a deadlock
*/
* @param string $pos the binlog position
* @param integer $timeout the maximum number of seconds to wait for synchronisation
*/
- function masterPosWait( $file, $pos, $timeout ) {
+ function masterPosWait( MySQLMasterPos $pos, $timeout ) {
$fname = 'Database::masterPosWait';
wfProfileIn( $fname );
-
# Commit any open transactions
- $this->immediateCommit();
+ if ( $this->mTrxLevel ) {
+ $this->immediateCommit();
+ }
+
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
+ if ( $wait > $timeout * 1e6 ) {
+ wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
+ wfProfileOut( $fname );
+ return -1;
+ } elseif ( $wait > 0 ) {
+ wfDebug( "Fake slave waiting $wait us\n" );
+ usleep( $wait );
+ wfProfileOut( $fname );
+ return 1;
+ } else {
+ wfDebug( "Fake slave up to date ($wait us)\n" );
+ wfProfileOut( $fname );
+ return 0;
+ }
+ }
# Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
- $encFile = $this->strencode( $file );
- $sql = "SELECT MASTER_POS_WAIT('$encFile', $pos, $timeout)";
+ $encFile = $this->addQuotes( $pos->file );
+ $encPos = intval( $pos->pos );
+ $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
$res = $this->doQuery( $sql );
if ( $res && $row = $this->fetchRow( $res ) ) {
$this->freeResult( $res );
* Get the position of the master from SHOW SLAVE STATUS
*/
function getSlavePos() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
+ wfDebug( __METHOD__.": fake slave pos = $pos\n" );
+ return $pos;
+ }
$res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
$row = $this->fetchObject( $res );
if ( $row ) {
- return array( $row->Master_Log_File, $row->Read_Master_Log_Pos );
+ return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
} else {
- return array( false, false );
+ return false;
}
}
* Get the position of the master from SHOW MASTER STATUS
*/
function getMasterPos() {
+ if ( $this->mFakeMaster ) {
+ return new MySQLMasterPos( 'fake', microtime( true ) );
+ }
$res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
$row = $this->fetchObject( $res );
if ( $row ) {
- return array( $row->File, $row->Position );
+ return new MySQLMasterPos( $row->File, $row->Position );
} else {
- return array( false, false );
+ return false;
}
}
}
/**
- * Rollback a transaction
+ * Rollback a transaction.
+ * No-op on non-transactional databases.
*/
function rollback( $fname = 'Database::rollback' ) {
- $this->query( 'ROLLBACK', $fname );
+ $this->query( 'ROLLBACK', $fname, true );
$this->mTrxLevel = 0;
}
*/
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 );
}
* Ping the server and try to reconnect if it there is no connection
*/
function ping() {
- if( function_exists( 'mysql_ping' ) ) {
- return mysql_ping( $this->mConn );
- } else {
+ if( !function_exists( 'mysql_ping' ) ) {
wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
return true;
}
+ $ping = mysql_ping( $this->mConn );
+ if ( $ping ) {
+ return true;
+ }
+
+ // Need to reconnect manually in MySQL client 5.0.13+
+ if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
+ mysql_close( $this->mConn );
+ $this->mOpened = false;
+ $this->mConn = false;
+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ return true;
+ }
+ return false;
}
/**
* At the moment, this will only work if the DB user has the PROCESS privilege
*/
function getLag() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
+ return $this->mFakeSlaveLag;
+ }
$res = $this->query( 'SHOW PROCESSLIST' );
- # Find slave SQL thread. Assumed to be the second one running, which is a bit
- # dubious, but unfortunately there's no easy rigorous way
+ # Find slave SQL thread
while ( $row = $this->fetchObject( $res ) ) {
/* This should work for most situations - when default db
* for thread is not specified, it had no events executed,
/**
* 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;
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();
$done = false;
}
}
- fclose( $fp );
return true;
}
+
/**
* Replace variables in sourced SQL
*/
$varnames = array(
'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
- 'wgDBadminuser', 'wgDBadminpassword',
+ 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
);
// Ordinary variables
return $this->tableName( $matches[1] );
}
+ /*
+ * Build a concatenation list to feed into a SQL query
+ */
+ function buildConcat( $stringList ) {
+ return 'CONCAT(' . implode( ',', $stringList ) . ')';
+ }
+
}
/**
* Database abstraction object for mySQL
* Inherit all methods and properties of Database::Database()
*
+ * @ingroup Database
* @see Database
*/
class DatabaseMysql extends Database {
# Inherit all
}
+/******************************************************************************
+ * Utility classes
+ *****************************************************************************/
/**
- * Result wrapper for grabbing data queried by someone else
+ * Utility class.
+ * @ingroup Database
+ */
+class DBObject {
+ public $mData;
+
+ function DBObject($data) {
+ $this->mData = $data;
+ }
+
+ function isLOB() {
+ return false;
+ }
+
+ function data() {
+ return $this->mData;
+ }
+}
+
+/**
+ * Utility class
+ * @ingroup Database
*
+ * This allows us to distinguish a blob from a normal string and an array of strings
*/
-class ResultWrapper {
- var $db, $result;
+class Blob {
+ private $mData;
+ function __construct($data) {
+ $this->mData = $data;
+ }
+ function fetch() {
+ return $this->mData;
+ }
+}
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class MySQLField {
+ private $name, $tablename, $default, $max_length, $nullable,
+ $is_pk, $is_unique, $is_multiple, $is_key, $type;
+ function __construct ($info) {
+ $this->name = $info->name;
+ $this->tablename = $info->table;
+ $this->default = $info->def;
+ $this->max_length = $info->max_length;
+ $this->nullable = !$info->not_null;
+ $this->is_pk = $info->primary_key;
+ $this->is_unique = $info->unique_key;
+ $this->is_multiple = $info->multiple_key;
+ $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
+ $this->type = $info->type;
+ }
+
+ function name() {
+ return $this->name;
+ }
+
+ function tableName() {
+ return $this->tableName;
+ }
+
+ function defaultValue() {
+ return $this->default;
+ }
+
+ function maxLength() {
+ return $this->max_length;
+ }
+
+ function nullable() {
+ return $this->nullable;
+ }
+
+ function isKey() {
+ return $this->is_key;
+ }
+
+ function isMultipleKey() {
+ return $this->is_multiple;
+ }
+
+ function type() {
+ return $this->type;
+ }
+}
+
+/******************************************************************************
+ * Error classes
+ *****************************************************************************/
+
+/**
+ * Database error base class
+ * @ingroup Database
+ */
+class DBError extends MWException {
+ public $db;
/**
- * @todo document
+ * Construct a database error
+ * @param Database $db The database object which threw the error
+ * @param string $error A simple error message to be used for debugging
+ */
+ function __construct( Database &$db, $error ) {
+ $this->db =& $db;
+ parent::__construct( $error );
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBConnectionError extends DBError {
+ public $error;
+
+ function __construct( Database &$db, $error = 'unknown error' ) {
+ $msg = 'DB connection error';
+ if ( trim( $error ) != '' ) {
+ $msg .= ": $error";
+ }
+ $this->error = $error;
+ parent::__construct( $db, $msg );
+ }
+
+ function useOutputPage() {
+ // Not likely to work
+ return false;
+ }
+
+ function useMessageCache() {
+ // Not likely to work
+ return false;
+ }
+
+ function getText() {
+ return $this->getMessage() . "\n";
+ }
+
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
+ }
+
+ function getPageTitle() {
+ global $wgSitename;
+ return "$wgSitename has a problem";
+ }
+
+ function getHTML() {
+ global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
+ global $wgSitename, $wgServer, $wgMessageCache;
+
+ # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
+ # Hard coding strings instead.
+
+ $noconnect = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
+ $mainpage = 'Main Page';
+ $searchdisabled = <<<EOT
+<p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
+<span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
+EOT;
+
+ $googlesearch = "
+<!-- SiteSearch Google -->
+<FORM method=GET action=\"http://www.google.com/search\">
+<TABLE bgcolor=\"#FFFFFF\"><tr><td>
+<A HREF=\"http://www.google.com/\">
+<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
+border=\"0\" ALT=\"Google\"></A>
+</td>
+<td>
+<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
+<INPUT type=submit name=btnG VALUE=\"Google Search\">
+<font size=-1>
+<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
+<input type='hidden' name='ie' value='$2'>
+<input type='hidden' name='oe' value='$2'>
+</font>
+</td></tr></TABLE>
+</FORM>
+<!-- SiteSearch Google -->";
+ $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
+
+ # No database access
+ if ( is_object( $wgMessageCache ) ) {
+ $wgMessageCache->disable();
+ }
+
+ if ( trim( $this->error ) == '' ) {
+ $this->error = $this->db->getProperty('mServer');
+ }
+
+ $text = str_replace( '$1', $this->error, $noconnect );
+ $text .= wfGetSiteNotice();
+
+ if($wgUseFileCache) {
+ if($wgTitle) {
+ $t =& $wgTitle;
+ } else {
+ if($title) {
+ $t = Title::newFromURL( $title );
+ } elseif (@/**/$_REQUEST['search']) {
+ $search = $_REQUEST['search'];
+ return $searchdisabled .
+ str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
+ $wgInputEncoding ), $googlesearch );
+ } else {
+ $t = Title::newFromText( $mainpage );
+ }
+ }
+
+ $cache = new HTMLFileCache( $t );
+ if( $cache->isFileCached() ) {
+ // @todo, FIXME: $msg is not defined on the next line.
+ $msg = '<p style="color: red"><b>'.$msg."<br />\n" .
+ $cachederror . "</b></p>\n";
+
+ $tag = '<div id="article">';
+ $text = str_replace(
+ $tag,
+ $tag . $msg,
+ $cache->fetchPageText() );
+ }
+ }
+
+ return $text;
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBQueryError extends DBError {
+ public $error, $errno, $sql, $fname;
+
+ function __construct( Database &$db, $error, $errno, $sql, $fname ) {
+ $message = "A database error has occurred\n" .
+ "Query: $sql\n" .
+ "Function: $fname\n" .
+ "Error: $errno $error\n";
+
+ parent::__construct( $db, $message );
+ $this->error = $error;
+ $this->errno = $errno;
+ $this->sql = $sql;
+ $this->fname = $fname;
+ }
+
+ function getText() {
+ if ( $this->useMessageCache() ) {
+ return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+ } else {
+ return $this->getMessage();
+ }
+ }
+
+ function getSQL() {
+ global $wgShowSQLErrors;
+ if( !$wgShowSQLErrors ) {
+ return $this->msg( 'sqlhidden', 'SQL hidden' );
+ } else {
+ return $this->sql;
+ }
+ }
+
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
+ }
+
+ function getPageTitle() {
+ return $this->msg( 'databaseerror', 'Database error' );
+ }
+
+ function getHTML() {
+ if ( $this->useMessageCache() ) {
+ return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
+ } else {
+ return nl2br( htmlspecialchars( $this->getMessage() ) );
+ }
+ }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBUnexpectedError extends DBError {}
+
+
+/**
+ * Result wrapper for grabbing data queried by someone else
+ * @ingroup Database
+ */
+class ResultWrapper implements Iterator {
+ var $db, $result, $pos = 0, $currentRow = null;
+
+ /**
+ * Create a new result object from a result resource and a Database object
*/
- function ResultWrapper( &$database, $result ) {
- $this->db =& $database;
- $this->result =& $result;
+ function ResultWrapper( $database, $result ) {
+ $this->db = $database;
+ if ( $result instanceof ResultWrapper ) {
+ $this->result = $result->result;
+ } else {
+ $this->result = $result;
+ }
}
/**
- * @todo document
+ * Get the number of rows in a result object
*/
function numRows() {
return $this->db->numRows( $this->result );
}
/**
- * @todo document
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchObject() {
return $this->db->fetchObject( $this->result );
}
/**
- * @todo document
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchRow() {
return $this->db->fetchRow( $this->result );
}
/**
- * @todo document
+ * Free a result object
*/
function free() {
$this->db->freeResult( $this->result );
unset( $this->db );
}
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
function seek( $row ) {
$this->db->dataSeek( $this->result, $row );
}
-
+
+ /*********************
+ * Iterator functions
+ * Note that using these in combination with the non-iterator functions
+ * above may cause rows to be skipped or repeated.
+ */
+
function rewind() {
if ($this->numRows()) {
$this->db->dataSeek($this->result, 0);
}
+ $this->pos = 0;
+ $this->currentRow = null;
}
+ function current() {
+ if ( is_null( $this->currentRow ) ) {
+ $this->next();
+ }
+ return $this->currentRow;
+ }
+
+ function key() {
+ return $this->pos;
+ }
+
+ function next() {
+ $this->pos++;
+ $this->currentRow = $this->fetchObject();
+ return $this->currentRow;
+ }
+
+ function valid() {
+ return $this->current() !== false;
+ }
}
-?>
+class MySQLMasterPos {
+ var $file, $pos;
+
+ function __construct( $file, $pos ) {
+ $this->file = $file;
+ $this->pos = $pos;
+ }
+
+ function __toString() {
+ return "{$this->file}/{$this->pos}";
+ }
+}