/**
* Utility class.
+ * @addtogroup Database
*/
class DBObject {
public $mData;
}
};
+/**
+ * Utility class
+ * @addtogroup Database
+ *
+ * This allows us to distinguish a blob from a normal string and an array of strings
+ */
+class Blob {
+ private $mData;
+ function __construct($data) {
+ $this->mData = $data;
+ }
+ function fetch() {
+ return $this->mData;
+ }
+};
+
/**
* Utility class.
+ * @addtogroup Database
*/
class MySQLField {
private $name, $tablename, $default, $max_length, $nullable,
/**
* Database error base class
+ * @addtogroup Database
*/
class DBError extends MWException {
public $db;
}
}
+/**
+ * @addtogroup Database
+ */
class DBConnectionError extends DBError {
public $error;
$cache = new HTMLFileCache( $t );
if( $cache->isFileCached() ) {
- // FIXME: $msg is not defined on the next line.
+ // @todo, FIXME: $msg is not defined on the next line.
$msg = '<p style="color: red"><b>'.$msg."<br />\n" .
$cachederror . "</b></p>\n";
}
}
+/**
+ * @addtogroup Database
+ */
class DBQueryError extends DBError {
public $error, $errno, $sql, $fname;
}
}
+/**
+ * @addtogroup Database
+ */
class DBUnexpectedError extends DBError {}
/******************************************************************************/
/**
* Database abstraction object
+ * @addtogroup Database
*/
class Database {
return true;
}
+ /**
+ * Returns true if this database does an implicit order by when the column has an index
+ * For example: SELECT page_title FROM page LIMIT 1
+ */
+ function implicitOrderby() {
+ return true;
+ }
+
/**
* Returns true if this database can do a native search on IP columns
* e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
return false;
}
+ /**
+ * Returns true if this database can use functional indexes
+ */
+ function functionalIndexes() {
+ return false;
+ }
+
/**#@+
* Get function
*/
@/**/$this->mConn = mysql_connect( $server, $user, $password, true );
}
if ($this->mConn === false) {
- $iplus = $i + 1;
+ #$iplus = $i + 1;
#wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
}
}
}
if ( $success ) {
- global $wgDBmysql5;
- if( $wgDBmysql5 ) {
+ $version = $this->getServerVersion();
+ if ( version_compare( $version, '4.1' ) >= 0 ) {
// Tell the server we're communicating with it in UTF-8.
// This may engage various charset conversions.
- $this->query( 'SET NAMES utf8' );
+ global $wgDBmysql5;
+ if( $wgDBmysql5 ) {
+ $this->query( 'SET NAMES utf8', __METHOD__ );
+ }
+ // Turn off strict mode
+ $this->query( "SET sql_mode = ''", __METHOD__ );
}
+
+ // Turn off strict mode if it is on
} else {
$this->reportConnectionError();
}
* Usually aborts on failure. If errors are explicitly ignored, returns success.
*
* @param $sql String: SQL query
- * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST comment (you can use __METHOD__ or add some extra info)
- * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors... maybe best to catch the exception instead?
- * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure if $tempIgnore set
+ * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST
+ * comment (you can use __METHOD__ or add some extra info)
+ * @param $tempIgnore Bool: Whether to avoid throwing an exception on errors...
+ * maybe best to catch the exception instead?
+ * @return true for a successful write query, ResultWrapper object for a successful read query,
+ * or false on failure if $tempIgnore set
* @throws DBQueryError Thrown when the database returns an error of any kind
*/
public function query( $sql, $fname = '', $tempIgnore = false ) {
$this->mLastQuery = $sql;
# Add a comment for easy SHOW PROCESSLIST interpretation
- if ( $fname ) {
- $commentedSql = preg_replace('/\s/', " /* $fname */ ", $sql, 1);
- } else {
- $commentedSql = $sql;
- }
+ #if ( $fname ) {
+ global $wgUser;
+ if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
+ $userName = $wgUser->getName();
+ if ( mb_strlen( $userName ) > 15 ) {
+ $userName = mb_substr( $userName, 0, 15 ) . '...';
+ }
+ $userName = str_replace( '/', '', $userName );
+ } else {
+ $userName = '';
+ }
+ $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
+ #} else {
+ # $commentedSql = $sql;
+ #}
# If DBO_TRX is set, start a transaction
if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() &&
- $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK'
- ) {
- $this->begin();
+ $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
+ // avoid establishing transactions for SHOW and SET statements too -
+ // that would delay transaction initializations to once connection
+ // is really used by application
+ $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
+ if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0)
+ $this->begin();
}
if ( $this->debug() ) {
wfProfileOut( $queryProf );
wfProfileOut( $totalProf );
}
- return $ret;
+ return $this->resultObject( $ret );
}
/**
* 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" );
}
* @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() ) );
* @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
@/**/$row = mysql_fetch_array( $res );
if ( $this->lastErrno() ) {
throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
* Get the number of rows in a result object
*/
function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
@/**/$n = mysql_num_rows( $res );
if( $this->lastErrno() ) {
throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
* Get the number of fields in a result object
* See documentation for mysql_num_fields()
*/
- function numFields( $res ) { return mysql_num_fields( $res ); }
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_num_fields( $res );
+ }
/**
* Get a field name in a result object
* See documentation for mysql_field_name():
* http://www.php.net/mysql_field_name
*/
- function fieldName( $res, $n ) { return mysql_field_name( $res, $n ); }
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_field_name( $res, $n );
+ }
/**
* Get the inserted value of an auto-increment row
* Change the position of the cursor in a result object
* See mysql_data_seek()
*/
- function dataSeek( $res, $row ) { return mysql_data_seek( $res, $row ); }
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_data_seek( $res, $row );
+ }
/**
* Get the last error number
}
if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
//if (isset($options['LIMIT'])) {
if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
# Various MySQL extensions
if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
if (isset($options['EXPLAIN'])) {
$sql = 'EXPLAIN ' . $sql;
}
-
return $this->query( $sql, $fname );
}
function fieldInfo( $table, $field ) {
$table = $this->tableName( $table );
$res = $this->query( "SELECT * FROM $table LIMIT 1" );
- $n = mysql_num_fields( $res );
+ $n = mysql_num_fields( $res->result );
for( $i = 0; $i < $n; $i++ ) {
- $meta = mysql_fetch_field( $res, $i );
+ $meta = mysql_fetch_field( $res->result, $i );
if( $field == $meta->name ) {
return new MySQLField($meta);
}
* mysql_field_type() wrapper
*/
function fieldType( $res, $index ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
return mysql_field_type( $res, $index );
}
* (for the log)
* @param array $options An array of UPDATE options, can be one or
* more of IGNORE, LOW_PRIORITY
+ * @return bool
*/
function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
$table = $this->tableName( $table );
if ( $conds != '*' ) {
$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
}
- $this->query( $sql, $fname );
+ return $this->query( $sql, $fname );
}
/**
$list .= "($value)";
} elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
$list .= "$value";
- } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array ($value) ) {
- $list .= $field." IN (".$this->makeList($value).") ";
+ } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
+ if( count( $value ) == 0 ) {
+ // Empty input... or should this throw an error?
+ $list .= '0';
+ } elseif( count( $value ) == 1 ) {
+ // Special-case single values, as IN isn't terribly efficient
+ $list .= $field." = ".$this->addQuotes( $value[0] );
+ } else {
+ $list .= $field." IN (".$this->makeList($value).") ";
+ }
+ } elseif( is_null($value) ) {
+ if ( $mode == LIST_AND || $mode == LIST_OR ) {
+ $list .= "$field IS ";
+ } elseif ( $mode == LIST_SET ) {
+ $list .= "$field = ";
+ }
+ $list .= 'NULL';
} else {
if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
$list .= "$field = ";
global $wgSharedDB;
# Skip quoted literals
if ( $name{0} != '`' ) {
- if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) {
+ if ( $this->mTablePrefix !== '' && strpos( $name, '.' ) === false ) {
$name = "{$this->mTablePrefix}$name";
}
if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) {
* This is handy when you need to construct SQL for joins
*
* Example:
- * list( $user, $watchlist ) = $dbr->tableNames('user','watchlist');
+ * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
* $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
* WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
*/
*/
function resultObject( $result ) {
if( empty( $result ) ) {
- return NULL;
+ return false;
+ } elseif ( $result instanceof ResultWrapper ) {
+ return $result;
+ } elseif ( $result === true ) {
+ // Successful write query
+ return $result;
} else {
return new ResultWrapper( $this, $result );
}
*/
function getLag() {
$res = $this->query( 'SHOW PROCESSLIST' );
- # Find slave SQL thread. Assumed to be the second one running, which is a bit
- # dubious, but unfortunately there's no easy rigorous way
+ # Find slave SQL thread
while ( $row = $this->fetchObject( $res ) ) {
/* This should work for most situations - when default db
* for thread is not specified, it had no events executed,
/**
* Read and execute SQL commands from a file.
* Returns true on success, error string on failure
+ * @param string $filename File name to open
+ * @param callback $lineCallback Optional function called before reading each line
+ * @param callback $resultCallback Optional function called for each MySQL result
*/
- function sourceFile( $filename ) {
+ function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
$fp = fopen( $filename, 'r' );
if ( false === $fp ) {
return "Could not open \"{$filename}\".\n";
}
+ $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
+ fclose( $fp );
+ return $error;
+ }
+ /**
+ * Read and execute commands from an open file handle
+ * Returns true on success, error string on failure
+ * @param string $fp File handle
+ * @param callback $lineCallback Optional function called before reading each line
+ * @param callback $resultCallback Optional function called for each MySQL result
+ */
+ function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
$cmd = "";
$done = false;
$dollarquote = false;
while ( ! feof( $fp ) ) {
+ if ( $lineCallback ) {
+ call_user_func( $lineCallback );
+ }
$line = trim( fgets( $fp, 1024 ) );
$sl = strlen( $line ) - 1;
if ( $done ) {
$cmd = str_replace(';;', ";", $cmd);
$cmd = $this->replaceVars( $cmd );
- $res = $this->query( $cmd, 'dbsource', true );
+ $res = $this->query( $cmd, __METHOD__, true );
+ if ( $resultCallback ) {
+ call_user_func( $resultCallback, $res );
+ }
if ( false === $res ) {
$err = $this->lastError();
$done = false;
}
}
- fclose( $fp );
return true;
}
+
/**
* Replace variables in sourced SQL
*/
$varnames = array(
'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
- 'wgDBadminuser', 'wgDBadminpassword',
+ 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
);
// Ordinary variables
return $this->tableName( $matches[1] );
}
+ /*
+ * Build a concatenation list to feed into a SQL query
+ */
+ function buildConcat( $stringList ) {
+ return 'CONCAT(' . implode( ',', $stringList ) . ')';
+ }
+
}
/**
* Database abstraction object for mySQL
* Inherit all methods and properties of Database::Database()
*
+ * @addtogroup Database
* @see Database
*/
class DatabaseMysql extends Database {
/**
* Result wrapper for grabbing data queried by someone else
- *
+ * @addtogroup Database
*/
-class ResultWrapper {
- var $db, $result;
+class ResultWrapper implements Iterator {
+ var $db, $result, $pos = 0, $currentRow = null;
/**
- * @todo document
+ * Create a new result object from a result resource and a Database object
*/
- function ResultWrapper( &$database, $result ) {
- $this->db =& $database;
- $this->result =& $result;
+ function ResultWrapper( $database, $result ) {
+ $this->db = $database;
+ if ( $result instanceof ResultWrapper ) {
+ $this->result = $result->result;
+ } else {
+ $this->result = $result;
+ }
}
/**
- * @todo document
+ * Get the number of rows in a result object
*/
function numRows() {
return $this->db->numRows( $this->result );
}
/**
- * @todo document
+ * Fetch the next row from the given result object, in object form.
+ * Fields can be retrieved with $row->fieldname, with fields acting like
+ * member variables.
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchObject() {
return $this->db->fetchObject( $this->result );
}
/**
- * @todo document
+ * Fetch the next row from the given result object, in associative array
+ * form. Fields are retrieved with $row['fieldname'].
+ *
+ * @param $res SQL result object as returned from Database::query(), etc.
+ * @return MySQL row object
+ * @throws DBUnexpectedError Thrown if the database returns an error
*/
function fetchRow() {
return $this->db->fetchRow( $this->result );
}
/**
- * @todo document
+ * Free a result object
*/
function free() {
$this->db->freeResult( $this->result );
unset( $this->db );
}
+ /**
+ * Change the position of the cursor in a result object
+ * See mysql_data_seek()
+ */
function seek( $row ) {
$this->db->dataSeek( $this->result, $row );
}
-
+
+ /*********************
+ * Iterator functions
+ * Note that using these in combination with the non-iterator functions
+ * above may cause rows to be skipped or repeated.
+ */
+
function rewind() {
if ($this->numRows()) {
$this->db->dataSeek($this->result, 0);
}
+ $this->pos = 0;
+ $this->currentRow = null;
+ }
+
+ function current() {
+ if ( is_null( $this->currentRow ) ) {
+ $this->next();
+ }
+ return $this->currentRow;
}
+ function key() {
+ return $this->pos;
+ }
+
+ function next() {
+ $this->pos++;
+ $this->currentRow = $this->fetchObject();
+ return $this->currentRow;
+ }
+
+ function valid() {
+ return $this->current() !== false;
+ }
}
-?>
+