/**
* This file deals with MySQL interface functions
* and query specifics/optimisations
- * @package MediaWiki
*/
-/** See Database::makeList() */
-define( 'LIST_COMMA', 0 );
-define( 'LIST_AND', 1 );
-define( 'LIST_SET', 2 );
-define( 'LIST_NAMES', 3);
-define( 'LIST_OR', 4);
-
/** Number of times to re-try an operation in case of deadlock */
define( 'DEADLOCK_TRIES', 4 );
/** Minimum time to wait before retry, in microseconds */
* Utility classes
*****************************************************************************/
+/**
+ * Utility class.
+ */
class DBObject {
public $mData;
}
};
+/**
+ * Utility class.
+ */
+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
*****************************************************************************/
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, $wgOutputEncoding;
- global $wgSitename, $wgServer, $wgMessageCache, $wgLogo;
+ 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.
}
}
- $cache = new CacheManager( $t );
+ $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";
}
}
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
+ }
+
function getPageTitle() {
return $this->msg( 'databaseerror', 'Database error' );
}
/**
* Database abstraction object
- * @package MediaWiki
*/
class Database {
protected $mTrxLevel = 0;
protected $mErrorCount = 0;
protected $mLBInfo = array();
- protected $mCascadingDeletes = false;
- protected $mCleanupTriggers = false;
#------------------------------------------------------------------------------
# Accessors
* Turns on (false) or off (true) the automatic generation and sending
* of a "we're sorry, but there has been a database error" page on
* database errors. Default is on (false). When turned off, the
- * code should use wfLastErrno() and wfLastError() to handle the
+ * code should use lastErrno() and lastError() to handle the
* situation as appropriate.
*/
function ignoreErrors( $ignoreErrors = NULL ) {
* 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;
+ }
+
+ /**
+ * Returns true if this database is strict about what can be put into an IP field.
+ * Specifically, it uses a NULL value instead of an empty string.
+ */
+ function strictIPs() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database uses timestamps rather than integers
+ */
+ function realTimestamps() {
+ return false;
+ }
+
+ /**
+ * Returns true if this database does an implicit sort when doing GROUP BY
+ */
+ function implicitGroupby() {
+ 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';
+ */
+ function searchableIPs() {
+ return false;
}
/**#@+
# 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 );
}
*/
function open( $server, $user, $password, $dbName ) {
global $wguname;
+ wfProfileIn( __METHOD__ );
# Test for missing mysql.so
# First try to load it
$success = false;
- if ( $this->mFlags & DBO_PERSISTENT ) {
- @/**/$this->mConn = mysql_pconnect( $server, $user, $password );
- } else {
- # Create a new connection...
- @/**/$this->mConn = mysql_connect( $server, $user, $password, true );
+ wfProfileIn("dbconnect-$server");
+
+ # LIVE PATCH by Tim, ask Domas for why: retry loop
+ $this->mConn = false;
+ $max = 3;
+ for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
+ if ( $i > 1 ) {
+ usleep( 1000 );
+ }
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ @/**/$this->mConn = mysql_pconnect( $server, $user, $password );
+ } else {
+ # Create a new connection...
+ @/**/$this->mConn = mysql_connect( $server, $user, $password, true );
+ }
+ if ($this->mConn === false) {
+ $iplus = $i + 1;
+ #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
+ }
}
+
+ wfProfileOut("dbconnect-$server");
if ( $dbName != '' ) {
if ( $this->mConn !== false ) {
if ( !$success ) {
$error = "Error selecting database $dbName on server {$this->mServer} " .
"from client host {$wguname['nodename']}\n";
+ wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
wfDebug( $error );
}
} else {
$success = (bool)$this->mConn;
}
- if ( !$success ) {
+ if ( $success ) {
+ global $wgDBmysql5;
+ if( $wgDBmysql5 ) {
+ // Tell the server we're communicating with it in UTF-8.
+ // This may engage various charset conversions.
+ $this->query( 'SET NAMES utf8' );
+ }
+ } else {
$this->reportConnectionError();
}
- global $wgDBmysql5;
- if( $wgDBmysql5 ) {
- // Tell the server we're communicating with it in UTF-8.
- // This may engage various charset conversions.
- $this->query( 'SET NAMES utf8' );
- }
-
$this->mOpened = $success;
+ wfProfileOut( __METHOD__ );
return $success;
}
- /**@}}*/
/**
* Closes a database connection.
# Add a comment for easy SHOW PROCESSLIST interpretation
if ( $fname ) {
- $commentedSql = preg_replace("/\s/", " /* $fname */ ", $sql, 1);
+ $commentedSql = preg_replace('/\s/', " /* $fname */ ", $sql, 1);
} else {
$commentedSql = $sql;
}
wfDebug( "Connection lost, reconnecting...\n" );
if ( $this->ping() ) {
wfDebug( "Reconnected\n" );
+ $sqlx = substr( $commentedSql, 0, 500 );
+ $sqlx = strtr( $sqlx, "\t\n", ' ' );
+ global $wgRequestTime;
+ $elapsed = round( microtime(true) - $wgRequestTime, 3 );
+ wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
$ret = $this->doQuery( $commentedSql );
} else {
wfDebug( "Failed\n" );
* @param bool $tempIgnore
*/
function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- global $wgCommandLineMode, $wgFullyInitialised, $wgColorErrors;
+ global $wgCommandLineMode;
# Ignore errors during error handling to avoid infinite recursion
$ignore = $this->ignoreErrors( true );
++$this->mErrorCount;
case '\\!': return '!';
case '\\&': return '&';
}
- list( $n, $arg ) = each( $this->preparedArgs );
+ list( /* $n */ , $arg ) = each( $this->preparedArgs );
switch( $matches[1] ) {
case '?': return $this->addQuotes( $arg );
case '!': return $arg;
* @return array
*/
function makeSelectOptions( $options ) {
- $tailOpts = '';
+ $preLimitTail = $postLimitTail = '';
$startOpts = '';
$noKeyOptions = array();
}
}
- 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['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( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
- if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
+ //if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ //}
+
+ 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 */';
if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
$useIndex = '';
}
- return array( $startOpts, $useIndex, $tailOpts );
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
}
/**
* 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
+ * @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() )
{
$options = array( $options );
}
if( is_array( $table ) ) {
- if ( @is_array( $options['USE INDEX'] ) )
+ if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
$from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
else
$from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
} elseif ($table!='') {
- $from = ' FROM ' . $this->tableName( $table );
+ if ($table{0}==' ') {
+ $from = ' FROM ' . $table;
+ } else {
+ $from = ' FROM ' . $this->tableName( $table );
+ }
} else {
$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";
+
return $this->query( $sql, $fname );
}
$sql = preg_replace ('/".*"/s', "'X'", $sql);
# All newlines, tabs, etc replaced by single space
- $sql = preg_replace ( "/\s+/", ' ', $sql);
+ $sql = preg_replace ( '/\s+/', ' ', $sql);
# All numbers => N
$sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
return NULL;
}
+ $result = array();
while ( $row = $this->fetchObject( $res ) ) {
if ( $row->Key_name == $index ) {
- return $row;
+ $result[] = $row;
}
}
- return false;
+ $this->freeResult($res);
+
+ return empty($result) ? false : $result;
}
/**
for( $i = 0; $i < $n; $i++ ) {
$meta = mysql_fetch_field( $res, $i );
if( $field == $meta->name ) {
- return $meta;
+ return new MySQLField($meta);
}
}
return false;
if ( !$indexInfo ) {
return NULL;
}
- return !$indexInfo->Non_unique;
+ return !$indexInfo[0]->Non_unique;
}
/**
}
/**
- * Makes a wfStrencoded list from an array
+ * Makes an encoded list of strings from an array
* $mode:
* LIST_COMMA - comma separated, no field names
* LIST_AND - ANDed WHERE clause (without the WHERE)
}
if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
$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).") ";
} else {
* $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
* WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*/
- function tableNames() {
+ public function tableNames() {
$inArray = func_get_args();
$retVal = array();
foreach ( $inArray as $name ) {
}
return $retVal;
}
+
+ /**
+ * 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');
+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+ */
+ public function tableNamesN() {
+ $inArray = func_get_args();
+ $retVal = array();
+ foreach ( $inArray as $name ) {
+ $retVal[] = $this->tableName( $name );
+ }
+ return $retVal;
+ }
/**
* @private
$row = $this->fetchObject( $res );
$this->freeResult( $res );
- if ( preg_match( "/\((.*)\)/", $row->Type, $m ) ) {
+ $m = array();
+ if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
$size = $m[1];
} else {
$size = -1;
* @return string Version information from the database
*/
function getServerVersion() {
- return mysql_get_server_info();
+ return mysql_get_server_info( $this->mConn );
}
/**
$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
- $slaveThreads = 0;
while ( $row = $this->fetchObject( $res ) ) {
/* This should work for most situations - when default db
* for thread is not specified, it had no events executed,
return $b;
}
+ /**
+ * Override database's default connection timeout.
+ * May be useful for very long batch queries such as
+ * full-wiki dumps, where a single query reads out
+ * over hours or days.
+ * @param int $timeout in seconds
+ */
+ public function setTimeout( $timeout ) {
+ $this->query( "SET net_read_timeout=$timeout" );
+ $this->query( "SET net_write_timeout=$timeout" );
+ }
+
/**
* Read and execute SQL commands from a file.
* Returns true on success, error string on failure
function sourceFile( $filename ) {
$fp = fopen( $filename, 'r' );
if ( false === $fp ) {
- return "Could not open \"{$fname}\".\n";
+ return "Could not open \"{$filename}\".\n";
}
$cmd = "";
* Database abstraction object for mySQL
* Inherit all methods and properties of Database::Database()
*
- * @package MediaWiki
* @see Database
*/
class DatabaseMysql extends Database {
/**
* Result wrapper for grabbing data queried by someone else
*
- * @package MediaWiki
*/
class ResultWrapper {
var $db, $result;
function seek( $row ) {
$this->db->dataSeek( $this->result, $row );
}
+
+ function rewind() {
+ if ($this->numRows()) {
+ $this->db->dataSeek($this->result, 0);
+ }
+ }
}