<?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
- *****************************************************************************/
-
/**
- * Utility class.
- * @addtogroup Database
+ * Database abstraction object
+ * @ingroup Database
*/
-class DBObject {
- public $mData;
+class Database {
- function DBObject($data) {
- $this->mData = $data;
+#------------------------------------------------------------------------------
+# Variables
+#------------------------------------------------------------------------------
+
+ protected $mLastQuery = '';
+
+ protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
+ protected $mOut, $mOpened = false;
+
+ protected $mFailFunction;
+ protected $mTablePrefix;
+ protected $mFlags;
+ protected $mTrxLevel = 0;
+ protected $mErrorCount = 0;
+ protected $mLBInfo = array();
+ protected $mFakeSlaveLag = null, $mFakeMaster = false;
+
+#------------------------------------------------------------------------------
+# Accessors
+#------------------------------------------------------------------------------
+ # These optionally set a variable and return the previous state
+
+ /**
+ * Fail function, takes a Database as a parameter
+ * Set to false for default, 1 for ignore errors
+ */
+ function failFunction( $function = NULL ) {
+ return wfSetVar( $this->mFailFunction, $function );
}
- function isLOB() {
- return false;
+ /**
+ * Output page, used for reporting errors
+ * FALSE means discard output
+ */
+ function setOutputPage( $out ) {
+ $this->mOut = $out;
}
- function data() {
- return $this->mData;
+ /**
+ * Boolean, controls output of large amounts of debug information
+ */
+ function debug( $debug = NULL ) {
+ return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
}
-};
-/**
- * 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;
+ /**
+ * Turns buffering of SQL result sets on (true) or off (false).
+ * Default is "on" and it should not be changed without good reasons.
+ */
+ function bufferResults( $buffer = NULL ) {
+ if ( is_null( $buffer ) ) {
+ return !(bool)( $this->mFlags & DBO_NOBUFFER );
+ } else {
+ return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
+ }
}
- function fetch() {
- return $this->mData;
+
+ /**
+ * 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 lastErrno() and lastError() to handle the
+ * situation as appropriate.
+ */
+ function ignoreErrors( $ignoreErrors = NULL ) {
+ return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
}
-};
-/**
- * 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;
+ /**
+ * The current depth of nested transactions
+ * @param $level Integer: , default NULL.
+ */
+ function trxLevel( $level = NULL ) {
+ return wfSetVar( $this->mTrxLevel, $level );
}
- function name() {
- return $this->name;
+ /**
+ * Number of errors logged, only useful when errors are ignored
+ */
+ function errorCount( $count = NULL ) {
+ return wfSetVar( $this->mErrorCount, $count );
}
- function tableName() {
- return $this->tableName;
+ function tablePrefix( $prefix = null ) {
+ return wfSetVar( $this->mTablePrefix, $prefix );
}
- function defaultValue() {
- return $this->default;
+ /**
+ * Properties passed down from the server info array of the load balancer
+ */
+ function getLBInfo( $name = NULL ) {
+ if ( is_null( $name ) ) {
+ return $this->mLBInfo;
+ } else {
+ if ( array_key_exists( $name, $this->mLBInfo ) ) {
+ return $this->mLBInfo[$name];
+ } else {
+ return NULL;
+ }
+ }
}
- function maxLength() {
- return $this->max_length;
+ function setLBInfo( $name, $value = NULL ) {
+ if ( is_null( $value ) ) {
+ $this->mLBInfo = $name;
+ } else {
+ $this->mLBInfo[$name] = $value;
+ }
}
- function nullable() {
- return $this->nullable;
+ /**
+ * Set lag time in seconds for a fake slave
+ */
+ function setFakeSlaveLag( $lag ) {
+ $this->mFakeSlaveLag = $lag;
}
- function isKey() {
- return $this->is_key;
+ /**
+ * Make this connection a fake master
+ */
+ function setFakeMaster( $enabled = true ) {
+ $this->mFakeMaster = $enabled;
}
- function isMultipleKey() {
- return $this->is_multiple;
+ /**
+ * Returns true if this database supports (and uses) cascading deletes
+ */
+ function cascadingDeletes() {
+ return false;
}
- function type() {
- return $this->type;
+ /**
+ * Returns true if this database supports (and uses) triggers (e.g. on the page table)
+ */
+ function cleanupTriggers() {
+ return false;
}
-}
-/******************************************************************************
- * Error classes
- *****************************************************************************/
+ /**
+ * 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;
+ }
-/**
- * Database error base class
- * @addtogroup Database
- */
-class DBError extends MWException {
- public $db;
+ /**
+ * Returns true if this database uses timestamps rather than integers
+ */
+ function realTimestamps() {
+ return false;
+ }
/**
- * 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
+ * Returns true if this database does an implicit sort when doing GROUP BY
*/
- function __construct( Database &$db, $error ) {
- $this->db =& $db;
- parent::__construct( $error );
+ function implicitGroupby() {
+ return true;
}
-}
-/**
- * @addtogroup 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 );
+ /**
+ * 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;
}
- function useOutputPage() {
- // Not likely to work
+ /**
+ * 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;
}
- function useMessageCache() {
- // Not likely to work
+ /**
+ * Returns true if this database can use functional indexes
+ */
+ function functionalIndexes() {
return false;
}
-
- function getText() {
- return $this->getMessage() . "\n";
+
+ /**#@+
+ * Get function
+ */
+ function lastQuery() { return $this->mLastQuery; }
+ function isOpen() { return $this->mOpened; }
+ /**#@-*/
+
+ function setFlag( $flag ) {
+ $this->mFlags |= $flag;
}
- function getLogMessage() {
- # Don't send to the exception log
- return false;
+ function clearFlag( $flag ) {
+ $this->mFlags &= ~$flag;
}
- function getPageTitle() {
- global $wgSitename;
- return "$wgSitename has a problem";
+ function getFlag( $flag ) {
+ return !!($this->mFlags & $flag);
}
- function getHTML() {
- global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
- global $wgSitename, $wgServer, $wgMessageCache;
+ /**
+ * General read-only accessor
+ */
+ function getProperty( $name ) {
+ return $this->$name;
+ }
- # 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;
- }
-}
-
-/**
- * @addtogroup 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() ) );
- }
- }
-}
-
-/**
- * @addtogroup Database
- */
-class DBUnexpectedError extends DBError {}
-
-/******************************************************************************/
-
-/**
- * Database abstraction object
- * @addtogroup Database
- */
-class Database {
-
-#------------------------------------------------------------------------------
-# Variables
-#------------------------------------------------------------------------------
-
- protected $mLastQuery = '';
-
- protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
- protected $mOut, $mOpened = false;
-
- protected $mFailFunction;
- protected $mTablePrefix;
- protected $mFlags;
- protected $mTrxLevel = 0;
- protected $mErrorCount = 0;
- protected $mLBInfo = array();
-
-#------------------------------------------------------------------------------
-# Accessors
-#------------------------------------------------------------------------------
- # These optionally set a variable and return the previous state
-
- /**
- * Fail function, takes a Database as a parameter
- * Set to false for default, 1 for ignore errors
- */
- function failFunction( $function = NULL ) {
- return wfSetVar( $this->mFailFunction, $function );
- }
-
- /**
- * Output page, used for reporting errors
- * FALSE means discard output
- */
- function setOutputPage( $out ) {
- $this->mOut = $out;
- }
-
- /**
- * Boolean, controls output of large amounts of debug information
- */
- function debug( $debug = NULL ) {
- return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
- }
-
- /**
- * Turns buffering of SQL result sets on (true) or off (false).
- * Default is "on" and it should not be changed without good reasons.
- */
- function bufferResults( $buffer = NULL ) {
- if ( is_null( $buffer ) ) {
- return !(bool)( $this->mFlags & DBO_NOBUFFER );
+ function getWikiID() {
+ if( $this->mTablePrefix ) {
+ return "{$this->mDBname}-{$this->mTablePrefix}";
} else {
- return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
+ return $this->mDBname;
}
}
- /**
- * 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 lastErrno() and lastError() to handle the
- * situation as appropriate.
- */
- function ignoreErrors( $ignoreErrors = NULL ) {
- return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
- }
-
- /**
- * The current depth of nested transactions
- * @param $level Integer: , default NULL.
- */
- function trxLevel( $level = NULL ) {
- return wfSetVar( $this->mTrxLevel, $level );
- }
-
- /**
- * Number of errors logged, only useful when errors are ignored
- */
- function errorCount( $count = NULL ) {
- return wfSetVar( $this->mErrorCount, $count );
- }
-
- /**
- * Properties passed down from the server info array of the load balancer
- */
- function getLBInfo( $name = NULL ) {
- if ( is_null( $name ) ) {
- return $this->mLBInfo;
- } else {
- if ( array_key_exists( $name, $this->mLBInfo ) ) {
- return $this->mLBInfo[$name];
- } else {
- return NULL;
- }
- }
- }
-
- function setLBInfo( $name, $value = NULL ) {
- if ( is_null( $value ) ) {
- $this->mLBInfo = $name;
- } else {
- $this->mLBInfo[$name] = $value;
- }
- }
-
- /**
- * Returns true if this database supports (and uses) cascading deletes
- */
- function cascadingDeletes() {
- return false;
- }
-
- /**
- * Returns true if this database supports (and uses) triggers (e.g. on the page table)
- */
- function cleanupTriggers() {
- 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 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';
- */
- function searchableIPs() {
- return false;
- }
-
- /**
- * Returns true if this database can use functional indexes
- */
- function functionalIndexes() {
- return false;
- }
-
- /**#@+
- * Get function
- */
- function lastQuery() { return $this->mLastQuery; }
- function isOpen() { return $this->mOpened; }
- /**#@-*/
-
- function setFlag( $flag ) {
- $this->mFlags |= $flag;
- }
-
- function clearFlag( $flag ) {
- $this->mFlags &= ~$flag;
- }
-
- function getFlag( $flag ) {
- return !!($this->mFlags & $flag);
- }
-
- /**
- * General read-only accessor
- */
- function getProperty( $name ) {
- return $this->$name;
- }
-
#------------------------------------------------------------------------------
# Other functions
#------------------------------------------------------------------------------
* 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;
* @throws DBQueryError Thrown when the database returns an error of any kind
*/
public function query( $sql, $fname = '', $tempIgnore = false ) {
- global $wgProfiling;
+ global $wgProfiler;
- if ( $wgProfiling ) {
+ $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 );
global $wgUser;
if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
$userName = $wgUser->getName();
- if ( strlen( $userName ) > 15 ) {
- $userName = substr( $userName, 0, 15 ) . '...';
+ if ( mb_strlen( $userName ) > 15 ) {
+ $userName = mb_substr( $userName, 0, 15 ) . '...';
}
$userName = str_replace( '/', '', $userName );
} else {
# 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 Result object to feed to fetchObject, fetchRow, ...; or false on failure
* @access private
*/
- /*private*/ function doQuery( $sql ) {
- if( $this->bufferResults() ) {
- $ret = mysql_query( $sql, $this->mConn );
+ /*private*/ function doQuery( $sql ) {
+ if( $this->bufferResults() ) {
+ $ret = mysql_query( $sql, $this->mConn );
+ } else {
+ $ret = mysql_unbuffered_query( $sql, $this->mConn );
+ }
+ return $ret;
+ }
+
+ /**
+ * @param $error
+ * @param $errno
+ * @param $sql
+ * @param string $fname
+ * @param bool $tempIgnore
+ */
+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+ global $wgCommandLineMode;
+ # Ignore errors during error handling to avoid infinite recursion
+ $ignore = $this->ignoreErrors( true );
+ ++$this->mErrorCount;
+
+ if( $ignore || $tempIgnore ) {
+ wfDebug("SQL ERROR (ignored): $error\n");
+ $this->ignoreErrors( $ignore );
+ } else {
+ $sql1line = str_replace( "\n", "\\n", $sql );
+ wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
+ wfDebug("SQL ERROR: " . $error . "\n");
+ throw new DBQueryError( $this, $error, $errno, $sql, $fname );
+ }
+ }
+
+
+ /**
+ * Intended to be compatible with the PEAR::DB wrapper functions.
+ * http://pear.php.net/manual/en/package.database.db.intro-execute.php
+ *
+ * ? = scalar value, quoted as necessary
+ * ! = raw SQL bit (a function for instance)
+ * & = filename; reads the file and inserts as a blob
+ * (we don't use this though...)
+ */
+ function prepare( $sql, $func = 'Database::prepare' ) {
+ /* MySQL doesn't support prepared statements (yet), so just
+ pack up the query for reference. We'll manually replace
+ the bits later. */
+ return array( 'query' => $sql, 'func' => $func );
+ }
+
+ function freePrepared( $prepared ) {
+ /* No-op for MySQL */
+ }
+
+ /**
+ * Execute a prepared query with the various arguments
+ * @param string $prepared the prepared sql
+ * @param mixed $args Either an array here, or put scalars as varargs
+ */
+ function execute( $prepared, $args = null ) {
+ if( !is_array( $args ) ) {
+ # Pull the var args
+ $args = func_get_args();
+ array_shift( $args );
+ }
+ $sql = $this->fillPrepared( $prepared['query'], $args );
+ return $this->query( $sql, $prepared['func'] );
+ }
+
+ /**
+ * Prepare & execute an SQL statement, quoting and inserting arguments
+ * in the appropriate places.
+ * @param string $query
+ * @param string $args ...
+ */
+ function safeQuery( $query, $args = null ) {
+ $prepared = $this->prepare( $query, 'Database::safeQuery' );
+ if( !is_array( $args ) ) {
+ # Pull the var args
+ $args = func_get_args();
+ array_shift( $args );
+ }
+ $retval = $this->execute( $prepared, $args );
+ $this->freePrepared( $prepared );
+ return $retval;
+ }
+
+ /**
+ * For faking prepared SQL statements on DBs that don't support
+ * it directly.
+ * @param string $preparedSql - a 'preparable' SQL statement
+ * @param array $args - array of arguments to fill it with
+ * @return string executable SQL
+ */
+ function fillPrepared( $preparedQuery, $args ) {
+ reset( $args );
+ $this->preparedArgs =& $args;
+ return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
+ array( &$this, 'fillPreparedArg' ), $preparedQuery );
+ }
+
+ /**
+ * preg_callback func for fillPrepared()
+ * The arguments should be in $this->preparedArgs and must not be touched
+ * while we're doing this.
+ *
+ * @param array $matches
+ * @return string
+ * @private
+ */
+ function fillPreparedArg( $matches ) {
+ switch( $matches[1] ) {
+ case '\\?': return '?';
+ case '\\!': return '!';
+ case '\\&': return '&';
+ }
+ list( /* $n */ , $arg ) = each( $this->preparedArgs );
+ switch( $matches[1] ) {
+ case '?': return $this->addQuotes( $arg );
+ case '!': return $arg;
+ case '&':
+ # return $this->addQuotes( file_get_contents( $arg ) );
+ throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
+ default:
+ throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
+ }
+ }
+
+ /**#@+
+ * @param mixed $res A SQL result
+ */
+ /**
+ * 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.
+ * 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() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * 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() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * 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() ) );
+ }
+ return $n;
+ }
+
+ /**
+ * Get the number of fields in a result object
+ * See documentation for mysql_num_fields()
+ */
+ 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 ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_field_name( $res, $n );
+ }
+
+ /**
+ * Get the inserted value of an auto-increment row
+ *
+ * The value inserted should be fetched from nextSequenceValue()
+ *
+ * Example:
+ * $id = $dbw->nextSequenceValue('page_page_id_seq');
+ * $dbw->insert('page',array('page_id' => $id));
+ * $id = $dbw->insertId();
+ */
+ function insertId() { return mysql_insert_id( $this->mConn ); }
+
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_data_seek( $res, $row );
+ }
+
+ /**
+ * Get the last error number
+ * See mysql_errno()
+ */
+ function lastErrno() {
+ if ( $this->mConn ) {
+ return mysql_errno( $this->mConn );
} else {
- $ret = mysql_unbuffered_query( $sql, $this->mConn );
+ return mysql_errno();
}
- return $ret;
}
/**
- * @param $error
- * @param $errno
- * @param $sql
- * @param string $fname
- * @param bool $tempIgnore
+ * Get a description of the last error
+ * See mysql_error() for more details
*/
- function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
- global $wgCommandLineMode;
- # Ignore errors during error handling to avoid infinite recursion
- $ignore = $this->ignoreErrors( true );
- ++$this->mErrorCount;
-
- if( $ignore || $tempIgnore ) {
- wfDebug("SQL ERROR (ignored): $error\n");
- $this->ignoreErrors( $ignore );
+ function lastError() {
+ if ( $this->mConn ) {
+ # Even if it's non-zero, it can still be invalid
+ wfSuppressWarnings();
+ $error = mysql_error( $this->mConn );
+ if ( !$error ) {
+ $error = mysql_error();
+ }
+ wfRestoreWarnings();
} else {
- $sql1line = str_replace( "\n", "\\n", $sql );
- wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
- wfDebug("SQL ERROR: " . $error . "\n");
- throw new DBQueryError( $this, $error, $errno, $sql, $fname );
+ $error = mysql_error();
+ }
+ if( $error ) {
+ $error .= ' (' . $this->mServer . ')';
}
+ return $error;
}
-
+ /**
+ * Get the number of rows affected by the last write query
+ * See mysql_affected_rows() for more details
+ */
+ function affectedRows() { return mysql_affected_rows( $this->mConn ); }
+ /**#@-*/ // end of template : @param $result
/**
- * Intended to be compatible with the PEAR::DB wrapper functions.
- * http://pear.php.net/manual/en/package.database.db.intro-execute.php
+ * Simple UPDATE wrapper
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
*
- * ? = scalar value, quoted as necessary
- * ! = raw SQL bit (a function for instance)
- * & = filename; reads the file and inserts as a blob
- * (we don't use this though...)
+ * This function exists for historical reasons, Database::update() has a more standard
+ * calling convention and feature set
*/
- function prepare( $sql, $func = 'Database::prepare' ) {
- /* MySQL doesn't support prepared statements (yet), so just
- pack up the query for reference. We'll manually replace
- the bits later. */
- return array( 'query' => $sql, 'func' => $func );
- }
-
- function freePrepared( $prepared ) {
- /* No-op for MySQL */
+ function set( $table, $var, $value, $cond, $fname = 'Database::set' )
+ {
+ $table = $this->tableName( $table );
+ $sql = "UPDATE $table SET $var = '" .
+ $this->strencode( $value ) . "' WHERE ($cond)";
+ return (bool)$this->query( $sql, $fname );
}
/**
- * Execute a prepared query with the various arguments
- * @param string $prepared the prepared sql
- * @param mixed $args Either an array here, or put scalars as varargs
+ * Simple SELECT wrapper, returns a single field, input must be encoded
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns FALSE on failure
*/
- function execute( $prepared, $args = null ) {
- if( !is_array( $args ) ) {
- # Pull the var args
- $args = func_get_args();
- array_shift( $args );
+ function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ $options['LIMIT'] = 1;
+
+ $res = $this->select( $table, $var, $cond, $fname, $options );
+ if ( $res === false || !$this->numRows( $res ) ) {
+ return false;
+ }
+ $row = $this->fetchRow( $res );
+ if ( $row !== false ) {
+ $this->freeResult( $res );
+ return $row[0];
+ } else {
+ return false;
}
- $sql = $this->fillPrepared( $prepared['query'], $args );
- return $this->query( $sql, $prepared['func'] );
}
/**
- * Prepare & execute an SQL statement, quoting and inserting arguments
- * in the appropriate places.
- * @param string $query
- * @param string $args ...
+ * Returns an optional USE INDEX clause to go after the table, and a
+ * string to go at the end of the query
+ *
+ * @private
+ *
+ * @param array $options an associative array of options to be turned into
+ * an SQL query, valid keys are listed in the function.
+ * @return array
*/
- function safeQuery( $query, $args = null ) {
- $prepared = $this->prepare( $query, 'Database::safeQuery' );
- if( !is_array( $args ) ) {
- # Pull the var args
- $args = func_get_args();
- array_shift( $args );
+ function makeSelectOptions( $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
+
+ $noKeyOptions = array();
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
}
- $retval = $this->execute( $prepared, $args );
- $this->freePrepared( $prepared );
- return $retval;
+
+ 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( $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';
+ if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
+ if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
+ if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
+ if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
+
+ if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ } else {
+ $useIndex = '';
+ }
+
+ return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
}
/**
- * For faking prepared SQL statements on DBs that don't support
- * it directly.
- * @param string $preparedSql - a 'preparable' SQL statement
- * @param array $args - array of arguments to fill it with
- * @return string executable SQL
+ * 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 mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
*/
- function fillPrepared( $preparedQuery, $args ) {
- reset( $args );
- $this->preparedArgs =& $args;
- return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
- array( &$this, 'fillPreparedArg' ), $preparedQuery );
+ 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 );
}
-
+
/**
- * preg_callback func for fillPrepared()
- * The arguments should be in $this->preparedArgs and must not be touched
- * while we're doing this.
+ * SELECT wrapper
*
- * @param array $matches
- * @return string
- * @private
+ * @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 fillPreparedArg( $matches ) {
- switch( $matches[1] ) {
- case '\\?': return '?';
- case '\\!': return '!';
- case '\\&': return '&';
+ function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
+ if( is_array( $vars ) ) {
+ $vars = implode( ',', $vars );
}
- list( /* $n */ , $arg ) = each( $this->preparedArgs );
- switch( $matches[1] ) {
- case '?': return $this->addQuotes( $arg );
- case '!': return $arg;
- case '&':
- # return $this->addQuotes( file_get_contents( $arg ) );
- throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
- default:
- throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
+ if( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ if( is_array( $table ) ) {
+ 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!='') {
+ if ($table{0}==' ') {
+ $from = ' FROM ' . $table;
+ } else {
+ $from = ' FROM ' . $this->tableName( $table );
+ }
+ } else {
+ $from = '';
}
- }
- /**#@+
- * @param mixed $res A SQL result
- */
- /**
- * Free a result object
- */
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ 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 $preLimitTail";
+ } else {
+ $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
}
- if ( !@/**/mysql_free_result( $res ) ) {
- throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+
+ 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 $sql;
}
/**
- * 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.
+ * Single row SELECT wrapper
+ * Aborts or returns FALSE on error
*
- * @param $res SQL result object as returned from Database::query(), etc.
- * @return MySQL row object
- * @throws DBUnexpectedError Thrown if the database returns an error
+ * $vars: the selected variables
+ * $conds: a condition map, terms are ANDed together.
+ * Items with numeric keys are taken to be literal conditions
+ * Takes an array of selected variables, and a condition map, which is ANDed
+ * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
+ * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
+ * $obj- >page_id is the ID of the Astronomy article
+ *
+ * @todo migrate documentation to phpdocumentor format
*/
- 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() ) );
+ function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
+ $options['LIMIT'] = 1;
+ $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
+ if ( $res === false )
+ return false;
+ if ( !$this->numRows($res) ) {
+ $this->freeResult($res);
+ return false;
}
- return $row;
- }
+ $obj = $this->fetchObject( $res );
+ $this->freeResult( $res );
+ return $obj;
+ }
+
/**
- * 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
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * Takes same arguments as Database::select()
*/
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+
+ 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;
}
- @/**/$row = mysql_fetch_array( $res );
- if ( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+
+ $rows=1;
+
+ while( $plan = $this->fetchObject( $res ) ) {
+ $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
}
- return $row;
+
+ $this->freeResult($res);
+ return $rows;
}
+
/**
- * Get the number of rows in a result object
+ * Removes most variables from an SQL query and replaces them with X or N for numbers.
+ * It's only slightly flawed. Don't use for anything important.
+ *
+ * @param string $sql A SQL Query
+ * @static
*/
- function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- @/**/$n = mysql_num_rows( $res );
- if( $this->lastErrno() ) {
- throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $n;
+ static function generalizeSQL( $sql ) {
+ # This does the same as the regexp below would do, but in such a way
+ # as to avoid crashing php on some large strings.
+ # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
+
+ $sql = str_replace ( "\\\\", '', $sql);
+ $sql = str_replace ( "\\'", '', $sql);
+ $sql = str_replace ( "\\\"", '', $sql);
+ $sql = preg_replace ("/'.*'/s", "'X'", $sql);
+ $sql = preg_replace ('/".*"/s', "'X'", $sql);
+
+ # All newlines, tabs, etc replaced by single space
+ $sql = preg_replace ( '/\s+/', ' ', $sql);
+
+ # All numbers => N
+ $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
+
+ return $sql;
}
/**
- * Get the number of fields in a result object
- * See documentation for mysql_num_fields()
+ * Determines whether a field exists in a table
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
*/
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( 'DESCRIBE '.$table, $fname );
+ if ( !$res ) {
+ return NULL;
}
- return mysql_num_fields( $res );
+
+ $found = false;
+
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Field == $field ) {
+ $found = true;
+ break;
+ }
+ }
+ return $found;
}
/**
- * Get a field name in a result object
- * See documentation for mysql_field_name():
- * http://www.php.net/mysql_field_name
+ * Determines whether an index exists
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns NULL on failure
*/
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
+ $info = $this->indexInfo( $table, $index, $fname );
+ if ( is_null( $info ) ) {
+ return NULL;
+ } else {
+ return $info !== false;
}
- return mysql_field_name( $res, $n );
}
- /**
- * Get the inserted value of an auto-increment row
- *
- * The value inserted should be fetched from nextSequenceValue()
- *
- * Example:
- * $id = $dbw->nextSequenceValue('page_page_id_seq');
- * $dbw->insert('page',array('page_id' => $id));
- * $id = $dbw->insertId();
- */
- function insertId() { return mysql_insert_id( $this->mConn ); }
/**
- * Change the position of the cursor in a result object
- * See mysql_data_seek()
+ * Get information about an index into an object
+ * Returns false if the index does not exist
*/
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
+ # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
+ # SHOW INDEX should work for 3.x and up:
+ # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
+ $table = $this->tableName( $table );
+ $sql = 'SHOW INDEX FROM '.$table;
+ $res = $this->query( $sql, $fname );
+ if ( !$res ) {
+ return NULL;
}
- return mysql_data_seek( $res, $row );
+
+ $result = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ if ( $row->Key_name == $index ) {
+ $result[] = $row;
+ }
+ }
+ $this->freeResult($res);
+
+ return empty($result) ? false : $result;
}
/**
- * Get the last error number
- * See mysql_errno()
+ * Query whether a given table exists
*/
- function lastErrno() {
- if ( $this->mConn ) {
- return mysql_errno( $this->mConn );
+ function tableExists( $table ) {
+ $table = $this->tableName( $table );
+ $old = $this->ignoreErrors( true );
+ $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
+ $this->ignoreErrors( $old );
+ if( $res ) {
+ $this->freeResult( $res );
+ return true;
} else {
- return mysql_errno();
+ return false;
}
}
/**
- * Get a description of the last error
- * See mysql_error() for more details
+ * mysql_fetch_field() wrapper
+ * Returns false if the field doesn't exist
+ *
+ * @param $table
+ * @param $field
*/
- function lastError() {
- if ( $this->mConn ) {
- # Even if it's non-zero, it can still be invalid
- wfSuppressWarnings();
- $error = mysql_error( $this->mConn );
- if ( !$error ) {
- $error = mysql_error();
+ function fieldInfo( $table, $field ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT * FROM $table LIMIT 1" );
+ $n = mysql_num_fields( $res->result );
+ for( $i = 0; $i < $n; $i++ ) {
+ $meta = mysql_fetch_field( $res->result, $i );
+ if( $field == $meta->name ) {
+ return new MySQLField($meta);
}
- wfRestoreWarnings();
- } else {
- $error = mysql_error();
- }
- if( $error ) {
- $error .= ' (' . $this->mServer . ')';
}
- return $error;
+ return false;
}
- /**
- * Get the number of rows affected by the last write query
- * See mysql_affected_rows() for more details
- */
- function affectedRows() { return mysql_affected_rows( $this->mConn ); }
- /**#@-*/ // end of template : @param $result
/**
- * Simple UPDATE wrapper
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
- *
- * This function exists for historical reasons, Database::update() has a more standard
- * calling convention and feature set
+ * mysql_field_type() wrapper
*/
- function set( $table, $var, $value, $cond, $fname = 'Database::set' )
- {
- $table = $this->tableName( $table );
- $sql = "UPDATE $table SET $var = '" .
- $this->strencode( $value ) . "' WHERE ($cond)";
- return (bool)$this->query( $sql, $fname );
+ function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_field_type( $res, $index );
}
/**
- * Simple SELECT wrapper, returns a single field, input must be encoded
- * Usually aborts on failure
- * If errors are explicitly ignored, returns FALSE on failure
+ * Determines if a given index is unique
*/
- function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
- if ( !is_array( $options ) ) {
- $options = array( $options );
- }
- $options['LIMIT'] = 1;
-
- $res = $this->select( $table, $var, $cond, $fname, $options );
- if ( $res === false || !$this->numRows( $res ) ) {
- return false;
- }
- $row = $this->fetchRow( $res );
- if ( $row !== false ) {
- $this->freeResult( $res );
- return $row[0];
- } else {
- return false;
+ function indexUnique( $table, $index ) {
+ $indexInfo = $this->indexInfo( $table, $index );
+ if ( !$indexInfo ) {
+ return NULL;
}
+ return !$indexInfo[0]->Non_unique;
}
/**
- * Returns an optional USE INDEX clause to go after the table, and a
- * string to go at the end of the query
+ * INSERT wrapper, inserts an array into a table
*
- * @private
+ * $a may be a single associative array, or an array of these with numeric keys, for
+ * multi-row insert.
*
- * @param array $options an associative array of options to be turned into
- * an SQL query, valid keys are listed in the function.
- * @return array
+ * Usually aborts on failure
+ * If errors are explicitly ignored, returns success
*/
- function makeSelectOptions( $options ) {
- $preLimitTail = $postLimitTail = '';
- $startOpts = '';
-
- $noKeyOptions = array();
- foreach ( $options as $key => $option ) {
- if ( is_numeric( $key ) ) {
- $noKeyOptions[$option] = true;
- }
+ function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+ # No rows to insert, easy just return now
+ if ( !count( $a ) ) {
+ return true;
}
- 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( $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';
+ $table = $this->tableName( $table );
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $a[0] );
+ } else {
+ $multi = false;
+ $keys = array_keys( $a );
+ }
- # 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';
- if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
- if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
- if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
- if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
+ $sql = 'INSERT ' . implode( ' ', $options ) .
+ " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
- if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
- $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ if ( $multi ) {
+ $first = true;
+ foreach ( $a as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
+ }
} else {
- $useIndex = '';
+ $sql .= '(' . $this->makeList( $a ) . ')';
}
-
- return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+ return (bool)$this->query( $sql, $fname );
}
/**
- * SELECT wrapper
+ * Make UPDATE options for the Database::update function
*
- * @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
+ * @private
+ * @param array $options The options passed to Database::update
+ * @return string
*/
- function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() )
- {
- if( is_array( $vars ) ) {
- $vars = implode( ',', $vars );
- }
+ function makeUpdateOptions( $options ) {
if( !is_array( $options ) ) {
$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'] );
- else
- $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
- } elseif ($table!='') {
- if ($table{0}==' ') {
- $from = ' FROM ' . $table;
- } else {
- $from = ' FROM ' . $this->tableName( $table );
- }
- } else {
- $from = '';
- }
+ $opts = array();
+ if ( in_array( 'LOW_PRIORITY', $options ) )
+ $opts[] = $this->lowPriorityOption();
+ if ( in_array( 'IGNORE', $options ) )
+ $opts[] = 'IGNORE';
+ return implode(' ', $opts);
+ }
- list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+ /**
+ * UPDATE wrapper, takes a condition array and a SET array
+ *
+ * @param string $table The table to UPDATE
+ * @param array $values An array of values to SET
+ * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
+ * @param string $fname The Class::Function calling this function
+ * (for the log)
+ * @param array $options An array of UPDATE options, can be one or
+ * more of IGNORE, LOW_PRIORITY
+ * @return bool
+ */
+ function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
+ $table = $this->tableName( $table );
+ $opts = $this->makeUpdateOptions( $options );
+ $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
+ if ( $conds != '*' ) {
+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
- if( !empty( $conds ) ) {
- if ( is_array( $conds ) ) {
- $conds = $this->makeList( $conds, LIST_AND );
- }
- $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
- } else {
- $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
+ /**
+ * 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)
+ * LIST_OR - ORed WHERE clause (without the WHERE)
+ * LIST_SET - comma separated with field names, like a SET clause
+ * LIST_NAMES - comma separated field names
+ */
+ function makeList( $a, $mode = LIST_COMMA ) {
+ if ( !is_array( $a ) ) {
+ throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
}
- 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;
+ $first = true;
+ $list = '';
+ foreach ( $a as $field => $value ) {
+ if ( !$first ) {
+ if ( $mode == LIST_AND ) {
+ $list .= ' AND ';
+ } elseif($mode == LIST_OR) {
+ $list .= ' OR ';
+ } else {
+ $list .= ',';
+ }
+ } else {
+ $first = false;
+ }
+ 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) ) {
+ 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 = ";
+ }
+ $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
+ }
}
- return $this->query( $sql, $fname );
+ return $list;
}
/**
- * Single row SELECT wrapper
- * Aborts or returns FALSE on error
- *
- * $vars: the selected variables
- * $conds: a condition map, terms are ANDed together.
- * Items with numeric keys are taken to be literal conditions
- * Takes an array of selected variables, and a condition map, which is ANDed
- * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
- * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
- * $obj- >page_id is the ID of the Astronomy article
- *
- * @todo migrate documentation to phpdocumentor format
+ * Change the current database
*/
- function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array() ) {
- $options['LIMIT'] = 1;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
- if ( $res === false )
- return false;
- if ( !$this->numRows($res) ) {
- $this->freeResult($res);
- return false;
- }
- $obj = $this->fetchObject( $res );
- $this->freeResult( $res );
- return $obj;
+ function selectDB( $db ) {
+ $this->mDBname = $db;
+ return mysql_select_db( $db, $this->mConn );
+ }
+ /**
+ * Get the current DB name
+ */
+ function getDBname() {
+ return $this->mDBname;
}
-
+
/**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * Takes same arguments as Database::select()
+ * Get the server hostname or IP address
*/
-
- 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;
- }
+ function getServer() {
+ return $this->mServer;
+ }
+
+ /**
+ * Format a table name ready for use in constructing an SQL query
+ *
+ * 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, $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;
- $rows=1;
-
- while( $plan = $this->fetchObject( $res ) ) {
- $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
+ # 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;
}
- $this->freeResult($res);
- return $rows;
+ # 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;
}
-
/**
- * Removes most variables from an SQL query and replaces them with X or N for numbers.
- * It's only slightly flawed. Don't use for anything important.
+ * Fetch a number of table names into an array
+ * This is handy when you need to construct SQL for joins
*
- * @param string $sql A SQL Query
- * @static
+ * Example:
+ * extract($dbr->tableNames('user','watchlist'));
+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*/
- static function generalizeSQL( $sql ) {
- # This does the same as the regexp below would do, but in such a way
- # as to avoid crashing php on some large strings.
- # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
-
- $sql = str_replace ( "\\\\", '', $sql);
- $sql = str_replace ( "\\'", '', $sql);
- $sql = str_replace ( "\\\"", '', $sql);
- $sql = preg_replace ("/'.*'/s", "'X'", $sql);
- $sql = preg_replace ('/".*"/s', "'X'", $sql);
-
- # All newlines, tabs, etc replaced by single space
- $sql = preg_replace ( '/\s+/', ' ', $sql);
-
- # All numbers => N
- $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
-
- return $sql;
+ public function tableNames() {
+ $inArray = func_get_args();
+ $retVal = array();
+ foreach ( $inArray as $name ) {
+ $retVal[$name] = $this->tableName( $name );
+ }
+ return $retVal;
}
-
+
/**
- * Determines whether a field exists in a table
- * Usually aborts on failure
- * If errors are explicitly ignored, returns NULL on failure
+ * 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->tableNamesN('user','watchlist');
+ * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+ * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*/
- function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
- $table = $this->tableName( $table );
- $res = $this->query( 'DESCRIBE '.$table, $fname );
- if ( !$res ) {
- return NULL;
+ public function tableNamesN() {
+ $inArray = func_get_args();
+ $retVal = array();
+ foreach ( $inArray as $name ) {
+ $retVal[] = $this->tableName( $name );
}
+ return $retVal;
+ }
- $found = false;
-
- while ( $row = $this->fetchObject( $res ) ) {
- if ( $row->Field == $field ) {
- $found = true;
- break;
+ /**
+ * @private
+ */
+ function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
+ $ret = array();
+ $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;
}
}
- return $found;
+ // 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) );
}
/**
- * Determines whether an index exists
- * Usually aborts on failure
- * If errors are explicitly ignored, returns NULL on failure
+ * Wrapper for addslashes()
+ * @param string $s String to be slashed.
+ * @return string slashed string.
*/
- function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
- $info = $this->indexInfo( $table, $index, $fname );
- if ( is_null( $info ) ) {
- return NULL;
+ function strencode( $s ) {
+ return mysql_real_escape_string( $s, $this->mConn );
+ }
+
+ /**
+ * If it's a string, adds quotes and backslashes
+ * Otherwise returns as-is
+ */
+ function addQuotes( $s ) {
+ if ( is_null( $s ) ) {
+ return 'NULL';
} else {
- return $info !== false;
+ # This will also quote numeric values. This should be harmless,
+ # and protects against weird problems that occur when they really
+ # _are_ strings such as article titles and string->number->string
+ # conversion is not 1:1.
+ return "'" . $this->strencode( $s ) . "'";
}
}
+ /**
+ * Escape string for safe LIKE usage
+ */
+ function escapeLike( $s ) {
+ $s=$this->strencode( $s );
+ $s=str_replace(array('%','_'),array('\%','\_'),$s);
+ return $s;
+ }
/**
- * Get information about an index into an object
- * Returns false if the index does not exist
+ * Returns an appropriately quoted sequence value for inserting a new row.
+ * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
+ * subclass will return an integer, and save the value for insertId()
*/
- function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
- # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
- # SHOW INDEX should work for 3.x and up:
- # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
- $table = $this->tableName( $table );
- $sql = 'SHOW INDEX FROM '.$table;
- $res = $this->query( $sql, $fname );
- if ( !$res ) {
- return NULL;
- }
+ function nextSequenceValue( $seqName ) {
+ return NULL;
+ }
- $result = array();
- while ( $row = $this->fetchObject( $res ) ) {
- if ( $row->Key_name == $index ) {
- $result[] = $row;
- }
- }
- $this->freeResult($res);
-
- return empty($result) ? false : $result;
+ /**
+ * USE INDEX clause
+ * PostgreSQL doesn't have them and returns ""
+ */
+ function useIndexClause( $index ) {
+ return "FORCE INDEX ($index)";
}
/**
- * Query whether a given table exists
+ * REPLACE query wrapper
+ * PostgreSQL simulates this with a DELETE followed by INSERT
+ * $row is the row to insert, an associative array
+ * $uniqueIndexes is an array of indexes. Each element may be either a
+ * field name or an array of field names
+ *
+ * It may be more efficient to leave off unique indexes which are unlikely to collide.
+ * However if you do this, you run the risk of encountering errors which wouldn't have
+ * occurred in MySQL
+ *
+ * @todo migrate comment to phodocumentor format
*/
- function tableExists( $table ) {
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
$table = $this->tableName( $table );
- $old = $this->ignoreErrors( true );
- $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
- $this->ignoreErrors( $old );
- if( $res ) {
- $this->freeResult( $res );
- return true;
- } else {
- return false;
+
+ # Single row case
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
+ }
+
+ $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
+ $first = true;
+ foreach ( $rows as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ',';
+ }
+ $sql .= '(' . $this->makeList( $row ) . ')';
}
+ return $this->query( $sql, $fname );
}
/**
- * mysql_fetch_field() wrapper
- * Returns false if the field doesn't exist
+ * DELETE where the condition is a join
+ * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
*
- * @param $table
- * @param $field
+ * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
+ * join condition matches, set $conds='*'
+ *
+ * DO NOT put the join condition in $conds
+ *
+ * @param string $delTable The table to delete from.
+ * @param string $joinTable The other table.
+ * @param string $delVar The variable to join on, in the first table.
+ * @param string $joinVar The variable to join on, in the second table.
+ * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
*/
- function fieldInfo( $table, $field ) {
- $table = $this->tableName( $table );
- $res = $this->query( "SELECT * FROM $table LIMIT 1" );
- $n = mysql_num_fields( $res->result );
- for( $i = 0; $i < $n; $i++ ) {
- $meta = mysql_fetch_field( $res->result, $i );
- if( $field == $meta->name ) {
- return new MySQLField($meta);
- }
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
}
- return false;
+
+ $delTable = $this->tableName( $delTable );
+ $joinTable = $this->tableName( $joinTable );
+ $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+ if ( $conds != '*' ) {
+ $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+ }
+
+ return $this->query( $sql, $fname );
}
/**
- * mysql_field_type() wrapper
+ * Returns the size of a text field, or -1 for "unlimited"
*/
- function fieldType( $res, $index ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
+ $res = $this->query( $sql, 'Database::textFieldSize' );
+ $row = $this->fetchObject( $res );
+ $this->freeResult( $res );
+
+ $m = array();
+ if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
+ $size = $m[1];
+ } else {
+ $size = -1;
}
- return mysql_field_type( $res, $index );
+ return $size;
}
/**
- * Determines if a given index is unique
+ * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
*/
- function indexUnique( $table, $index ) {
- $indexInfo = $this->indexInfo( $table, $index );
- if ( !$indexInfo ) {
- return NULL;
- }
- return !$indexInfo[0]->Non_unique;
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
}
/**
- * INSERT wrapper, inserts an array into a table
- *
- * $a may be a single associative array, or an array of these with numeric keys, for
- * multi-row insert.
+ * DELETE query wrapper
*
- * Usually aborts on failure
- * If errors are explicitly ignored, returns success
+ * Use $conds == "*" to delete all rows
*/
- function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
- # No rows to insert, easy just return now
- if ( !count( $a ) ) {
- return true;
+ function delete( $table, $conds, $fname = 'Database::delete' ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
}
-
$table = $this->tableName( $table );
- if ( !is_array( $options ) ) {
- $options = array( $options );
+ $sql = "DELETE FROM $table";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
}
- if ( isset( $a[0] ) && is_array( $a[0] ) ) {
- $multi = true;
- $keys = array_keys( $a[0] );
+ return $this->query( $sql, $fname );
+ }
+
+ /**
+ * INSERT SELECT wrapper
+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+ * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
+ * $conds may be "*" to copy the whole table
+ * srcTable may be an array of tables.
+ */
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
+ $insertOptions = array(), $selectOptions = array() )
+ {
+ $destTable = $this->tableName( $destTable );
+ if ( is_array( $insertOptions ) ) {
+ $insertOptions = implode( ' ', $insertOptions );
+ }
+ if( !is_array( $selectOptions ) ) {
+ $selectOptions = array( $selectOptions );
+ }
+ list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+ if( is_array( $srcTable ) ) {
+ $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
} else {
- $multi = false;
- $keys = array_keys( $a );
+ $srcTable = $this->tableName( $srcTable );
}
+ $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ " SELECT $startOpts " . implode( ',', $varMap ) .
+ " FROM $srcTable $useIndex ";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= " $tailOpts";
+ return $this->query( $sql, $fname );
+ }
- $sql = 'INSERT ' . implode( ' ', $options ) .
- " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
- if ( $multi ) {
- $first = true;
- foreach ( $a as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
- }
- } else {
- $sql .= '(' . $this->makeList( $a ) . ')';
+ /**
+ * Construct a LIMIT query with optional offset
+ * This is used for query pages
+ * $sql string SQL query we will append the limit too
+ * $limit integer the SQL limit
+ * $offset integer the SQL offset (default false)
+ */
+ function limitResult($sql, $limit, $offset=false) {
+ if( !is_numeric($limit) ) {
+ throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
}
- return (bool)$this->query( $sql, $fname );
+ return "$sql LIMIT "
+ . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
+ . "{$limit} ";
+ }
+ function limitResultForUpdate($sql, $num) {
+ return $this->limitResult($sql, $num, 0);
}
/**
- * Make UPDATE options for the Database::update function
+ * Returns an SQL expression for a simple conditional.
+ * Uses IF on MySQL.
*
- * @private
- * @param array $options The options passed to Database::update
- * @return string
+ * @param string $cond SQL expression which will result in a boolean value
+ * @param string $trueVal SQL expression to return if true
+ * @param string $falseVal SQL expression to return if false
+ * @return string SQL fragment
*/
- function makeUpdateOptions( $options ) {
- if( !is_array( $options ) ) {
- $options = array( $options );
- }
- $opts = array();
- if ( in_array( 'LOW_PRIORITY', $options ) )
- $opts[] = $this->lowPriorityOption();
- if ( in_array( 'IGNORE', $options ) )
- $opts[] = 'IGNORE';
- return implode(' ', $opts);
+ function conditional( $cond, $trueVal, $falseVal ) {
+ return " IF($cond, $trueVal, $falseVal) ";
}
/**
- * UPDATE wrapper, takes a condition array and a SET array
+ * Returns a comand for str_replace function in SQL query.
+ * Uses REPLACE() in MySQL
*
- * @param string $table The table to UPDATE
- * @param array $values An array of values to SET
- * @param array $conds An array of conditions (WHERE). Use '*' to update all rows.
- * @param string $fname The Class::Function calling this function
- * (for the log)
- * @param array $options An array of UPDATE options, can be one or
- * more of IGNORE, LOW_PRIORITY
- * @return bool
+ * @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 update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
- $table = $this->tableName( $table );
- $opts = $this->makeUpdateOptions( $options );
- $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
- if ( $conds != '*' ) {
- $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
- }
- return $this->query( $sql, $fname );
+ function strreplace( $orig, $old, $new ) {
+ return "REPLACE({$orig}, {$old}, {$new})";
}
/**
- * 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)
- * LIST_OR - ORed WHERE clause (without the WHERE)
- * LIST_SET - comma separated with field names, like a SET clause
- * LIST_NAMES - comma separated field names
+ * Determines if the last failure was due to a deadlock
*/
- function makeList( $a, $mode = LIST_COMMA ) {
- if ( !is_array( $a ) ) {
- throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
+ function wasDeadlock() {
+ return $this->lastErrno() == 1213;
+ }
+
+ /**
+ * Perform a deadlock-prone transaction.
+ *
+ * This function invokes a callback function to perform a set of write
+ * queries. If a deadlock occurs during the processing, the transaction
+ * will be rolled back and the callback function will be called again.
+ *
+ * Usage:
+ * $dbw->deadlockLoop( callback, ... );
+ *
+ * Extra arguments are passed through to the specified callback function.
+ *
+ * Returns whatever the callback function returned on its successful,
+ * iteration, or false on error, for example if the retry limit was
+ * reached.
+ */
+ function deadlockLoop() {
+ $myFname = 'Database::deadlockLoop';
+
+ $this->begin();
+ $args = func_get_args();
+ $function = array_shift( $args );
+ $oldIgnore = $this->ignoreErrors( true );
+ $tries = DEADLOCK_TRIES;
+ if ( is_array( $function ) ) {
+ $fname = $function[0];
+ } else {
+ $fname = $function;
}
+ do {
+ $retVal = call_user_func_array( $function, $args );
+ $error = $this->lastError();
+ $errno = $this->lastErrno();
+ $sql = $this->lastQuery();
- $first = true;
- $list = '';
- foreach ( $a as $field => $value ) {
- if ( !$first ) {
- if ( $mode == LIST_AND ) {
- $list .= ' AND ';
- } elseif($mode == LIST_OR) {
- $list .= ' OR ';
- } else {
- $list .= ',';
- }
- } else {
- $first = false;
- }
- 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) ) {
- 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] );
+ if ( $errno ) {
+ if ( $this->wasDeadlock() ) {
+ # Retry
+ usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
} 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 = ";
+ $this->reportQueryError( $error, $errno, $sql, $fname );
}
- $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
}
+ } while( $this->wasDeadlock() && --$tries > 0 );
+ $this->ignoreErrors( $oldIgnore );
+ if ( $tries <= 0 ) {
+ $this->query( 'ROLLBACK', $myFname );
+ $this->reportQueryError( $error, $errno, $sql, $fname );
+ return false;
+ } else {
+ $this->query( 'COMMIT', $myFname );
+ return $retVal;
}
- return $list;
- }
-
- /**
- * Change the current database
- */
- function selectDB( $db ) {
- $this->mDBname = $db;
- return mysql_select_db( $db, $this->mConn );
}
/**
- * 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.
- *
- * 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.
+ * Do a SELECT MASTER_POS_WAIT()
*
- * @param string $name database table name
+ * @param string $file the binlog file
+ * @param string $pos the binlog position
+ * @param integer $timeout the maximum number of seconds to wait for synchronisation
*/
- 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`";
+ function masterPosWait( MySQLMasterPos $pos, $timeout ) {
+ $fname = 'Database::masterPosWait';
+ wfProfileIn( $fname );
+
+ # Commit any open transactions
+ 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 {
- # Standard quoting
- $name = "`$name`";
+ wfDebug( "Fake slave up to date ($wait us)\n" );
+ wfProfileOut( $fname );
+ return 0;
}
}
- return $name;
+
+ # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
+ $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 );
+ wfProfileOut( $fname );
+ return $row[0];
+ } else {
+ wfProfileOut( $fname );
+ return false;
+ }
}
/**
- * Fetch a number of table names into an array
- * This is handy when you need to construct SQL for joins
- *
- * Example:
- * extract($dbr->tableNames('user','watchlist'));
- * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
- * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+ * Get the position of the master from SHOW SLAVE STATUS
*/
- public function tableNames() {
- $inArray = func_get_args();
- $retVal = array();
- foreach ( $inArray as $name ) {
- $retVal[$name] = $this->tableName( $name );
+ 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 new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
+ } else {
+ return false;
}
- 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->tableNamesN('user','watchlist');
- * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
- * WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+ * Get the position of the master from SHOW MASTER STATUS
*/
- public function tableNamesN() {
- $inArray = func_get_args();
- $retVal = array();
- foreach ( $inArray as $name ) {
- $retVal[] = $this->tableName( $name );
+ 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 new MySQLMasterPos( $row->File, $row->Position );
+ } else {
+ return false;
}
- return $retVal;
}
/**
- * @private
+ * Begin a transaction, committing any previously open transaction
*/
- function tableNamesWithUseIndex( $tables, $use_index ) {
- $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 );
+ function begin( $fname = 'Database::begin' ) {
+ $this->query( 'BEGIN', $fname );
+ $this->mTrxLevel = 1;
}
/**
- * Wrapper for addslashes()
- * @param string $s String to be slashed.
- * @return string slashed string.
+ * End a transaction
*/
- function strencode( $s ) {
- return mysql_real_escape_string( $s, $this->mConn );
+ function commit( $fname = 'Database::commit' ) {
+ $this->query( 'COMMIT', $fname );
+ $this->mTrxLevel = 0;
}
/**
- * If it's a string, adds quotes and backslashes
- * Otherwise returns as-is
+ * Rollback a transaction.
+ * No-op on non-transactional databases.
*/
- function addQuotes( $s ) {
- if ( is_null( $s ) ) {
- return 'NULL';
- } else {
- # This will also quote numeric values. This should be harmless,
- # and protects against weird problems that occur when they really
- # _are_ strings such as article titles and string->number->string
- # conversion is not 1:1.
- return "'" . $this->strencode( $s ) . "'";
- }
+ function rollback( $fname = 'Database::rollback' ) {
+ $this->query( 'ROLLBACK', $fname, true );
+ $this->mTrxLevel = 0;
}
/**
- * Escape string for safe LIKE usage
+ * Begin a transaction, committing any previously open transaction
+ * @deprecated use begin()
*/
- function escapeLike( $s ) {
- $s=$this->strencode( $s );
- $s=str_replace(array('%','_'),array('\%','\_'),$s);
- return $s;
+ function immediateBegin( $fname = 'Database::immediateBegin' ) {
+ $this->begin();
}
/**
- * Returns an appropriately quoted sequence value for inserting a new row.
- * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
- * subclass will return an integer, and save the value for insertId()
+ * Commit transaction, if one is open
+ * @deprecated use commit()
*/
- function nextSequenceValue( $seqName ) {
- return NULL;
+ function immediateCommit( $fname = 'Database::immediateCommit' ) {
+ $this->commit();
}
/**
- * USE INDEX clause
- * PostgreSQL doesn't have them and returns ""
+ * Return MW-style timestamp used for MySQL schema
*/
- function useIndexClause( $index ) {
- return "FORCE INDEX ($index)";
+ function timestamp( $ts=0 ) {
+ return wfTimestamp(TS_MW,$ts);
}
/**
- * REPLACE query wrapper
- * PostgreSQL simulates this with a DELETE followed by INSERT
- * $row is the row to insert, an associative array
- * $uniqueIndexes is an array of indexes. Each element may be either a
- * field name or an array of field names
- *
- * It may be more efficient to leave off unique indexes which are unlikely to collide.
- * However if you do this, you run the risk of encountering errors which wouldn't have
- * occurred in MySQL
- *
- * @todo migrate comment to phodocumentor format
+ * Local database timestamp format or null
*/
- function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
- $table = $this->tableName( $table );
-
- # Single row case
- if ( !is_array( reset( $rows ) ) ) {
- $rows = array( $rows );
- }
-
- $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
- $first = true;
- foreach ( $rows as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeList( $row ) . ')';
+ function timestampOrNull( $ts = null ) {
+ if( is_null( $ts ) ) {
+ return null;
+ } else {
+ return $this->timestamp( $ts );
}
- return $this->query( $sql, $fname );
}
/**
- * DELETE where the condition is a join
- * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
- *
- * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
- * join condition matches, set $conds='*'
- *
- * DO NOT put the join condition in $conds
- *
- * @param string $delTable The table to delete from.
- * @param string $joinTable The other table.
- * @param string $delVar The variable to join on, in the first table.
- * @param string $joinVar The variable to join on, in the second table.
- * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
+ * @todo document
*/
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
- }
-
- $delTable = $this->tableName( $delTable );
- $joinTable = $this->tableName( $joinTable );
- $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
- if ( $conds != '*' ) {
- $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+ function resultObject( $result ) {
+ if( empty( $result ) ) {
+ return false;
+ } elseif ( $result instanceof ResultWrapper ) {
+ return $result;
+ } elseif ( $result === true ) {
+ // Successful write query
+ return $result;
+ } else {
+ return new ResultWrapper( $this, $result );
}
-
- return $this->query( $sql, $fname );
}
/**
- * Returns the size of a text field, or -1 for "unlimited"
+ * Return aggregated value alias
*/
- function textFieldSize( $table, $field ) {
- $table = $this->tableName( $table );
- $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
- $res = $this->query( $sql, 'Database::textFieldSize' );
- $row = $this->fetchObject( $res );
- $this->freeResult( $res );
+ function aggregateValue ($valuedata,$valuename='value') {
+ return $valuename;
+ }
- $m = array();
- if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
- $size = $m[1];
- } else {
- $size = -1;
- }
- return $size;
+ /**
+ * @return string wikitext of a link to the server software's web site
+ */
+ function getSoftwareLink() {
+ return "[http://www.mysql.com/ MySQL]";
}
/**
- * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
+ * @return string Version information from the database
*/
- function lowPriorityOption() {
- return 'LOW_PRIORITY';
+ function getServerVersion() {
+ return mysql_get_server_info( $this->mConn );
}
/**
- * DELETE query wrapper
- *
- * Use $conds == "*" to delete all rows
+ * Ping the server and try to reconnect if it there is no connection
*/
- function delete( $table, $conds, $fname = 'Database::delete' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
+ function ping() {
+ if( !function_exists( 'mysql_ping' ) ) {
+ wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
+ return true;
}
- $table = $this->tableName( $table );
- $sql = "DELETE FROM $table";
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ $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 $this->query( $sql, $fname );
+ return false;
}
/**
- * INSERT SELECT wrapper
- * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
- * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
- * $conds may be "*" to copy the whole table
- * srcTable may be an array of tables.
+ * Get slave lag.
+ * At the moment, this will only work if the DB user has the PROCESS privilege
*/
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
- $insertOptions = array(), $selectOptions = array() )
- {
- $destTable = $this->tableName( $destTable );
- if ( is_array( $insertOptions ) ) {
- $insertOptions = implode( ' ', $insertOptions );
- }
- if( !is_array( $selectOptions ) ) {
- $selectOptions = array( $selectOptions );
- }
- list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
- if( is_array( $srcTable ) ) {
- $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
- } else {
- $srcTable = $this->tableName( $srcTable );
+ function getLag() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
+ return $this->mFakeSlaveLag;
}
- $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
- " SELECT $startOpts " . implode( ',', $varMap ) .
- " FROM $srcTable $useIndex ";
- if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ $res = $this->query( 'SHOW PROCESSLIST' );
+ # 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,
+ * and therefore it doesn't know yet how lagged it is.
+ *
+ * Relay log I/O thread does not select databases.
+ */
+ if ( $row->User == 'system user' &&
+ $row->State != 'Waiting for master to send event' &&
+ $row->State != 'Connecting to master' &&
+ $row->State != 'Queueing master event to the relay log' &&
+ $row->State != 'Waiting for master update' &&
+ $row->State != 'Requesting binlog dump'
+ ) {
+ # This is it, return the time (except -ve)
+ if ( $row->Time > 0x7fffffff ) {
+ return false;
+ } else {
+ return $row->Time;
+ }
+ }
}
- $sql .= " $tailOpts";
- return $this->query( $sql, $fname );
+ return false;
}
/**
- * Construct a LIMIT query with optional offset
- * This is used for query pages
- * $sql string SQL query we will append the limit too
- * $limit integer the SQL limit
- * $offset integer the SQL offset (default false)
+ * Get status information from SHOW STATUS in an associative array
*/
- function limitResult($sql, $limit, $offset=false) {
- if( !is_numeric($limit) ) {
- throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
+ function getStatus($which="%") {
+ $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
+ $status = array();
+ while ( $row = $this->fetchObject( $res ) ) {
+ $status[$row->Variable_name] = $row->Value;
}
- return " $sql LIMIT "
- . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
- . "{$limit} ";
+ return $status;
}
- function limitResultForUpdate($sql, $num) {
- return $this->limitResult($sql, $num, 0);
+
+ /**
+ * Return the maximum number of items allowed in a list, or 0 for unlimited.
+ */
+ function maxListLen() {
+ return 0;
+ }
+
+ function encodeBlob($b) {
+ return $b;
+ }
+
+ function decodeBlob($b) {
+ return $b;
}
/**
- * Returns an SQL expression for a simple conditional.
- * Uses IF on MySQL.
- *
- * @param string $cond SQL expression which will result in a boolean value
- * @param string $trueVal SQL expression to return if true
- * @param string $falseVal SQL expression to return if false
- * @return string SQL fragment
+ * 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
*/
- function conditional( $cond, $trueVal, $falseVal ) {
- return " IF($cond, $trueVal, $falseVal) ";
+ public function setTimeout( $timeout ) {
+ $this->query( "SET net_read_timeout=$timeout" );
+ $this->query( "SET net_write_timeout=$timeout" );
}
/**
- * Determines if the last failure was due to a deadlock
+ * 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 wasDeadlock() {
- return $this->lastErrno() == 1213;
+ 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;
}
/**
- * Perform a deadlock-prone transaction.
- *
- * This function invokes a callback function to perform a set of write
- * queries. If a deadlock occurs during the processing, the transaction
- * will be rolled back and the callback function will be called again.
- *
- * Usage:
- * $dbw->deadlockLoop( callback, ... );
- *
- * Extra arguments are passed through to the specified callback function.
- *
- * Returns whatever the callback function returned on its successful,
- * iteration, or false on error, for example if the retry limit was
- * reached.
+ * 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 deadlockLoop() {
- $myFname = 'Database::deadlockLoop';
+ function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
+ $cmd = "";
+ $done = false;
+ $dollarquote = false;
- $this->begin();
- $args = func_get_args();
- $function = array_shift( $args );
- $oldIgnore = $this->ignoreErrors( true );
- $tries = DEADLOCK_TRIES;
- if ( is_array( $function ) ) {
- $fname = $function[0];
- } else {
- $fname = $function;
- }
- do {
- $retVal = call_user_func_array( $function, $args );
- $error = $this->lastError();
- $errno = $this->lastErrno();
- $sql = $this->lastQuery();
+ while ( ! feof( $fp ) ) {
+ if ( $lineCallback ) {
+ call_user_func( $lineCallback );
+ }
+ $line = trim( fgets( $fp, 1024 ) );
+ $sl = strlen( $line ) - 1;
- if ( $errno ) {
- if ( $this->wasDeadlock() ) {
- # Retry
- usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
- } else {
- $this->reportQueryError( $error, $errno, $sql, $fname );
+ if ( $sl < 0 ) { continue; }
+ if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
+
+ ## Allow dollar quoting for function declarations
+ if (substr($line,0,4) == '$mw$') {
+ if ($dollarquote) {
+ $dollarquote = false;
+ $done = true;
+ }
+ else {
+ $dollarquote = true;
}
}
- } while( $this->wasDeadlock() && --$tries > 0 );
- $this->ignoreErrors( $oldIgnore );
- if ( $tries <= 0 ) {
- $this->query( 'ROLLBACK', $myFname );
- $this->reportQueryError( $error, $errno, $sql, $fname );
- return false;
- } else {
- $this->query( 'COMMIT', $myFname );
- return $retVal;
+ else if (!$dollarquote) {
+ if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
+ $done = true;
+ $line = substr( $line, 0, $sl );
+ }
+ }
+
+ if ( '' != $cmd ) { $cmd .= ' '; }
+ $cmd .= "$line\n";
+
+ if ( $done ) {
+ $cmd = str_replace(';;', ";", $cmd);
+ $cmd = $this->replaceVars( $cmd );
+ $res = $this->query( $cmd, __METHOD__, true );
+ if ( $resultCallback ) {
+ call_user_func( $resultCallback, $res );
+ }
+
+ if ( false === $res ) {
+ $err = $this->lastError();
+ return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+ }
+
+ $cmd = '';
+ $done = false;
+ }
}
+ return true;
}
+
/**
- * Do a SELECT MASTER_POS_WAIT()
- *
- * @param string $file the binlog file
- * @param string $pos the binlog position
- * @param integer $timeout the maximum number of seconds to wait for synchronisation
+ * Replace variables in sourced SQL
*/
- function masterPosWait( $file, $pos, $timeout ) {
- $fname = 'Database::masterPosWait';
- wfProfileIn( $fname );
-
-
- # Commit any open transactions
- $this->immediateCommit();
+ protected function replaceVars( $ins ) {
+ $varnames = array(
+ 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
+ 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
+ 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
+ );
- # 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)";
- $res = $this->doQuery( $sql );
- if ( $res && $row = $this->fetchRow( $res ) ) {
- $this->freeResult( $res );
- wfProfileOut( $fname );
- return $row[0];
- } else {
- wfProfileOut( $fname );
- return false;
+ // Ordinary variables
+ foreach ( $varnames as $var ) {
+ if( isset( $GLOBALS[$var] ) ) {
+ $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
+ $ins = str_replace( '{$' . $var . '}', $val, $ins );
+ $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
+ $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
+ }
}
+
+ // Table prefixes
+ $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-z_]*)/',
+ array( &$this, 'tableNameCallback' ), $ins );
+ return $ins;
}
/**
- * Get the position of the master from SHOW SLAVE STATUS
+ * Table name callback
+ * @private
*/
- function getSlavePos() {
- $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 );
- } else {
- return array( false, false );
- }
+ protected function tableNameCallback( $matches ) {
+ return $this->tableName( $matches[1] );
}
- /**
- * Get the position of the master from SHOW MASTER STATUS
- */
- function getMasterPos() {
- $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
- $row = $this->fetchObject( $res );
- if ( $row ) {
- return array( $row->File, $row->Position );
- } else {
- return array( false, false );
- }
+ /*
+ * Build a concatenation list to feed into a SQL query
+ */
+ function buildConcat( $stringList ) {
+ return 'CONCAT(' . implode( ',', $stringList ) . ')';
}
- /**
- * Begin a transaction, committing any previously open transaction
- */
- function begin( $fname = 'Database::begin' ) {
- $this->query( 'BEGIN', $fname );
- $this->mTrxLevel = 1;
+}
+
+/**
+ * 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
+ *****************************************************************************/
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class DBObject {
+ public $mData;
+
+ function DBObject($data) {
+ $this->mData = $data;
}
- /**
- * End a transaction
- */
- function commit( $fname = 'Database::commit' ) {
- $this->query( 'COMMIT', $fname );
- $this->mTrxLevel = 0;
+ function isLOB() {
+ return false;
}
- /**
- * Rollback a transaction
- */
- function rollback( $fname = 'Database::rollback' ) {
- $this->query( 'ROLLBACK', $fname );
- $this->mTrxLevel = 0;
+ function data() {
+ return $this->mData;
}
+}
- /**
- * Begin a transaction, committing any previously open transaction
- * @deprecated use begin()
- */
- function immediateBegin( $fname = 'Database::immediateBegin' ) {
- $this->begin();
+/**
+ * Utility class
+ * @ingroup 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;
}
+}
- /**
- * Commit transaction, if one is open
- * @deprecated use commit()
- */
- function immediateCommit( $fname = 'Database::immediateCommit' ) {
- $this->commit();
+/**
+ * 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;
}
- /**
- * Return MW-style timestamp used for MySQL schema
- */
- function timestamp( $ts=0 ) {
- return wfTimestamp(TS_MW,$ts);
+ function name() {
+ return $this->name;
}
- /**
- * Local database timestamp format or null
- */
- function timestampOrNull( $ts = null ) {
- if( is_null( $ts ) ) {
- return null;
- } else {
- return $this->timestamp( $ts );
- }
+ function tableName() {
+ return $this->tableName;
}
- /**
- * @todo document
- */
- function resultObject( $result ) {
- if( empty( $result ) ) {
- return false;
- } elseif ( $result instanceof ResultWrapper ) {
- return $result;
- } elseif ( $result === true ) {
- // Successful write query
- return $result;
- } else {
- return new ResultWrapper( $this, $result );
- }
+ function defaultValue() {
+ return $this->default;
}
- /**
- * Return aggregated value alias
- */
- function aggregateValue ($valuedata,$valuename='value') {
- return $valuename;
+ function maxLength() {
+ return $this->max_length;
}
- /**
- * @return string wikitext of a link to the server software's web site
- */
- function getSoftwareLink() {
- return "[http://www.mysql.com/ MySQL]";
+ function nullable() {
+ return $this->nullable;
}
- /**
- * @return string Version information from the database
- */
- function getServerVersion() {
- return mysql_get_server_info( $this->mConn );
+ function isKey() {
+ return $this->is_key;
}
- /**
- * 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 {
- wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
- return true;
- }
+ function isMultipleKey() {
+ return $this->is_multiple;
}
- /**
- * Get slave lag.
- * At the moment, this will only work if the DB user has the PROCESS privilege
- */
- function getLag() {
- $res = $this->query( 'SHOW PROCESSLIST' );
- # 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,
- * and therefore it doesn't know yet how lagged it is.
- *
- * Relay log I/O thread does not select databases.
- */
- if ( $row->User == 'system user' &&
- $row->State != 'Waiting for master to send event' &&
- $row->State != 'Connecting to master' &&
- $row->State != 'Queueing master event to the relay log' &&
- $row->State != 'Waiting for master update' &&
- $row->State != 'Requesting binlog dump'
- ) {
- # This is it, return the time (except -ve)
- if ( $row->Time > 0x7fffffff ) {
- return false;
- } else {
- return $row->Time;
- }
- }
- }
- return false;
+ function type() {
+ return $this->type;
}
+}
+
+/******************************************************************************
+ * Error classes
+ *****************************************************************************/
+
+/**
+ * Database error base class
+ * @ingroup Database
+ */
+class DBError extends MWException {
+ public $db;
/**
- * Get status information from SHOW STATUS in an associative array
+ * 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 getStatus($which="%") {
- $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
- $status = array();
- while ( $row = $this->fetchObject( $res ) ) {
- $status[$row->Variable_name] = $row->Value;
- }
- return $status;
+ function __construct( Database &$db, $error ) {
+ $this->db =& $db;
+ parent::__construct( $error );
}
+}
- /**
- * Return the maximum number of items allowed in a list, or 0 for unlimited.
- */
- function maxListLen() {
- return 0;
+/**
+ * @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 encodeBlob($b) {
- return $b;
+ function useOutputPage() {
+ // Not likely to work
+ return false;
}
- function decodeBlob($b) {
- return $b;
+ function useMessageCache() {
+ // Not likely to work
+ return false;
+ }
+
+ function getText() {
+ return $this->getMessage() . "\n";
}
- /**
- * 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" );
+ function getLogMessage() {
+ # Don't send to the exception log
+ return false;
}
- /**
- * 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, $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;
+ function getPageTitle() {
+ global $wgSitename;
+ return "$wgSitename has a problem";
}
- /**
- * 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;
+ function getHTML() {
+ global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
+ global $wgSitename, $wgServer, $wgMessageCache;
- while ( ! feof( $fp ) ) {
- if ( $lineCallback ) {
- call_user_func( $lineCallback );
- }
- $line = trim( fgets( $fp, 1024 ) );
- $sl = strlen( $line ) - 1;
+ # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
+ # Hard coding strings instead.
- if ( $sl < 0 ) { continue; }
- if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
+ $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;
- ## Allow dollar quoting for function declarations
- if (substr($line,0,4) == '$mw$') {
- if ($dollarquote) {
- $dollarquote = false;
- $done = true;
- }
- else {
- $dollarquote = true;
- }
- }
- else if (!$dollarquote) {
- if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
- $done = true;
- $line = substr( $line, 0, $sl );
- }
- }
+ $googlesearch = "
+<!-- SiteSearch Google -->
+<FORM method=GET action=\"http://www.google.com/search\">
+<TABLE bgcolor=\"#FFFFFF\"><tr><td>
+<A HREF=\"http://www.google.com/\">
+<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
+border=\"0\" ALT=\"Google\"></A>
+</td>
+<td>
+<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
+<INPUT type=submit name=btnG VALUE=\"Google Search\">
+<font size=-1>
+<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
+<input type='hidden' name='ie' value='$2'>
+<input type='hidden' name='oe' value='$2'>
+</font>
+</td></tr></TABLE>
+</FORM>
+<!-- SiteSearch Google -->";
+ $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
- if ( '' != $cmd ) { $cmd .= ' '; }
- $cmd .= "$line\n";
+ # No database access
+ if ( is_object( $wgMessageCache ) ) {
+ $wgMessageCache->disable();
+ }
- if ( $done ) {
- $cmd = str_replace(';;', ";", $cmd);
- $cmd = $this->replaceVars( $cmd );
- $res = $this->query( $cmd, __METHOD__, true );
- if ( $resultCallback ) {
- call_user_func( $resultCallback, $res );
- }
+ if ( trim( $this->error ) == '' ) {
+ $this->error = $this->db->getProperty('mServer');
+ }
- if ( false === $res ) {
- $err = $this->lastError();
- return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+ $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 );
}
+ }
- $cmd = '';
- $done = false;
+ $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 true;
+
+ 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";
- /**
- * Replace variables in sourced SQL
- */
- protected function replaceVars( $ins ) {
- $varnames = array(
- 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
- 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
- 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
- );
+ parent::__construct( $db, $message );
+ $this->error = $error;
+ $this->errno = $errno;
+ $this->sql = $sql;
+ $this->fname = $fname;
+ }
- // Ordinary variables
- foreach ( $varnames as $var ) {
- if( isset( $GLOBALS[$var] ) ) {
- $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
- $ins = str_replace( '{$' . $var . '}', $val, $ins );
- $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
- $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
- }
+ function getText() {
+ if ( $this->useMessageCache() ) {
+ return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+ } else {
+ return $this->getMessage();
}
-
- // Table prefixes
- $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-z_]*)/',
- array( &$this, 'tableNameCallback' ), $ins );
- return $ins;
}
-
- /**
- * Table name callback
- * @private
- */
- protected function tableNameCallback( $matches ) {
- return $this->tableName( $matches[1] );
+
+ 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;
}
- /*
- * Build a concatenation list to feed into a SQL query
- */
- function buildConcat( $stringList ) {
- return 'CONCAT(' . implode( ',', $stringList ) . ')';
+ 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() ) );
+ }
+ }
}
/**
- * Database abstraction object for mySQL
- * Inherit all methods and properties of Database::Database()
- *
- * @addtogroup Database
- * @see Database
+ * @ingroup Database
*/
-class DatabaseMysql extends Database {
- # Inherit all
-}
+class DBUnexpectedError extends DBError {}
/**
* Result wrapper for grabbing data queried by someone else
- * @addtogroup Database
+ * @ingroup Database
*/
class ResultWrapper implements Iterator {
var $db, $result, $pos = 0, $currentRow = null;
}
}
+class MySQLMasterPos {
+ var $file, $pos;
+ function __construct( $file, $pos ) {
+ $this->file = $file;
+ $this->pos = $pos;
+ }
+
+ function __toString() {
+ return "{$this->file}/{$this->pos}";
+ }
+}