aboutsummaryrefslogtreecommitdiffstats
path: root/includes/Database.php
diff options
context:
space:
mode:
Diffstat (limited to 'includes/Database.php')
-rw-r--r--includes/Database.php372
1 files changed, 301 insertions, 71 deletions
diff --git a/includes/Database.php b/includes/Database.php
index bab6cd873570..c1ba5cc94cac 100644
--- a/includes/Database.php
+++ b/includes/Database.php
@@ -60,7 +60,8 @@ class Database {
# Other functions
#------------------------------------------------------------------------------
- function Database()
+ function Database( $server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $debug = false, $bufferResults = true, $ignoreErrors = false )
{
global $wgOut;
# Can't get a reference if it hasn't been set yet
@@ -68,27 +69,27 @@ class Database {
$wgOut = NULL;
}
$this->mOut =& $wgOut;
-
+
+ $this->mFailFunction = $failFunction;
+ $this->mIgnoreErrors = $ignoreErrors;
+ $this->mDebug = $debug;
+ $this->mBufferResults = $bufferResults;
+ if ( $server ) {
+ $this->open( $server, $user, $password, $dbName );
+ }
}
/* static */ function newFromParams( $server, $user, $password, $dbName,
$failFunction = false, $debug = false, $bufferResults = true, $ignoreErrors = false )
{
- $db = new Database;
- $db->mFailFunction = $failFunction;
- $db->mIgnoreErrors = $ignoreErrors;
- $db->mDebug = $debug;
- $db->mBufferResults = $bufferResults;
- $db->open( $server, $user, $password, $dbName );
- return $db;
+ return new Database( $server, $user, $password, $dbName, $failFunction, $debug,
+ $bufferResults, $ignoreErrors );
}
# Usually aborts on failure
# If the failFunction is set to a non-zero integer, returns success
function open( $server, $user, $password, $dbName )
{
- global $wgEmergencyContact;
-
# Test for missing mysql.so
# Otherwise we get a suppressed fatal error, which is very hard to track down
if ( !function_exists( 'mysql_connect' ) ) {
@@ -155,7 +156,7 @@ class Database {
# Usually aborts on failure
# If errors are explicitly ignored, returns success
- function query( $sql, $fname = "" )
+ function query( $sql, $fname = "", $tempIgnore = false )
{
global $wgProfiling, $wgCommandLineMode;
@@ -173,16 +174,26 @@ class Database {
$sqlx = wordwrap(strtr($sqlx,"\t\n"," "));
wfDebug( "SQL: $sqlx\n" );
}
+ # Add a comment for easy SHOW PROCESSLIST interpretation
+ if ( $fname ) {
+ $commentedSql = "/* $fname */ $sql";
+ } else {
+ $commentedSql = $sql;
+ }
+
if( $this->mBufferResults ) {
- $ret = mysql_query( $sql, $this->mConn );
+ $ret = mysql_query( $commentedSql, $this->mConn );
} else {
- $ret = mysql_unbuffered_query( $sql, $this->mConn );
+ $ret = mysql_unbuffered_query( $commentedSql, $this->mConn );
}
if ( false === $ret ) {
+ # Ignore errors during error handling to avoid infinite recursion
+ $ignore = $this->setIgnoreErrors( true );
+
$error = mysql_error( $this->mConn );
$errno = mysql_errno( $this->mConn );
- if( $this->mIgnoreErrors ) {
+ if( $ignore || $tempIgnore ) {
wfDebug("SQL ERROR (ignored): " . $error . "\n");
} else {
$sql1line = str_replace( "\n", "\\n", $sql );
@@ -199,6 +210,7 @@ class Database {
$this->mOut->databaseError( $fname, $sql, $error, $errno );
}
}
+ $this->setIgnoreErrors( $ignore );
}
if ( $wgProfiling ) {
@@ -216,7 +228,7 @@ class Database {
@$row = mysql_fetch_object( $res );
# FIXME: HACK HACK HACK HACK debug
if( mysql_errno() ) {
- wfDebugDieBacktrace( "SQL error: " . htmlspecialchars( mysql_error() ) );
+ wfDebugDieBacktrace( "Error in fetchObject(): " . htmlspecialchars( mysql_error() ) );
}
return $row;
}
@@ -224,7 +236,7 @@ class Database {
function fetchRow( $res ) {
@$row = mysql_fetch_array( $res );
if (mysql_errno() ) {
- wfDebugDieBacktrace( "SQL error: " . htmlspecialchars( mysql_error() ) );
+ wfDebugDieBacktrace( "Error in fetchRow(): " . htmlspecialchars( mysql_error() ) );
}
return $row;
}
@@ -232,7 +244,7 @@ class Database {
function numRows( $res ) {
@$n = mysql_num_rows( $res );
if( mysql_errno() ) {
- wfDebugDieBacktrace( "SQL error: " . htmlspecialchars( mysql_error() ) );
+ wfDebugDieBacktrace( "Error in numRows(): " . htmlspecialchars( mysql_error() ) );
}
return $n;
}
@@ -249,6 +261,7 @@ class Database {
# If errors are explicitly ignored, returns success
function set( $table, $var, $value, $cond, $fname = "Database::set" )
{
+ $table = $this->tableName( $table );
$sql = "UPDATE $table SET $var = '" .
wfStrencode( $value ) . "' WHERE ($cond)";
return !!$this->query( $sql, DB_WRITE, $fname );
@@ -257,41 +270,89 @@ class Database {
# 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 get( $table, $var, $cond, $fname = "Database::get" )
+ function getField( $table, $var, $cond, $fname = "Database::get" )
{
- $sql = "SELECT $var FROM $table WHERE ($cond)";
- $result = $this->query( $sql, DB_READ, $fname );
-
- $ret = "";
- if ( mysql_num_rows( $result ) > 0 ) {
- $s = mysql_fetch_object( $result );
- $ret = $s->$var;
- mysql_free_result( $result );
+ $table = $this->tableName( $table );
+ $from = $table?" FROM $table ":"";
+ if ( is_array( $cond ) ) {
+ $where = ' WHERE ' . $this->makeList( $cond, LIST_AND );
+ } elseif ( $cond ) {
+ $where = " WHERE ($cond)";
+ } else {
+ $where = '';
+ }
+ $sql = "SELECT $var $from $where LIMIT 1";
+ $result = $this->query( $sql, $fname );
+
+ $ret = false;
+ if ( $this->numRows( $result ) > 0 ) {
+ $s = $this->fetchRow( $result );
+ $ret = $s[0];
+ $this->freeResult( $result );
}
return $ret;
}
- # More complex SELECT wrapper, single row only
- # Aborts or returns FALSE on error
- # Takes an array of selected variables, and a condition map, which is ANDed
- # e.g. getArray( "cur", array( "cur_id" ), array( "cur_namespace" => 0, "cur_title" => "Astronomy" ) )
- # would return an object where $obj->cur_id is the ID of the Astronomy article
- function getArray( $table, $vars, $conds, $fname = "Database::getArray" )
+ # SELECT wrapper
+ function select( $table, $vars, $conds, $fname = "Database::select", $options = array() )
{
$vars = implode( ",", $vars );
+ $table = $this->tableName( $table );
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
+
+ $tailOpts = '';
+
+ if ( isset( $options['ORDER BY'] ) ) {
+ $tailOpts .= " ORDER BY {$options['ORDER BY']}";
+ }
+ if ( isset( $options['LIMIT'] ) ) {
+ $tailOpts .= " LIMIT {$options['LIMIT']}";
+ }
+
+ if ( is_numeric( array_search( 'FOR UPDATE', $options ) ) ) {
+ $tailOpts .= ' FOR UPDATE';
+ }
+
+ if ( is_numeric( array_search( 'LOCK IN SHARE MODE', $options ) ) ) {
+ $tailOpts .= ' LOCK IN SHARE MODE';
+ }
+
+ if ( isset( $options['USE INDEX'] ) ) {
+ $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+ } else {
+ $useIndex = '';
+ }
+
if ( $conds !== false ) {
- $where = Database::makeList( $conds, LIST_AND );
- $sql = "SELECT $vars FROM $table WHERE $where LIMIT 1";
+ $where = $this->makeList( $conds, LIST_AND );
+ $sql = "SELECT $vars FROM $table $useIndex WHERE $where $tailOpts";
} else {
- $sql = "SELECT $vars FROM $table LIMIT 1";
+ $sql = "SELECT $vars FROM $table $useIndex $tailOpts";
}
- $res = $this->query( $sql, $fname );
+ return $this->query( $sql, $fname );
+ }
+
+ # 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. getArray( "cur", array( "cur_id" ), array( "cur_namespace" => 0, "cur_title" => "Astronomy" ) )
+ # would return an object where $obj->cur_id is the ID of the Astronomy article
+ function getArray( $table, $vars, $conds, $fname = "Database::getArray", $options = array() ) {
+ $options['LIMIT'] = 1;
+ $res = $this->select( $table, $vars, $conds, $fname, $options );
if ( $res === false || !$this->numRows( $res ) ) {
return false;
}
$obj = $this->fetchObject( $res );
$this->freeResult( $res );
return $obj;
+
}
# Removes most variables from an SQL query and replaces them with X or N for numbers.
@@ -322,6 +383,7 @@ class Database {
# If errors are explicitly ignored, returns NULL on failure
function fieldExists( $table, $field, $fname = "Database::fieldExists" )
{
+ $table = $this->tableName( $table );
$res = $this->query( "DESCRIBE $table", DB_READ, $fname );
if ( !$res ) {
return NULL;
@@ -343,28 +405,35 @@ class Database {
# If errors are explicitly ignored, returns NULL on failure
function indexExists( $table, $index, $fname = "Database::indexExists" )
{
+ $info = $this->indexInfo( $table, $index, $fname );
+ if ( is_null( $info ) ) {
+ return NULL;
+ } else {
+ return $info !== false;
+ }
+ }
+
+ 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, DB_READ, $fname );
+ $res = $this->query( $sql, $fname );
if ( !$res ) {
return NULL;
}
- $found = false;
-
while ( $row = $this->fetchObject( $res ) ) {
if ( $row->Key_name == $index ) {
- $found = true;
- break;
+ return $row;
}
}
- return $found;
+ return false;
}
-
function tableExists( $table )
{
+ $table = $this->tableName( $table );
$old = $this->mIgnoreErrors;
$this->mIgnoreErrors = true;
$res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
@@ -379,6 +448,7 @@ class Database {
function fieldInfo( $table, $field )
{
+ $table = $this->tableName( $table );
$res = $this->query( "SELECT * FROM $table LIMIT 1" );
$n = mysql_num_fields( $res );
for( $i = 0; $i < $n; $i++ ) {
@@ -391,39 +461,61 @@ class Database {
}
# INSERT wrapper, inserts an array into a table
- # Keys are field names, values are values
+ #
+ # $a may be a single associative array, or an array of these with numeric keys, for
+ # multi-row insert.
+ #
# Usually aborts on failure
# If errors are explicitly ignored, returns success
- function insertArray( $table, $a, $fname = "Database::insertArray" )
+ function insertArray( $table, $a, $fname = "Database::insertArray", $options = array() )
{
- $sql1 = "INSERT INTO $table (";
- $sql2 = "VALUES (" . Database::makeList( $a );
- $first = true;
- foreach ( $a as $field => $value ) {
- if ( !$first ) {
- $sql1 .= ",";
+ $table = $this->tableName( $table );
+ if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+ $multi = true;
+ $keys = array_keys( $a[0] );
+ } else {
+ $multi = false;
+ $keys = array_keys( $a );
+ }
+
+ $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 ) . ')';
}
- $first = false;
- $sql1 .= $field;
+ } else {
+ $sql .= '(' . $this->makeList( $a ) . ')';
}
- $sql = "$sql1) $sql2)";
return !!$this->query( $sql, $fname );
}
- # A cross between insertArray and getArray, takes a condition array and a SET array
+ # UPDATE wrapper, takes a condition array and a SET array
function updateArray( $table, $values, $conds, $fname = "Database::updateArray" )
{
+ $table = $this->tableName( $table );
$sql = "UPDATE $table SET " . $this->makeList( $values, LIST_SET );
$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
$this->query( $sql, $fname );
}
# Makes a wfStrencoded list from an array
- # $mode: LIST_COMMA - comma separated, no field names
- # LIST_AND - ANDed WHERE clause (without the WHERE)
- # LIST_SET - comma separated with field names, like a SET clause
- /* static */ function makeList( $a, $mode = LIST_COMMA )
+ # $mode: LIST_COMMA - comma separated, no field names
+ # LIST_AND - ANDed WHERE clause (without the WHERE)
+ # LIST_SET - comma separated with field names, like a SET clause
+ function makeList( $a, $mode = LIST_COMMA )
{
+ if ( !is_array( $a ) ) {
+ wfDebugDieBacktrace( 'Database::makeList called with incorrect parameters' );
+ }
+
$first = true;
$list = "";
foreach ( $a as $field => $value ) {
@@ -436,13 +528,13 @@ class Database {
} else {
$first = false;
}
- if ( $mode == LIST_AND || $mode == LIST_SET ) {
- $list .= "$field=";
- }
- if ( !is_numeric( $value ) ) {
- $list .= "'" . wfStrencode( $value ) . "'";
+ if ( $mode == LIST_AND && is_numeric( $field ) ) {
+ $list .= "($value)";
} else {
- $list .= $value;
+ if ( $mode == LIST_AND || $mode == LIST_SET ) {
+ $list .= "$field=";
+ }
+ $list .= $this->addQuotes( $value );
}
}
return $list;
@@ -465,8 +557,151 @@ class Database {
function stopTimer()
{
}
+
+ function tableName( $name ) {
+ return $name;
+ }
+
+ function strencode( $s ) {
+ return addslashes( $s );
+ }
+
+ # If it's a string, adds quotes and backslashes
+ # Otherwise returns as-is
+ function addQuotes( $s ) {
+ if ( !is_numeric( $s ) ) {
+ $s = "'" . $this->strencode( $s ) . "'";
+ } else if ( is_null( $s ) ) {
+ $s = 'NULL';
+ }
+ return $s;
+ }
+
+ # 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 nextSequenceValue( $seqName ) {
+ return NULL;
+ }
+
+ # USE INDEX clause
+ # PostgreSQL doesn't have them and returns ""
+ function useIndexClause( $index ) {
+ return "USE INDEX ($index)";
+ }
+
+ # 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
+ 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_flip( $rows[0] ) ) .") VALUES ";
+ $first = true;
+ foreach ( $rows as $row ) {
+ if ( $first ) {
+ $first = false;
+ } else {
+ $sql .= ",";
+ }
+ $sql .= "(" . $this->makeList( $row ) . ")";
+ }
+ 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
+ #
+ # $delTable is the table to delete from
+ # $joinTable is the other table
+ # $delVar is the variable to join on, in the first table
+ # $joinVar is the variable to join on, in the second table
+ # $conds is a condition array of field names mapped to variables, ANDed together in the WHERE clause
+ #
+ # 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
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
+ if ( !$conds ) {
+ wfDebugDieBacktrace( '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 );
+ }
+
+ return $this->query( $sql, $fname );
+ }
+
+ # Returns the size of a text field, or -1 for "unlimited"
+ function textFieldSize( $table, $field ) {
+ $table = $this->tableName( $table );
+ $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
+ $res = $this->query( $sql, "Database::textFieldSize" );
+ $row = wfFetchObject( $res );
+ $this->freeResult( $res );
+
+ if ( preg_match( "/\((.*)\)/", $row->Type, $m ) ) {
+ $size = $m[1];
+ } else {
+ $size = -1;
+ }
+ return $size;
+ }
+
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
+ }
+
+ # Use $conds == "*" to delete all rows
+ function delete( $table, $conds, $fname = "Database::delete" ) {
+ if ( !$conds ) {
+ wfDebugDieBacktrace( "Database::delete() called with no conditions" );
+ }
+ $table = $this->tableName( $table );
+ $sql = "DELETE FROM $table ";
+ if ( $conds != '*' ) {
+ $sql .= "WHERE " . $this->makeList( $conds, LIST_AND );
+ }
+ 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
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect' ) {
+ $destTable = $this->tableName( $destTable );
+ $srcTable = $this->tableName( $srcTable );
+ $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ")" .
+ " SELECT " . implode( ',', $varMap ) .
+ " FROM $srcTable";
+ if ( $conds != '*' ) {
+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+ }
+ return $this->query( $sql, $fname );
+ }
}
+class DatabaseMysql extends Database {
+ # Inherit all
+}
+
#------------------------------------------------------------------------------
# Global functions
#------------------------------------------------------------------------------
@@ -520,11 +755,6 @@ function wfEmergencyAbort( &$conn, $error ) {
wfAbruptExit();
}
-function wfStrencode( $s )
-{
- return addslashes( $s );
-}
-
function wfLimitResult( $limit, $offset ) {
return " LIMIT ".(is_numeric($offset)?"{$offset},":"")."{$limit} ";
}