summaryrefslogtreecommitdiff
path: root/includes/db
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2013-12-08 09:55:49 +0100
committerPierre Schmitz <pierre@archlinux.de>2013-12-08 09:55:49 +0100
commit4ac9fa081a7c045f6a9f1cfc529d82423f485b2e (patch)
treeaf68743f2f4a47d13f2b0eb05f5c4aaf86d8ea37 /includes/db
parentaf4da56f1ad4d3ef7b06557bae365da2ea27a897 (diff)
Update to MediaWiki 1.22.0
Diffstat (limited to 'includes/db')
-rw-r--r--includes/db/ChronologyProtector.php106
-rw-r--r--includes/db/CloneDatabase.php18
-rw-r--r--includes/db/Database.php517
-rw-r--r--includes/db/DatabaseError.php202
-rw-r--r--includes/db/DatabaseMssql.php190
-rw-r--r--includes/db/DatabaseMysql.php933
-rw-r--r--includes/db/DatabaseMysqlBase.php1154
-rw-r--r--includes/db/DatabaseMysqli.php194
-rw-r--r--includes/db/DatabaseOracle.php92
-rw-r--r--includes/db/DatabasePostgres.php98
-rw-r--r--includes/db/DatabaseSqlite.php49
-rw-r--r--includes/db/DatabaseUtility.php3
-rw-r--r--includes/db/IORMRow.php24
-rw-r--r--includes/db/LBFactory.php94
-rw-r--r--includes/db/LBFactory_Multi.php6
-rw-r--r--includes/db/LoadBalancer.php104
-rw-r--r--includes/db/LoadMonitor.php27
-rw-r--r--includes/db/ORMRow.php182
-rw-r--r--includes/db/ORMTable.php211
19 files changed, 2683 insertions, 1521 deletions
diff --git a/includes/db/ChronologyProtector.php b/includes/db/ChronologyProtector.php
new file mode 100644
index 00000000..de5e72c3
--- /dev/null
+++ b/includes/db/ChronologyProtector.php
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Generator of database load balancing objects.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
+ * Kind of like Hawking's [[Chronology Protection Agency]].
+ */
+class ChronologyProtector {
+ /** @var Array (DB master name => position) */
+ protected $startupPositions = array();
+ /** @var Array (DB master name => position) */
+ protected $shutdownPositions = array();
+
+ protected $initialized = false; // bool; whether the session data was loaded
+
+ /**
+ * Initialise a LoadBalancer to give it appropriate chronology protection.
+ *
+ * If the session has a previous master position recorded, this will try to
+ * make sure that the next query to a slave of that master will see changes up
+ * to that position by delaying execution. The delay may timeout and allow stale
+ * data if no non-lagged slaves are available.
+ *
+ * @param $lb LoadBalancer
+ * @return void
+ */
+ public function initLB( LoadBalancer $lb ) {
+ if ( $lb->getServerCount() <= 1 ) {
+ return; // non-replicated setup
+ }
+ if ( !$this->initialized ) {
+ $this->initialized = true;
+ if ( isset( $_SESSION[__CLASS__] ) && is_array( $_SESSION[__CLASS__] ) ) {
+ $this->startupPositions = $_SESSION[__CLASS__];
+ }
+ }
+ $masterName = $lb->getServerName( 0 );
+ if ( !empty( $this->startupPositions[$masterName] ) ) {
+ $info = $lb->parentInfo();
+ $pos = $this->startupPositions[$masterName];
+ wfDebug( __METHOD__ . ": LB " . $info['id'] . " waiting for master pos $pos\n" );
+ $lb->waitFor( $pos );
+ }
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LoadBalancer is about to shut
+ * down. Saves replication positions.
+ *
+ * @param $lb LoadBalancer
+ * @return void
+ */
+ public function shutdownLB( LoadBalancer $lb ) {
+ if ( session_id() == '' || $lb->getServerCount() <= 1 ) {
+ return; // don't start a session; don't bother with non-replicated setups
+ }
+ $masterName = $lb->getServerName( 0 );
+ if ( isset( $this->shutdownPositions[$masterName] ) ) {
+ return; // already done
+ }
+ // Only save the position if writes have been done on the connection
+ $db = $lb->getAnyOpenConnection( 0 );
+ $info = $lb->parentInfo();
+ if ( !$db || !$db->doneWrites() ) {
+ wfDebug( __METHOD__ . ": LB {$info['id']}, no writes done\n" );
+ return;
+ }
+ $pos = $db->getMasterPos();
+ wfDebug( __METHOD__ . ": LB {$info['id']} has master pos $pos\n" );
+ $this->shutdownPositions[$masterName] = $pos;
+ }
+
+ /**
+ * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
+ * May commit chronology data to persistent storage.
+ *
+ * @return void
+ */
+ public function shutdown() {
+ if ( session_id() != '' && count( $this->shutdownPositions ) ) {
+ wfDebug( __METHOD__ . ": saving master pos for " .
+ count( $this->shutdownPositions ) . " master(s)\n" );
+ $_SESSION[__CLASS__] = $this->shutdownPositions;
+ }
+ }
+}
diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php
index 4e443741..819925cb 100644
--- a/includes/db/CloneDatabase.php
+++ b/includes/db/CloneDatabase.php
@@ -87,7 +87,7 @@ class CloneDatabase {
* Clone the table structure
*/
public function cloneTableStructure() {
- foreach( $this->tablesToClone as $tbl ) {
+ foreach ( $this->tablesToClone as $tbl ) {
# Clean up from previous aborted run. So that table escaping
# works correctly across DB engines, we need to change the pre-
# fix back and forth so tableName() works right.
@@ -98,7 +98,7 @@ class CloneDatabase {
self::changePrefix( $this->newTablePrefix );
$newTableName = $this->db->tableName( $tbl, 'raw' );
- if( $this->dropCurrentTables && !in_array( $this->db->getType(), array( 'postgres', 'oracle' ) ) ) {
+ if ( $this->dropCurrentTables && !in_array( $this->db->getType(), array( 'postgres', 'oracle' ) ) ) {
$this->db->dropTable( $tbl, __METHOD__ );
wfDebug( __METHOD__ . " dropping {$newTableName}\n", true );
//Dropping the oldTable because the prefix was changed
@@ -115,9 +115,9 @@ class CloneDatabase {
* @param bool $dropTables Optionally drop the tables we created
*/
public function destroy( $dropTables = false ) {
- if( $dropTables ) {
+ if ( $dropTables ) {
self::changePrefix( $this->newTablePrefix );
- foreach( $this->tablesToClone as $tbl ) {
+ foreach ( $this->tablesToClone as $tbl ) {
$this->db->dropTable( $tbl );
}
}
@@ -127,7 +127,7 @@ class CloneDatabase {
/**
* Change the table prefix on all open DB connections/
*
- * @param $prefix
+ * @param $prefix
* @return void
*/
public static function changePrefix( $prefix ) {
@@ -137,8 +137,8 @@ class CloneDatabase {
}
/**
- * @param $lb LoadBalancer
- * @param $prefix
+ * @param $lb LoadBalancer
+ * @param $prefix
* @return void
*/
public static function changeLBPrefix( $lb, $prefix ) {
@@ -146,8 +146,8 @@ class CloneDatabase {
}
/**
- * @param $db DatabaseBase
- * @param $prefix
+ * @param $db DatabaseBase
+ * @param $prefix
* @return void
*/
public static function changeDBPrefix( $db, $prefix ) {
diff --git a/includes/db/Database.php b/includes/db/Database.php
index 65a74abf..10645608 100644
--- a/includes/db/Database.php
+++ b/includes/db/Database.php
@@ -24,13 +24,6 @@
* @ingroup Database
*/
-/** Number of times to re-try an operation in case of deadlock */
-define( 'DEADLOCK_TRIES', 4 );
-/** Minimum time to wait before retry, in microseconds */
-define( 'DEADLOCK_DELAY_MIN', 500000 );
-/** Maximum time to wait before retry */
-define( 'DEADLOCK_DELAY_MAX', 1500000 );
-
/**
* Base interface for all DBMS-specific code. At a bare minimum, all of the
* following must be implemented to support MediaWiki
@@ -165,7 +158,7 @@ interface DatabaseType {
* @param string $fname Calling function name
* @return Mixed: Database-specific index description class or false if the index does not exist
*/
- function indexInfo( $table, $index, $fname = 'Database::indexInfo' );
+ function indexInfo( $table, $index, $fname = __METHOD__ );
/**
* Get the number of rows affected by the last write query
@@ -191,7 +184,7 @@ interface DatabaseType {
*
* @return string: wikitext of a link to the server software's web site
*/
- static function getSoftwareLink();
+ function getSoftwareLink();
/**
* A string describing the current software version, like from
@@ -212,10 +205,22 @@ interface DatabaseType {
}
/**
+ * Interface for classes that implement or wrap DatabaseBase
+ * @ingroup Database
+ */
+interface IDatabase {}
+
+/**
* Database abstraction object
* @ingroup Database
*/
-abstract class DatabaseBase implements DatabaseType {
+abstract class DatabaseBase implements IDatabase, DatabaseType {
+ /** Number of times to re-try an operation in case of deadlock */
+ const DEADLOCK_TRIES = 4;
+ /** Minimum time to wait before retry, in microseconds */
+ const DEADLOCK_DELAY_MIN = 500000;
+ /** Maximum time to wait before retry */
+ const DEADLOCK_DELAY_MAX = 1500000;
# ------------------------------------------------------------------------------
# Variables
@@ -230,14 +235,14 @@ abstract class DatabaseBase implements DatabaseType {
protected $mConn = null;
protected $mOpened = false;
- /**
- * @since 1.20
- * @var array of Closure
- */
+ /** @var callable[] */
protected $mTrxIdleCallbacks = array();
+ /** @var callable[] */
+ protected $mTrxPreCommitCallbacks = array();
protected $mTablePrefix;
protected $mFlags;
+ protected $mForeign;
protected $mTrxLevel = 0;
protected $mErrorCount = 0;
protected $mLBInfo = array();
@@ -282,6 +287,12 @@ abstract class DatabaseBase implements DatabaseType {
*/
protected $fileHandle = null;
+ /**
+ * @since 1.22
+ * @var Process cache of VIEWs names in the database
+ */
+ protected $allViews = null;
+
# ------------------------------------------------------------------------------
# Accessors
# ------------------------------------------------------------------------------
@@ -355,6 +366,8 @@ abstract class DatabaseBase implements DatabaseType {
* code should use lastErrno() and lastError() to handle the
* situation as appropriate.
*
+ * Do not use this function outside of the Database classes.
+ *
* @param $ignoreErrors bool|null
*
* @return bool The previous value of the flag.
@@ -553,12 +566,14 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Returns true if there is a transaction open with possible write
- * queries or transaction idle callbacks waiting on it to finish.
+ * queries or transaction pre-commit/idle callbacks waiting on it to finish.
*
* @return bool
*/
public function writesOrCallbacksPending() {
- return $this->mTrxLevel && ( $this->mTrxDoneWrites || $this->mTrxIdleCallbacks );
+ return $this->mTrxLevel && (
+ $this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
+ );
}
/**
@@ -575,7 +590,6 @@ abstract class DatabaseBase implements DatabaseType {
* @param $flag Integer: DBO_* constants from Defines.php:
* - DBO_DEBUG: output some debug info (same as debug())
* - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
- * - DBO_IGNORE: ignore errors (same as ignoreErrors())
* - DBO_TRX: automatically start transactions
* - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode
* and removes it in command line mode
@@ -584,7 +598,7 @@ abstract class DatabaseBase implements DatabaseType {
public function setFlag( $flag ) {
global $wgDebugDBTransactions;
$this->mFlags |= $flag;
- if ( ( $flag & DBO_TRX) & $wgDebugDBTransactions ) {
+ if ( ( $flag & DBO_TRX ) & $wgDebugDBTransactions ) {
wfDebug( "Implicit transactions are now disabled.\n" );
}
}
@@ -654,15 +668,28 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Constructor.
+ *
+ * FIXME: It is possible to construct a Database object with no associated
+ * connection object, by specifying no parameters to __construct(). This
+ * feature is deprecated and should be removed.
+ *
+ * FIXME: The long list of formal parameters here is not really appropriate
+ * for MySQL, and not at all appropriate for any other DBMS. It should be
+ * replaced by named parameters as in DatabaseBase::factory().
+ *
+ * DatabaseBase subclasses should not be constructed directly in external
+ * code. DatabaseBase::factory() should be used instead.
+ *
* @param string $server database server host
* @param string $user database user name
* @param string $password database user password
* @param string $dbName database name
* @param $flags
* @param string $tablePrefix database table prefixes. By default use the prefix gave in LocalSettings.php
+ * @param bool $foreign disable some operations specific to local databases
*/
function __construct( $server = false, $user = false, $password = false, $dbName = false,
- $flags = 0, $tablePrefix = 'get from global'
+ $flags = 0, $tablePrefix = 'get from global', $foreign = false
) {
global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions;
@@ -689,6 +716,8 @@ abstract class DatabaseBase implements DatabaseType {
$this->mTablePrefix = $tablePrefix;
}
+ $this->mForeign = $foreign;
+
if ( $user ) {
$this->open( $server, $user, $password, $dbName );
}
@@ -706,7 +735,7 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Given a DB type, construct the name of the appropriate child class of
* DatabaseBase. This is designed to replace all of the manual stuff like:
- * $class = 'Database' . ucfirst( strtolower( $type ) );
+ * $class = 'Database' . ucfirst( strtolower( $dbType ) );
* as well as validate against the canonical list of DB types we have
*
* This factory function is mostly useful for when you need to connect to a
@@ -714,7 +743,6 @@ abstract class DatabaseBase implements DatabaseType {
* an extension, et cetera). Do not use this to connect to the MediaWiki
* database. Example uses in core:
* @see LoadBalancer::reallyOpenConnection()
- * @see ExternalUser_MediaWiki::initFromCond()
* @see ForeignDBRepo::getMasterDB()
* @see WebInstaller_DBConnect::execute()
*
@@ -722,24 +750,55 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param string $dbType A possible DB type
* @param array $p An array of options to pass to the constructor.
- * Valid options are: host, user, password, dbname, flags, tablePrefix
+ * Valid options are: host, user, password, dbname, flags, tablePrefix, driver
* @return DatabaseBase subclass or null
*/
final public static function factory( $dbType, $p = array() ) {
$canonicalDBTypes = array(
- 'mysql', 'postgres', 'sqlite', 'oracle', 'mssql'
+ 'mysql' => array( 'mysqli', 'mysql' ),
+ 'postgres' => array(),
+ 'sqlite' => array(),
+ 'oracle' => array(),
+ 'mssql' => array(),
);
+
+ $driver = false;
$dbType = strtolower( $dbType );
- $class = 'Database' . ucfirst( $dbType );
+ if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
+ $possibleDrivers = $canonicalDBTypes[$dbType];
+ if ( !empty( $p['driver'] ) ) {
+ if ( in_array( $p['driver'], $possibleDrivers ) ) {
+ $driver = $p['driver'];
+ } else {
+ throw new MWException( __METHOD__ .
+ " cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
+ }
+ } else {
+ foreach ( $possibleDrivers as $posDriver ) {
+ if ( extension_loaded( $posDriver ) ) {
+ $driver = $posDriver;
+ break;
+ }
+ }
+ }
+ } else {
+ $driver = $dbType;
+ }
+ if ( $driver === false ) {
+ throw new MWException( __METHOD__ .
+ " no viable database extension found for type '$dbType'" );
+ }
- if( in_array( $dbType, $canonicalDBTypes ) || ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) ) {
+ $class = 'Database' . ucfirst( $driver );
+ if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
return new $class(
isset( $p['host'] ) ? $p['host'] : false,
isset( $p['user'] ) ? $p['user'] : false,
isset( $p['password'] ) ? $p['password'] : false,
isset( $p['dbname'] ) ? $p['dbname'] : false,
isset( $p['flags'] ) ? $p['flags'] : 0,
- isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global'
+ isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global',
+ isset( $p['foreign'] ) ? $p['foreign'] : false
);
} else {
return null;
@@ -772,8 +831,9 @@ abstract class DatabaseBase implements DatabaseType {
/**
* @param $errno
* @param $errstr
+ * @access private
*/
- protected function connectionErrorHandler( $errno, $errstr ) {
+ public function connectionErrorHandler( $errno, $errstr ) {
$this->mPHPError = $errstr;
}
@@ -870,23 +930,8 @@ abstract class DatabaseBase implements DatabaseType {
* @return boolean|ResultWrapper. true for a successful write query, ResultWrapper object
* for a successful read query, or false on failure if $tempIgnore set
*/
- public function query( $sql, $fname = '', $tempIgnore = false ) {
- $isMaster = !is_null( $this->getLBInfo( 'master' ) );
- if ( !Profiler::instance()->isStub() ) {
- # generalizeSQL will probably cut down the query to reasonable
- # logging size most of the time. The substr is really just a sanity check.
-
- if ( $isMaster ) {
- $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'DatabaseBase::query-master';
- } else {
- $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
- $totalProf = 'DatabaseBase::query';
- }
-
- wfProfileIn( $totalProf );
- wfProfileIn( $queryProf );
- }
+ public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+ global $wgUser, $wgDebugDBTransactions;
$this->mLastQuery = $sql;
if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) {
@@ -896,7 +941,6 @@ abstract class DatabaseBase implements DatabaseType {
}
# Add a comment for easy SHOW PROCESSLIST interpretation
- global $wgUser;
if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
$userName = $wgUser->getName();
if ( mb_strlen( $userName ) > 15 ) {
@@ -920,7 +964,6 @@ abstract class DatabaseBase implements DatabaseType {
# is really used by application
$sqlstart = substr( $sql, 0, 10 ); // very much worth it, benchmark certified(tm)
if ( strpos( $sqlstart, "SHOW " ) !== 0 && strpos( $sqlstart, "SET " ) !== 0 ) {
- global $wgDebugDBTransactions;
if ( $wgDebugDBTransactions ) {
wfDebug( "Implicit transaction start.\n" );
}
@@ -932,6 +975,22 @@ abstract class DatabaseBase implements DatabaseType {
# Keep track of whether the transaction has write queries pending
if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $this->isWriteQuery( $sql ) ) {
$this->mTrxDoneWrites = true;
+ Profiler::instance()->transactionWritingIn( $this->mServer, $this->mDBname );
+ }
+
+ $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+ if ( !Profiler::instance()->isStub() ) {
+ # generalizeSQL will probably cut down the query to reasonable
+ # logging size most of the time. The substr is really just a sanity check.
+ if ( $isMaster ) {
+ $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'DatabaseBase::query-master';
+ } else {
+ $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
+ $totalProf = 'DatabaseBase::query';
+ }
+ wfProfileIn( $totalProf );
+ wfProfileIn( $queryProf );
}
if ( $this->debug() ) {
@@ -945,10 +1004,6 @@ abstract class DatabaseBase implements DatabaseType {
wfDebug( "Query {$this->mDBname} ($cnt) ($master): $sqlx\n" );
}
- if ( istainted( $sql ) & TC_MYSQL ) {
- throw new MWException( 'Tainted query found' );
- }
-
$queryId = MWDebug::query( $sql, $fname, $isMaster );
# Do the query and handle errors
@@ -961,6 +1016,7 @@ abstract class DatabaseBase implements DatabaseType {
# Transaction is gone, like it or not
$this->mTrxLevel = 0;
$this->mTrxIdleCallbacks = array(); // cancel
+ $this->mTrxPreCommitCallbacks = array(); // cancel
wfDebug( "Connection lost, reconnecting...\n" );
if ( $this->ping() ) {
@@ -1091,17 +1147,22 @@ abstract class DatabaseBase implements DatabaseType {
* @return String
*/
protected function fillPreparedArg( $matches ) {
- switch( $matches[1] ) {
- case '\\?': return '?';
- case '\\!': return '!';
- case '\\&': return '&';
+ 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;
+ 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.' );
@@ -1117,7 +1178,8 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $res Mixed: A SQL result
*/
- public function freeResult( $res ) {}
+ public function freeResult( $res ) {
+ }
/**
* A SELECT wrapper which returns a single field from a single result row.
@@ -1136,9 +1198,9 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool|mixed The value from the field, or false on failure.
*/
- public function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField',
- $options = array() )
- {
+ public function selectField( $table, $var, $cond = '', $fname = __METHOD__,
+ $options = array()
+ ) {
if ( !is_array( $options ) ) {
$options = array( $options );
}
@@ -1236,7 +1298,7 @@ abstract class DatabaseBase implements DatabaseType {
$startOpts .= ' SQL_NO_CACHE';
}
- if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+ if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
$useIndex = $this->useIndexClause( $options['USE INDEX'] );
} else {
$useIndex = '';
@@ -1427,7 +1489,7 @@ abstract class DatabaseBase implements DatabaseType {
* DBQueryError exception will be thrown, except if the "ignore errors"
* option was set, in which case false will be returned.
*/
- public function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+ public function select( $table, $vars, $conds = '', $fname = __METHOD__,
$options = array(), $join_conds = array() ) {
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
@@ -1450,7 +1512,7 @@ abstract class DatabaseBase implements DatabaseType {
* @return string SQL query string.
* @see DatabaseBase::select()
*/
- public function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select',
+ public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
$options = array(), $join_conds = array() )
{
if ( is_array( $vars ) ) {
@@ -1458,28 +1520,26 @@ abstract class DatabaseBase implements DatabaseType {
}
$options = (array)$options;
+ $useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
+ ? $options['USE INDEX']
+ : array();
if ( is_array( $table ) ) {
- $useIndex = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
- ? $options['USE INDEX']
- : array();
- if ( count( $join_conds ) || count( $useIndex ) ) {
- $from = ' FROM ' .
- $this->tableNamesWithUseIndexOrJOIN( $table, $useIndex, $join_conds );
- } else {
- $from = ' FROM ' . implode( ',', $this->tableNamesWithAlias( $table ) );
- }
+ $from = ' FROM ' .
+ $this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
} elseif ( $table != '' ) {
if ( $table[0] == ' ' ) {
$from = ' FROM ' . $table;
} else {
- $from = ' FROM ' . $this->tableName( $table );
+ $from = ' FROM ' .
+ $this->tableNamesWithUseIndexOrJOIN( array( $table ), $useIndexes, array() );
}
} else {
$from = '';
}
- list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+ list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
+ $this->makeSelectOptions( $options );
if ( !empty( $conds ) ) {
if ( is_array( $conds ) ) {
@@ -1517,7 +1577,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return object|bool
*/
- public function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow',
+ public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
$options = array(), $join_conds = array() )
{
$options = (array)$options;
@@ -1558,7 +1618,7 @@ abstract class DatabaseBase implements DatabaseType {
* @return Integer: row count
*/
public function estimateRowCount( $table, $vars = '*', $conds = '',
- $fname = 'DatabaseBase::estimateRowCount', $options = array() )
+ $fname = __METHOD__, $options = array() )
{
$rows = 0;
$res = $this->select( $table, array( 'rowcount' => 'COUNT(*)' ), $conds, $fname, $options );
@@ -1582,19 +1642,20 @@ abstract class DatabaseBase implements DatabaseType {
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 = 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 );
+ $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 );
+ $sql = preg_replace( '/\s+/', ' ', $sql );
# All numbers => N
- $sql = preg_replace ( '/-?[0-9]+/s', 'N', $sql );
+ $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
+ $sql = preg_replace( '/-?\d+/s', 'N', $sql );
return $sql;
}
@@ -1607,7 +1668,7 @@ abstract class DatabaseBase implements DatabaseType {
* @param string $fname calling function name (optional)
* @return Boolean: whether $table has filed $field
*/
- public function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) {
+ public function fieldExists( $table, $field, $fname = __METHOD__ ) {
$info = $this->fieldInfo( $table, $field );
return (bool)$info;
@@ -1624,8 +1685,8 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool|null
*/
- public function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) {
- if( !$this->tableExists( $table ) ) {
+ public function indexExists( $table, $index, $fname = __METHOD__ ) {
+ if ( !$this->tableExists( $table ) ) {
return null;
}
@@ -1729,7 +1790,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @return bool
*/
- public function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) {
+ public function insert( $table, $a, $fname = __METHOD__, $options = array() ) {
# No rows to insert, easy just return now
if ( !count( $a ) ) {
return true;
@@ -1828,7 +1889,7 @@ abstract class DatabaseBase implements DatabaseType {
* - LOW_PRIORITY: MySQL-specific, see MySQL manual.
* @return Boolean
*/
- function update( $table, $values, $conds, $fname = 'DatabaseBase::update', $options = array() ) {
+ function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) {
$table = $this->tableName( $table );
$opts = $this->makeUpdateOptions( $options );
$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
@@ -2065,6 +2126,7 @@ abstract class DatabaseBase implements DatabaseType {
} else {
list( $table ) = $dbDetails;
if ( $wgSharedDB !== null # We have a shared database
+ && $this->mForeign == false # We're not working on a foreign database
&& !$this->isQuotedIdentifier( $table ) # Paranoia check to prevent shared tables listing '`table`'
&& in_array( $table, $wgSharedTables ) # A shared table is selected
) {
@@ -2257,11 +2319,11 @@ abstract class DatabaseBase implements DatabaseType {
}
// We can't separate explicit JOIN clauses with ',', use ' ' for those
- $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
- $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
+ $implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
+ $explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
// Compile our final table clause
- return implode( ' ', array( $straightJoins, $otherJoins ) );
+ return implode( ' ', array( $implicitJoins, $explicitJoins ) );
}
/**
@@ -2274,9 +2336,9 @@ abstract class DatabaseBase implements DatabaseType {
protected function indexName( $index ) {
// Backwards-compatibility hack
$renamed = array(
- 'ar_usertext_timestamp' => 'usertext_timestamp',
- 'un_user_id' => 'user_id',
- 'un_user_ip' => 'user_ip',
+ 'ar_usertext_timestamp' => 'usertext_timestamp',
+ 'un_user_id' => 'user_id',
+ 'un_user_ip' => 'user_ip',
);
if ( isset( $renamed[$index] ) ) {
@@ -2287,8 +2349,7 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
- * If it's a string, adds quotes and backslashes
- * Otherwise returns as-is
+ * Adds quotes and backslashes.
*
* @param $s string
*
@@ -2445,7 +2506,7 @@ abstract class DatabaseBase implements DatabaseType {
* a field name or an array of field names
* @param string $fname Calling function name (use __METHOD__) for logs/profiling
*/
- public function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) {
+ public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
$quotedTable = $this->tableName( $table );
if ( count( $rows ) == 0 ) {
@@ -2457,7 +2518,7 @@ abstract class DatabaseBase implements DatabaseType {
$rows = array( $rows );
}
- foreach( $rows as $row ) {
+ foreach ( $rows as $row ) {
# Delete rows which collide
if ( $uniqueIndexes ) {
$sql = "DELETE FROM $quotedTable WHERE ";
@@ -2488,7 +2549,7 @@ abstract class DatabaseBase implements DatabaseType {
}
# Now insert the row
- $this->insert( $table, $row );
+ $this->insert( $table, $row, $fname );
}
}
@@ -2527,6 +2588,92 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
+ * INSERT ON DUPLICATE KEY UPDATE wrapper, upserts an array into a table.
+ *
+ * This updates any conflicting rows (according to the unique indexes) using
+ * the provided SET clause and inserts any remaining (non-conflicted) rows.
+ *
+ * $rows may be either:
+ * - A single associative array. The array keys are the field names, and
+ * the values are the values to insert. The values are treated as data
+ * and will be quoted appropriately. If NULL is inserted, this will be
+ * converted to a database NULL.
+ * - An array with numeric keys, holding a list of associative arrays.
+ * This causes a multi-row INSERT on DBMSs that support it. The keys in
+ * each subarray must be identical to each other, and in the same order.
+ *
+ * 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.
+ *
+ * Usually throws a DBQueryError on failure. If errors are explicitly ignored,
+ * returns success.
+ *
+ * @param string $table Table name. This will be passed through DatabaseBase::tableName().
+ * @param array $rows A single row or list of rows to insert
+ * @param array $uniqueIndexes List of single field names or field name tuples
+ * @param array $set An array of values to SET. For each array element,
+ * the key gives the field name, and the value gives the data
+ * to set that field to. The data will be quoted by
+ * DatabaseBase::addQuotes().
+ * @param string $fname Calling function name (use __METHOD__) for logs/profiling
+ * @param array $options of options
+ *
+ * @return bool
+ * @since 1.22
+ */
+ public function upsert(
+ $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
+ ) {
+ if ( !count( $rows ) ) {
+ return true; // nothing to do
+ }
+ $rows = is_array( reset( $rows ) ) ? $rows : array( $rows );
+
+ if ( count( $uniqueIndexes ) ) {
+ $clauses = array(); // list WHERE clauses that each identify a single row
+ foreach ( $rows as $row ) {
+ foreach ( $uniqueIndexes as $index ) {
+ $index = is_array( $index ) ? $index : array( $index ); // columns
+ $rowKey = array(); // unique key to this row
+ foreach ( $index as $column ) {
+ $rowKey[$column] = $row[$column];
+ }
+ $clauses[] = $this->makeList( $rowKey, LIST_AND );
+ }
+ }
+ $where = array( $this->makeList( $clauses, LIST_OR ) );
+ } else {
+ $where = false;
+ }
+
+ $useTrx = !$this->mTrxLevel;
+ if ( $useTrx ) {
+ $this->begin( $fname );
+ }
+ try {
+ # Update any existing conflicting row(s)
+ if ( $where !== false ) {
+ $ok = $this->update( $table, $set, $where, $fname );
+ } else {
+ $ok = true;
+ }
+ # Now insert any non-conflicting row(s)
+ $ok = $this->insert( $table, $rows, $fname, array( 'IGNORE' ) ) && $ok;
+ } catch ( Exception $e ) {
+ if ( $useTrx ) {
+ $this->rollback( $fname );
+ }
+ throw $e;
+ }
+ if ( $useTrx ) {
+ $this->commit( $fname );
+ }
+
+ return $ok;
+ }
+
+ /**
* DELETE where the condition is a join.
*
* MySQL overrides this to use a multi-table DELETE syntax, in other databases
@@ -2548,7 +2695,7 @@ abstract class DatabaseBase implements DatabaseType {
* @throws DBUnexpectedError
*/
public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
- $fname = 'DatabaseBase::deleteJoin' )
+ $fname = __METHOD__ )
{
if ( !$conds ) {
throw new DBUnexpectedError( $this,
@@ -2614,7 +2761,7 @@ abstract class DatabaseBase implements DatabaseType {
* @throws DBUnexpectedError
* @return bool|ResultWrapper
*/
- public function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) {
+ public function delete( $table, $conds, $fname = __METHOD__ ) {
if ( !$conds ) {
throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
}
@@ -2623,7 +2770,10 @@ abstract class DatabaseBase implements DatabaseType {
$sql = "DELETE FROM $table";
if ( $conds != '*' ) {
- $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ if ( is_array( $conds ) ) {
+ $conds = $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= ' WHERE ' . $conds;
}
return $this->query( $sql, $fname );
@@ -2656,7 +2806,7 @@ abstract class DatabaseBase implements DatabaseType {
* @return ResultWrapper
*/
public function insertSelect( $destTable, $srcTable, $varMap, $conds,
- $fname = 'DatabaseBase::insertSelect',
+ $fname = __METHOD__,
$insertOptions = array(), $selectOptions = array() )
{
$destTable = $this->tableName( $destTable );
@@ -2848,7 +2998,7 @@ abstract class DatabaseBase implements DatabaseType {
$args = func_get_args();
$function = array_shift( $args );
$oldIgnore = $this->ignoreErrors( true );
- $tries = DEADLOCK_TRIES;
+ $tries = self::DEADLOCK_TRIES;
if ( is_array( $function ) ) {
$fname = $function[0];
@@ -2865,7 +3015,7 @@ abstract class DatabaseBase implements DatabaseType {
if ( $errno ) {
if ( $this->wasDeadlock() ) {
# Retry
- usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
+ usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
} else {
$this->reportQueryError( $error, $errno, $sql, $fname );
}
@@ -2955,24 +3105,45 @@ abstract class DatabaseBase implements DatabaseType {
/**
* Run an anonymous function as soon as there is no transaction pending.
* If there is a transaction and it is rolled back, then the callback is cancelled.
+ * Queries in the function will run in AUTO-COMMIT mode unless there are begin() calls.
* Callbacks must commit any transactions that they begin.
*
- * This is useful for updates to different systems or separate transactions are needed.
+ * This is useful for updates to different systems or when separate transactions are needed.
+ * For example, one might want to enqueue jobs into a system outside the database, but only
+ * after the database is updated so that the jobs will see the data when they actually run.
+ * It can also be used for updates that easily cause deadlocks if locks are held too long.
*
+ * @param callable $callback
* @since 1.20
+ */
+ final public function onTransactionIdle( $callback ) {
+ $this->mTrxIdleCallbacks[] = array( $callback, wfGetCaller() );
+ if ( !$this->mTrxLevel ) {
+ $this->runOnTransactionIdleCallbacks();
+ }
+ }
+
+ /**
+ * Run an anonymous function before the current transaction commits or now if there is none.
+ * If there is a transaction and it is rolled back, then the callback is cancelled.
+ * Callbacks must not start nor commit any transactions.
+ *
+ * This is useful for updates that easily cause deadlocks if locks are held too long
+ * but where atomicity is strongly desired for these updates and some related updates.
*
- * @param Closure $callback
+ * @param callable $callback
+ * @since 1.22
*/
- final public function onTransactionIdle( Closure $callback ) {
+ final public function onTransactionPreCommitOrIdle( $callback ) {
if ( $this->mTrxLevel ) {
- $this->mTrxIdleCallbacks[] = $callback;
+ $this->mTrxPreCommitCallbacks[] = array( $callback, wfGetCaller() );
} else {
- $callback();
+ $this->onTransactionIdle( $callback ); // this will trigger immediately
}
}
/**
- * Actually run the "on transaction idle" callbacks.
+ * Actually any "on transaction idle" callbacks.
*
* @since 1.20
*/
@@ -2985,8 +3156,9 @@ abstract class DatabaseBase implements DatabaseType {
$this->mTrxIdleCallbacks = array(); // recursion guard
foreach ( $callbacks as $callback ) {
try {
+ list( $phpCallback ) = $callback;
$this->clearFlag( DBO_TRX ); // make each query its own transaction
- $callback();
+ call_user_func( $phpCallback );
$this->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin()
} catch ( Exception $e ) {}
}
@@ -2998,6 +3170,29 @@ abstract class DatabaseBase implements DatabaseType {
}
/**
+ * Actually any "on transaction pre-commit" callbacks.
+ *
+ * @since 1.22
+ */
+ protected function runOnTransactionPreCommitCallbacks() {
+ $e = null; // last exception
+ do { // callbacks may add callbacks :)
+ $callbacks = $this->mTrxPreCommitCallbacks;
+ $this->mTrxPreCommitCallbacks = array(); // recursion guard
+ foreach ( $callbacks as $callback ) {
+ try {
+ list( $phpCallback ) = $callback;
+ call_user_func( $phpCallback );
+ } catch ( Exception $e ) {}
+ }
+ } while ( count( $this->mTrxPreCommitCallbacks ) );
+
+ if ( $e instanceof Exception ) {
+ throw $e; // re-throw any last exception
+ }
+ }
+
+ /**
* Begin a transaction. If a transaction is already in progress, that transaction will be committed before the
* new transaction is started.
*
@@ -3009,7 +3204,7 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $fname string
*/
- final public function begin( $fname = 'DatabaseBase::begin' ) {
+ final public function begin( $fname = __METHOD__ ) {
global $wgDebugDBTransactions;
if ( $this->mTrxLevel ) { // implicit commit
@@ -3025,11 +3220,16 @@ abstract class DatabaseBase implements DatabaseType {
// log it if $wgDebugDBTransactions is enabled.
if ( $this->mTrxDoneWrites && $wgDebugDBTransactions ) {
wfDebug( "$fname: Automatic transaction with writes in progress" .
- " (from {$this->mTrxFname}), performing implicit commit!\n" );
+ " (from {$this->mTrxFname}), performing implicit commit!\n"
+ );
}
}
+ $this->runOnTransactionPreCommitCallbacks();
$this->doCommit( $fname );
+ if ( $this->mTrxDoneWrites ) {
+ Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
+ }
$this->runOnTransactionIdleCallbacks();
}
@@ -3062,22 +3262,27 @@ abstract class DatabaseBase implements DatabaseType {
* This will silently break any ongoing explicit transaction. Only set the flush flag if you are sure
* that it is safe to ignore these warnings in your context.
*/
- final public function commit( $fname = 'DatabaseBase::commit', $flush = '' ) {
+ final public function commit( $fname = __METHOD__, $flush = '' ) {
if ( $flush != 'flush' ) {
if ( !$this->mTrxLevel ) {
wfWarn( "$fname: No transaction to commit, something got out of sync!" );
- } elseif( $this->mTrxAutomatic ) {
+ } elseif ( $this->mTrxAutomatic ) {
wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" );
}
} else {
if ( !$this->mTrxLevel ) {
return; // nothing to do
- } elseif( !$this->mTrxAutomatic ) {
+ } elseif ( !$this->mTrxAutomatic ) {
wfWarn( "$fname: Flushing an explicit transaction, getting out of sync!" );
}
}
+ $this->runOnTransactionPreCommitCallbacks();
$this->doCommit( $fname );
+ if ( $this->mTrxDoneWrites ) {
+ Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
+ }
+ $this->mTrxDoneWrites = false;
$this->runOnTransactionIdleCallbacks();
}
@@ -3102,12 +3307,17 @@ abstract class DatabaseBase implements DatabaseType {
*
* @param $fname string
*/
- final public function rollback( $fname = 'DatabaseBase::rollback' ) {
+ final public function rollback( $fname = __METHOD__ ) {
if ( !$this->mTrxLevel ) {
wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
}
$this->doRollback( $fname );
$this->mTrxIdleCallbacks = array(); // cancel
+ $this->mTrxPreCommitCallbacks = array(); // cancel
+ if ( $this->mTrxDoneWrites ) {
+ Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname );
+ }
+ $this->mTrxDoneWrites = false;
}
/**
@@ -3139,8 +3349,8 @@ abstract class DatabaseBase implements DatabaseType {
* @return Boolean: true if operation was successful
*/
public function duplicateTableStructure( $oldName, $newName, $temporary = false,
- $fname = 'DatabaseBase::duplicateTableStructure' )
- {
+ $fname = __METHOD__
+ ) {
throw new MWException(
'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
}
@@ -3152,11 +3362,45 @@ abstract class DatabaseBase implements DatabaseType {
* @param string $fname calling function name
* @throws MWException
*/
- function listTables( $prefix = null, $fname = 'DatabaseBase::listTables' ) {
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
}
/**
+ * Reset the views process cache set by listViews()
+ * @since 1.22
+ */
+ final public function clearViewsCache() {
+ $this->allViews = null;
+ }
+
+ /**
+ * Lists all the VIEWs in the database
+ *
+ * For caching purposes the list of all views should be stored in
+ * $this->allViews. The process cache can be cleared with clearViewsCache()
+ *
+ * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
+ * @param string $fname Name of calling function
+ * @throws MWException
+ * @since 1.22
+ */
+ public function listViews( $prefix = null, $fname = __METHOD__ ) {
+ throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
+ }
+
+ /**
+ * Differentiates between a TABLE and a VIEW
+ *
+ * @param $name string: Name of the database-structure to test.
+ * @throws MWException
+ * @since 1.22
+ */
+ public function isView( $name ) {
+ throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
+ }
+
+ /**
* Convert a timestamp in one of the formats accepted by wfTimestamp()
* to the format used for inserting into timestamp fields in this DBMS.
*
@@ -3285,7 +3529,8 @@ abstract class DatabaseBase implements DatabaseType {
* @param $options Array
* @return void
*/
- public function setSessionOptions( array $options ) {}
+ public function setSessionOptions( array $options ) {
+ }
/**
* Read and execute SQL commands from a file.
@@ -3375,7 +3620,7 @@ abstract class DatabaseBase implements DatabaseType {
* @return bool|string
*/
public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
- $fname = 'DatabaseBase::sourceStream', $inputCallback = false )
+ $fname = __METHOD__, $inputCallback = false )
{
$cmd = '';
@@ -3614,12 +3859,12 @@ abstract class DatabaseBase implements DatabaseType {
* @return bool|ResultWrapper
* @since 1.18
*/
- public function dropTable( $tableName, $fName = 'DatabaseBase::dropTable' ) {
- if( !$this->tableExists( $tableName, $fName ) ) {
+ public function dropTable( $tableName, $fName = __METHOD__ ) {
+ if ( !$this->tableExists( $tableName, $fName ) ) {
return false;
}
$sql = "DROP TABLE " . $this->tableName( $tableName );
- if( $this->cascadingDeletes() ) {
+ if ( $this->cascadingDeletes() ) {
$sql .= " CASCADE";
}
return $this->query( $sql, $fName );
@@ -3691,9 +3936,21 @@ abstract class DatabaseBase implements DatabaseType {
return (string)$this->mConn;
}
+ /**
+ * Run a few simple sanity checks
+ */
public function __destruct() {
- if ( count( $this->mTrxIdleCallbacks ) ) { // sanity
- trigger_error( "Transaction idle callbacks still pending." );
+ if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
+ trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
+ }
+ if ( count( $this->mTrxIdleCallbacks ) || count( $this->mTrxPreCommitCallbacks ) ) {
+ $callers = array();
+ foreach ( $this->mTrxIdleCallbacks as $callbackInfo ) {
+ $callers[] = $callbackInfo[1];
+
+ }
+ $callers = implode( ', ', $callers );
+ trigger_error( "DB transaction callbacks still pending (from $callers)." );
}
}
}
diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php
index 628a2afc..0875695f 100644
--- a/includes/db/DatabaseError.php
+++ b/includes/db/DatabaseError.php
@@ -43,24 +43,12 @@ class DBError extends MWException {
}
/**
- * @param $html string
- * @return string
- */
- protected function getContentMessage( $html ) {
- if ( $html ) {
- return nl2br( htmlspecialchars( $this->getMessage() ) );
- } else {
- return $this->getMessage();
- }
- }
-
- /**
* @return string
*/
function getText() {
global $wgShowDBErrorBacktrace;
- $s = $this->getContentMessage( false ) . "\n";
+ $s = $this->getTextContent() . "\n";
if ( $wgShowDBErrorBacktrace ) {
$s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
@@ -75,14 +63,29 @@ class DBError extends MWException {
function getHTML() {
global $wgShowDBErrorBacktrace;
- $s = $this->getContentMessage( true );
+ $s = $this->getHTMLContent();
if ( $wgShowDBErrorBacktrace ) {
- $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+ $s .= '<p>Backtrace:</p><p>' .
+ nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . '</p>';
}
return $s;
}
+
+ /**
+ * @return string
+ */
+ protected function getTextContent() {
+ return $this->getMessage();
+ }
+
+ /**
+ * @return string
+ */
+ protected function getHTMLContent() {
+ return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) . '</p>';
+ }
}
/**
@@ -96,11 +99,12 @@ class DBConnectionError extends DBError {
if ( trim( $error ) != '' ) {
$msg .= ": $error";
+ } elseif ( $db ) {
+ $error = $this->db->getServer();
}
- $this->error = $error;
-
parent::__construct( $db, $msg );
+ $this->error = $error;
}
/**
@@ -130,10 +134,10 @@ class DBConnectionError extends DBError {
}
/**
- * @return bool
+ * @return boolean
*/
- function getLogMessage() {
- # Don't send to the exception log
+ function isLoggable() {
+ // Don't send to the exception log, already in dberror log
return false;
}
@@ -141,39 +145,51 @@ class DBConnectionError extends DBError {
* @return string
*/
function getPageTitle() {
- global $wgSitename;
- return htmlspecialchars( $this->msg( 'dberr-header', "$wgSitename has a problem" ) );
+ return $this->msg( 'dberr-header', 'This wiki has a problem' );
}
/**
* @return string
*/
function getHTML() {
- global $wgShowDBErrorBacktrace;
+ global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors;
- $sorry = htmlspecialchars( $this->msg( 'dberr-problems', 'Sorry! This site is experiencing technical difficulties.' ) );
+ $sorry = htmlspecialchars( $this->msg( 'dberr-problems', "Sorry!\nThis site is experiencing technical difficulties." ) );
$again = htmlspecialchars( $this->msg( 'dberr-again', 'Try waiting a few minutes and reloading.' ) );
- $info = htmlspecialchars( $this->msg( 'dberr-info', '(Can\'t contact the database server: $1)' ) );
-
- # No database access
- MessageCache::singleton()->disable();
- if ( trim( $this->error ) == '' && $this->db ) {
- $this->error = $this->db->getProperty( 'mServer' );
+ if ( $wgShowHostnames || $wgShowSQLErrors ) {
+ $info = str_replace(
+ '$1', Html::element( 'span', array( 'dir' => 'ltr' ), $this->error ),
+ htmlspecialchars( $this->msg( 'dberr-info', '(Cannot contact the database server: $1)' ) )
+ );
+ } else {
+ $info = htmlspecialchars( $this->msg( 'dberr-info-hidden', '(Cannot contact the database server)' ) );
}
- $this->error = Html::element( 'span', array( 'dir' => 'ltr' ), $this->error );
+ # No database access
+ MessageCache::singleton()->disable();
- $noconnect = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
- $text = str_replace( '$1', $this->error, $noconnect );
+ $text = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
if ( $wgShowDBErrorBacktrace ) {
- $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+ $text .= '<p>Backtrace:</p><p>' .
+ nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . '</p>';
}
- $extra = $this->searchForm();
+ $text .= '<hr />';
+ $text .= $this->searchForm();
- return "$text<hr />$extra";
+ return $text;
+ }
+
+ protected function getTextContent() {
+ global $wgShowHostnames, $wgShowSQLErrors;
+
+ if ( $wgShowHostnames || $wgShowSQLErrors ) {
+ return $this->getMessage();
+ } else {
+ return 'DB connection error';
+ }
}
public function reportHTML() {
@@ -188,7 +204,7 @@ class DBConnectionError extends DBError {
# Hack: extend the body for error messages
$cache = str_replace( array( '</html>', '</body>' ), '', $cache );
# Add cache notice...
- $cache .= '<div style="color:red;font-size:150%;font-weight:bold;">'.
+ $cache .= '<div style="color:red;font-size:150%;font-weight:bold;">' .
htmlspecialchars( $this->msg( 'dberr-cachederror',
'This is a cached copy of the requested page, and may not be up to date. ' ) ) .
'</div>';
@@ -302,55 +318,107 @@ class DBQueryError extends DBError {
}
/**
- * @param $html string
+ * @return boolean
+ */
+ function isLoggable() {
+ // Don't send to the exception log, already in dberror log
+ return false;
+ }
+
+ /**
* @return string
*/
- function getContentMessage( $html ) {
- if ( $this->useMessageCache() ) {
- if ( $html ) {
- $msg = 'dberrortext';
- $sql = htmlspecialchars( $this->getSQL() );
- $fname = htmlspecialchars( $this->fname );
- $error = htmlspecialchars( $this->error );
- } else {
- $msg = 'dberrortextcl';
- $sql = $this->getSQL();
- $fname = $this->fname;
- $error = $this->error;
+ function getPageTitle() {
+ return $this->msg( 'databaseerror', 'Database error' );
+ }
+
+ /**
+ * @return string
+ */
+ protected function getHTMLContent() {
+ $key = 'databaseerror-text';
+ $s = Html::element( 'p', array(), $this->msg( $key, $this->getFallbackMessage( $key ) ) );
+
+ $details = $this->getTechnicalDetails();
+ if ( $details ) {
+ $s .= '<ul>';
+ foreach ( $details as $key => $detail ) {
+ $s .= str_replace(
+ '$1', call_user_func_array( 'Html::element', $detail ),
+ Html::element( 'li', array(),
+ $this->msg( $key, $this->getFallbackMessage( $key ) )
+ )
+ );
}
- return wfMessage( $msg )->rawParams( $sql, $fname, $this->errno, $error )->text();
- } else {
- return parent::getContentMessage( $html );
+ $s .= '</ul>';
}
+
+ return $s;
}
/**
- * @return String
+ * @return string
*/
- function getSQL() {
- global $wgShowSQLErrors;
+ protected function getTextContent() {
+ $key = 'databaseerror-textcl';
+ $s = $this->msg( $key, $this->getFallbackMessage( $key ) ) . "\n";
- if ( !$wgShowSQLErrors ) {
- return $this->msg( 'sqlhidden', 'SQL hidden' );
- } else {
- return $this->sql;
+ foreach ( $this->getTechnicalDetails() as $key => $detail ) {
+ $s .= $this->msg( $key, $this->getFallbackMessage( $key ), $detail[2] ) . "\n";
}
+
+ return $s;
}
/**
- * @return bool
+ * Make a list of technical details that can be shown to the user. This information can
+ * aid in debugging yet may be useful to an attacker trying to exploit a security weakness
+ * in the software or server configuration.
+ *
+ * Thus no such details are shown by default, though if $wgShowHostnames is true, only the
+ * full SQL query is hidden; in fact, the error message often does contain a hostname, and
+ * sites using this option probably don't care much about "security by obscurity". Of course,
+ * if $wgShowSQLErrors is true, the SQL query *is* shown.
+ *
+ * @return array: Keys are message keys; values are arrays of arguments for Html::element().
+ * Array will be empty if users are not allowed to see any of these details at all.
*/
- function getLogMessage() {
- # Don't send to the exception log
- return false;
+ protected function getTechnicalDetails() {
+ global $wgShowHostnames, $wgShowSQLErrors;
+
+ $attribs = array( 'dir' => 'ltr' );
+ $details = array();
+
+ if ( $wgShowSQLErrors ) {
+ $details['databaseerror-query'] = array(
+ 'div', array( 'class' => 'mw-code' ) + $attribs, $this->sql );
+ }
+
+ if ( $wgShowHostnames || $wgShowSQLErrors ) {
+ $errorMessage = $this->errno . ' ' . $this->error;
+ $details['databaseerror-function'] = array( 'code', $attribs, $this->fname );
+ $details['databaseerror-error'] = array( 'samp', $attribs, $errorMessage );
+ }
+
+ return $details;
}
/**
- * @return String
+ * @param string $key Message key
+ * @return string: English message text
*/
- function getPageTitle() {
- return $this->msg( 'databaseerror', 'Database error' );
+ private function getFallbackMessage( $key ) {
+ $messages = array(
+ 'databaseerror-text' => 'A database query error has occurred.
+This may indicate a bug in the software.',
+ 'databaseerror-textcl' => 'A database query error has occurred.',
+ 'databaseerror-query' => 'Query: $1',
+ 'databaseerror-function' => 'Function: $1',
+ 'databaseerror-error' => 'Error: $1',
+ );
+ return $messages[$key];
}
+
}
/**
diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php
index 6c45ffaf..240a097c 100644
--- a/includes/db/DatabaseMssql.php
+++ b/includes/db/DatabaseMssql.php
@@ -37,24 +37,31 @@ class DatabaseMssql extends DatabaseBase {
function cascadingDeletes() {
return true;
}
+
function cleanupTriggers() {
return true;
}
+
function strictIPs() {
return true;
}
+
function realTimestamps() {
return true;
}
+
function implicitGroupby() {
return false;
}
+
function implicitOrderby() {
return false;
}
+
function functionalIndexes() {
return true;
}
+
function unionSupportsOrderAndLimit() {
return false;
}
@@ -89,7 +96,7 @@ class DatabaseMssql extends DatabaseBase {
$connectionInfo = array();
- if( $dbName ) {
+ if ( $dbName ) {
$connectionInfo['Database'] = $dbName;
}
@@ -102,7 +109,7 @@ class DatabaseMssql extends DatabaseBase {
$ntAuthPassTest = strtolower( $password );
// Decide which auth scenerio to use
- if( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) {
+ if ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) {
// Don't add credentials to $connectionInfo
} else {
$connectionInfo['UID'] = $user;
@@ -148,7 +155,7 @@ class DatabaseMssql extends DatabaseBase {
}
// MSSQL doesn't have EXTRACT(epoch FROM XXX)
- if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
+ if ( preg_match( '#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) {
// This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970
$sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql );
}
@@ -202,9 +209,9 @@ class DatabaseMssql extends DatabaseBase {
$retErrors = sqlsrv_errors( SQLSRV_ERR_ALL );
if ( $retErrors != null ) {
foreach ( $retErrors as $arrError ) {
- $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n";
- $strRet .= "Error Code: " . $arrError[ 'code'] . "\n";
- $strRet .= "Message: " . $arrError[ 'message'] . "\n";
+ $strRet .= "SQLState: " . $arrError['SQLSTATE'] . "\n";
+ $strRet .= "Error Code: " . $arrError['code'] . "\n";
+ $strRet .= "Message: " . $arrError['message'] . "\n";
}
} else {
$strRet = "No errors found";
@@ -290,7 +297,7 @@ class DatabaseMssql extends DatabaseBase {
* (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
* @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure
*/
- function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() )
+ function select( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() )
{
$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
if ( isset( $options['EXPLAIN'] ) ) {
@@ -315,7 +322,7 @@ class DatabaseMssql extends DatabaseBase {
* (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
* @return string, the SQL text
*/
- function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) {
+ function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) {
if ( isset( $options['EXPLAIN'] ) ) {
unset( $options['EXPLAIN'] );
}
@@ -330,14 +337,16 @@ class DatabaseMssql extends DatabaseBase {
* Takes same arguments as Database::select()
* @return int
*/
- function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) {
+ function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) {
$options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx
$res = $this->select( $table, $vars, $conds, $fname, $options );
$rows = -1;
if ( $res ) {
$row = $this->fetchRow( $res );
- if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows'];
+ if ( isset( $row['EstimateRows'] ) ) {
+ $rows = $row['EstimateRows'];
+ }
}
return $rows;
}
@@ -347,7 +356,7 @@ class DatabaseMssql extends DatabaseBase {
* If errors are explicitly ignored, returns NULL on failure
* @return array|bool|null
*/
- function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) {
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
# This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the
# returned value except to check for the existance of indexes.
$sql = "sp_helpindex '" . $table . "'";
@@ -392,7 +401,7 @@ class DatabaseMssql extends DatabaseBase {
* @throws DBQueryError
* @return bool
*/
- function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) {
+ function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) {
# No rows to insert, easy just return now
if ( !count( $arrToInsert ) ) {
return true;
@@ -414,7 +423,7 @@ class DatabaseMssql extends DatabaseBase {
$identity = null;
$tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name
$res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" );
- if( $res && $res->numrows() ) {
+ if ( $res && $res->numrows() ) {
// There is an identity for this table.
$identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) );
}
@@ -427,11 +436,11 @@ class DatabaseMssql extends DatabaseBase {
$identityClause = '';
// if we have an identity column
- if( $identity ) {
+ if ( $identity ) {
// iterate through
- foreach ($a as $k => $v ) {
+ foreach ( $a as $k => $v ) {
if ( $k == $identity ) {
- if( !is_null($v) ) {
+ if ( !is_null( $v ) ) {
// there is a value being passed to us, we need to turn on and off inserted identity
$sqlPre = "SET IDENTITY_INSERT $table ON;";
$sqlPost = ";SET IDENTITY_INSERT $table OFF;";
@@ -501,7 +510,7 @@ class DatabaseMssql extends DatabaseBase {
} elseif ( $ret != null ) {
// remember number of rows affected
$this->mAffectedRows = sqlsrv_rows_affected( $ret );
- if ( !is_null($identity) ) {
+ if ( !is_null( $identity ) ) {
// then we want to get the identity column value we were assigned and save it off
$row = sqlsrv_fetch_object( $ret );
$this->mInsertId = $row->$identity;
@@ -530,7 +539,7 @@ class DatabaseMssql extends DatabaseBase {
* @throws DBQueryError
* @return null|ResultWrapper
*/
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect',
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
$insertOptions = array(), $selectOptions = array() ) {
$ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions );
@@ -645,7 +654,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* @return string wikitext of a link to the server software's web site
*/
- public static function getSoftwareLink() {
+ public function getSoftwareLink() {
return "[http://www.microsoft.com/sql/ MS SQL Server]";
}
@@ -661,11 +670,11 @@ class DatabaseMssql extends DatabaseBase {
return $version;
}
- function tableExists ( $table, $fname = __METHOD__, $schema = false ) {
+ function tableExists( $table, $fname = __METHOD__, $schema = false ) {
$res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables
WHERE table_type='BASE TABLE' AND table_name = '$table'" );
if ( $res === false ) {
- print( "Error in tableExists query: " . $this->getErrors() );
+ print "Error in tableExists query: " . $this->getErrors();
return false;
}
if ( sqlsrv_fetch( $res ) ) {
@@ -679,12 +688,12 @@ class DatabaseMssql extends DatabaseBase {
* Query whether a given column exists in the mediawiki schema
* @return bool
*/
- function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) {
+ function fieldExists( $table, $field, $fname = __METHOD__ ) {
$table = $this->tableName( $table );
$res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns
WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
if ( $res === false ) {
- print( "Error in fieldExists query: " . $this->getErrors() );
+ print "Error in fieldExists query: " . $this->getErrors();
return false;
}
if ( sqlsrv_fetch( $res ) ) {
@@ -699,7 +708,7 @@ class DatabaseMssql extends DatabaseBase {
$res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns
WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" );
if ( $res === false ) {
- print( "Error in fieldInfo query: " . $this->getErrors() );
+ print "Error in fieldInfo query: " . $this->getErrors();
return false;
}
$meta = $this->fetchRow( $res );
@@ -712,7 +721,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* Begin a transaction, committing any previously open transaction
*/
- protected function doBegin( $fname = 'DatabaseMssql::begin' ) {
+ protected function doBegin( $fname = __METHOD__ ) {
sqlsrv_begin_transaction( $this->mConn );
$this->mTrxLevel = 1;
}
@@ -720,7 +729,7 @@ class DatabaseMssql extends DatabaseBase {
/**
* End a transaction
*/
- protected function doCommit( $fname = 'DatabaseMssql::commit' ) {
+ protected function doCommit( $fname = __METHOD__ ) {
sqlsrv_commit( $this->mConn );
$this->mTrxLevel = 0;
}
@@ -729,7 +738,7 @@ class DatabaseMssql extends DatabaseBase {
* Rollback a transaction.
* No-op on non-transactional databases.
*/
- protected function doRollback( $fname = 'DatabaseMssql::rollback' ) {
+ protected function doRollback( $fname = __METHOD__ ) {
sqlsrv_rollback( $this->mConn );
$this->mTrxLevel = 0;
}
@@ -952,7 +961,7 @@ class DatabaseMssql extends DatabaseBase {
*/
class MssqlField implements Field {
private $name, $tablename, $default, $max_length, $nullable, $type;
- function __construct ( $info ) {
+ function __construct( $info ) {
$this->name = $info['COLUMN_NAME'];
$this->tablename = $info['TABLE_NAME'];
$this->default = $info['COLUMN_DEFAULT'];
@@ -1002,7 +1011,7 @@ class MssqlResult {
$rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC );
- foreach( $rows as $row ) {
+ foreach ( $rows as $row ) {
if ( $row !== null ) {
foreach ( $row as $k => $v ) {
if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object
@@ -1040,7 +1049,7 @@ class MssqlResult {
$arrNum[] = $value;
}
}
- switch( $mode ) {
+ switch ( $mode ) {
case SQLSRV_FETCH_ASSOC:
$ret = $this->mRows[$this->mCursor];
break;
@@ -1093,37 +1102,96 @@ class MssqlResult {
$i++;
}
// http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table
- switch( $intType ) {
- case SQLSRV_SQLTYPE_BIGINT: $strType = 'bigint'; break;
- case SQLSRV_SQLTYPE_BINARY: $strType = 'binary'; break;
- case SQLSRV_SQLTYPE_BIT: $strType = 'bit'; break;
- case SQLSRV_SQLTYPE_CHAR: $strType = 'char'; break;
- case SQLSRV_SQLTYPE_DATETIME: $strType = 'datetime'; break;
- case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break;
- case SQLSRV_SQLTYPE_FLOAT: $strType = 'float'; break;
- case SQLSRV_SQLTYPE_IMAGE: $strType = 'image'; break;
- case SQLSRV_SQLTYPE_INT: $strType = 'int'; break;
- case SQLSRV_SQLTYPE_MONEY: $strType = 'money'; break;
- case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break;
- case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break;
- case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break;
- // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break;
- case SQLSRV_SQLTYPE_NTEXT: $strType = 'ntext'; break;
- case SQLSRV_SQLTYPE_REAL: $strType = 'real'; break;
- case SQLSRV_SQLTYPE_SMALLDATETIME: $strType = 'smalldatetime'; break;
- case SQLSRV_SQLTYPE_SMALLINT: $strType = 'smallint'; break;
- case SQLSRV_SQLTYPE_SMALLMONEY: $strType = 'smallmoney'; break;
- case SQLSRV_SQLTYPE_TEXT: $strType = 'text'; break;
- case SQLSRV_SQLTYPE_TIMESTAMP: $strType = 'timestamp'; break;
- case SQLSRV_SQLTYPE_TINYINT: $strType = 'tinyint'; break;
- case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break;
- case SQLSRV_SQLTYPE_UDT: $strType = 'UDT'; break;
- case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break;
- // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break;
- case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break;
- // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break;
- case SQLSRV_SQLTYPE_XML: $strType = 'xml'; break;
- default: $strType = $intType;
+ switch ( $intType ) {
+ case SQLSRV_SQLTYPE_BIGINT:
+ $strType = 'bigint';
+ break;
+ case SQLSRV_SQLTYPE_BINARY:
+ $strType = 'binary';
+ break;
+ case SQLSRV_SQLTYPE_BIT:
+ $strType = 'bit';
+ break;
+ case SQLSRV_SQLTYPE_CHAR:
+ $strType = 'char';
+ break;
+ case SQLSRV_SQLTYPE_DATETIME:
+ $strType = 'datetime';
+ break;
+ case SQLSRV_SQLTYPE_DECIMAL: // ($precision, $scale)
+ $strType = 'decimal';
+ break;
+ case SQLSRV_SQLTYPE_FLOAT:
+ $strType = 'float';
+ break;
+ case SQLSRV_SQLTYPE_IMAGE:
+ $strType = 'image';
+ break;
+ case SQLSRV_SQLTYPE_INT:
+ $strType = 'int';
+ break;
+ case SQLSRV_SQLTYPE_MONEY:
+ $strType = 'money';
+ break;
+ case SQLSRV_SQLTYPE_NCHAR: // ($charCount):
+ $strType = 'nchar';
+ break;
+ case SQLSRV_SQLTYPE_NUMERIC: // ($precision, $scale):
+ $strType = 'numeric';
+ break;
+ case SQLSRV_SQLTYPE_NVARCHAR: // ($charCount)
+ $strType = 'nvarchar';
+ break;
+ // case SQLSRV_SQLTYPE_NVARCHAR('max'):
+ // $strType = 'nvarchar(MAX)';
+ // break;
+ case SQLSRV_SQLTYPE_NTEXT:
+ $strType = 'ntext';
+ break;
+ case SQLSRV_SQLTYPE_REAL:
+ $strType = 'real';
+ break;
+ case SQLSRV_SQLTYPE_SMALLDATETIME:
+ $strType = 'smalldatetime';
+ break;
+ case SQLSRV_SQLTYPE_SMALLINT:
+ $strType = 'smallint';
+ break;
+ case SQLSRV_SQLTYPE_SMALLMONEY:
+ $strType = 'smallmoney';
+ break;
+ case SQLSRV_SQLTYPE_TEXT:
+ $strType = 'text';
+ break;
+ case SQLSRV_SQLTYPE_TIMESTAMP:
+ $strType = 'timestamp';
+ break;
+ case SQLSRV_SQLTYPE_TINYINT:
+ $strType = 'tinyint';
+ break;
+ case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER:
+ $strType = 'uniqueidentifier';
+ break;
+ case SQLSRV_SQLTYPE_UDT:
+ $strType = 'UDT';
+ break;
+ case SQLSRV_SQLTYPE_VARBINARY: // ($byteCount)
+ $strType = 'varbinary';
+ break;
+ // case SQLSRV_SQLTYPE_VARBINARY('max'):
+ // $strType = 'varbinary(MAX)';
+ // break;
+ case SQLSRV_SQLTYPE_VARCHAR: // ($charCount)
+ $strType = 'varchar';
+ break;
+ // case SQLSRV_SQLTYPE_VARCHAR('max'):
+ // $strType = 'varchar(MAX)';
+ // break;
+ case SQLSRV_SQLTYPE_XML:
+ $strType = 'xml';
+ break;
+ default:
+ $strType = $intType;
}
return $strType;
}
diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php
index 27aae188..956bb694 100644
--- a/includes/db/DatabaseMysql.php
+++ b/includes/db/DatabaseMysql.php
@@ -22,27 +22,19 @@
*/
/**
- * Database abstraction object for mySQL
- * Inherit all methods and properties of Database::Database()
+ * Database abstraction object for PHP extension mysql.
*
* @ingroup Database
* @see Database
*/
-class DatabaseMysql extends DatabaseBase {
-
- /**
- * @return string
- */
- function getType() {
- return 'mysql';
- }
+class DatabaseMysql extends DatabaseMysqlBase {
/**
* @param $sql string
* @return resource
*/
protected function doQuery( $sql ) {
- if( $this->bufferResults() ) {
+ if ( $this->bufferResults() ) {
$ret = mysql_query( $sql, $this->mConn );
} else {
$ret = mysql_unbuffered_query( $sql, $this->mConn );
@@ -50,39 +42,13 @@ class DatabaseMysql extends DatabaseBase {
return $ret;
}
- /**
- * @param $server string
- * @param $user string
- * @param $password string
- * @param $dbName string
- * @return bool
- * @throws DBConnectionError
- */
- function open( $server, $user, $password, $dbName ) {
- global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode;
- wfProfileIn( __METHOD__ );
-
- # Load mysql.so if we don't have it
- wfDl( 'mysql' );
-
+ protected function mysqlConnect( $realServer ) {
# Fail now
# Otherwise we get a suppressed fatal error, which is very hard to track down
- if ( !function_exists( 'mysql_connect' ) ) {
+ if ( !extension_loaded( 'mysql' ) ) {
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;
- $this->mPassword = $password;
- $this->mDBname = $dbName;
-
$connFlags = 0;
if ( $this->mFlags & DBO_SSL ) {
$connFlags |= MYSQL_CLIENT_SSL;
@@ -91,81 +57,27 @@ class DatabaseMysql extends DatabaseBase {
$connFlags |= MYSQL_CLIENT_COMPRESS;
}
- wfProfileIn( "dbconnect-$server" );
-
- # The kernel's default SYN retransmission period is far too slow for us,
- # so we use a short timeout plus a manual retry. Retrying means that a small
- # but finite rate of SYN packet loss won't cause user-visible errors.
- $this->mConn = false;
if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
$numAttempts = 2;
} else {
$numAttempts = 1;
}
- $this->installErrorHandler();
- for ( $i = 0; $i < $numAttempts && !$this->mConn; $i++ ) {
+
+ $conn = false;
+
+ for ( $i = 0; $i < $numAttempts && !$conn; $i++ ) {
if ( $i > 1 ) {
usleep( 1000 );
}
if ( $this->mFlags & DBO_PERSISTENT ) {
- $this->mConn = mysql_pconnect( $realServer, $user, $password, $connFlags );
+ $conn = mysql_pconnect( $realServer, $this->mUser, $this->mPassword, $connFlags );
} else {
# Create a new connection...
- $this->mConn = mysql_connect( $realServer, $user, $password, true, $connFlags );
- }
- #if ( $this->mConn === false ) {
- #$iplus = $i + 1;
- #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n");
- #}
- }
- $error = $this->restoreErrorHandler();
-
- wfProfileOut( "dbconnect-$server" );
-
- # Always log connection errors
- if ( !$this->mConn ) {
- if ( !$error ) {
- $error = $this->lastError();
- }
- wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
- wfDebug( "DB connection error\n" .
- "Server: $server, User: $user, Password: " .
- substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
-
- wfProfileOut( __METHOD__ );
- return $this->reportConnectionError( $error );
- }
-
- if ( $dbName != '' ) {
- wfSuppressWarnings();
- $success = mysql_select_db( $dbName, $this->mConn );
- wfRestoreWarnings();
- if ( !$success ) {
- wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" );
- wfDebug( "Error selecting database $dbName on server {$this->mServer} " .
- "from client host " . wfHostname() . "\n" );
-
- wfProfileOut( __METHOD__ );
- return $this->reportConnectionError( "Error selecting database $dbName" );
+ $conn = mysql_connect( $realServer, $this->mUser, $this->mPassword, true, $connFlags );
}
}
- // Tell the server we're communicating with it in UTF-8.
- // This may engage various charset conversions.
- if( $wgDBmysql5 ) {
- $this->query( 'SET NAMES utf8', __METHOD__ );
- } else {
- $this->query( 'SET NAMES binary', __METHOD__ );
- }
- // Set SQL mode, default is turning them all off, can be overridden or skipped with null
- if ( is_string( $wgSQLMode ) ) {
- $mode = $this->addQuotes( $wgSQLMode );
- $this->query( "SET sql_mode = $mode", __METHOD__ );
- }
-
- $this->mOpened = true;
- wfProfileOut( __METHOD__ );
- return true;
+ return $conn;
}
/**
@@ -176,113 +88,6 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @param $res ResultWrapper
- * @throws DBUnexpectedError
- */
- function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $ok = mysql_free_result( $res );
- wfRestoreWarnings();
- if ( !$ok ) {
- throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
- }
- }
-
- /**
- * @param $res ResultWrapper
- * @return object|bool
- * @throws DBUnexpectedError
- */
- function fetchObject( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $row = mysql_fetch_object( $res );
- wfRestoreWarnings();
-
- $errno = $this->lastErrno();
- // Unfortunately, mysql_fetch_object does not reset the last errno.
- // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
- // these are the only errors mysql_fetch_object can cause.
- // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
- if( $errno == 2000 || $errno == 2013 ) {
- throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
-
- /**
- * @param $res ResultWrapper
- * @return array|bool
- * @throws DBUnexpectedError
- */
- function fetchRow( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $row = mysql_fetch_array( $res );
- wfRestoreWarnings();
-
- $errno = $this->lastErrno();
- // Unfortunately, mysql_fetch_array does not reset the last errno.
- // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
- // these are the only errors mysql_fetch_object can cause.
- // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
- if( $errno == 2000 || $errno == 2013 ) {
- throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
- }
- return $row;
- }
-
- /**
- * @throws DBUnexpectedError
- * @param $res ResultWrapper
- * @return int
- */
- function numRows( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- wfSuppressWarnings();
- $n = mysql_num_rows( $res );
- wfRestoreWarnings();
- // Unfortunately, mysql_num_rows does not reset the last errno.
- // We are not checking for any errors here, since
- // these are no errors mysql_num_rows can cause.
- // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
- // See https://bugzilla.wikimedia.org/42430
- return $n;
- }
-
- /**
- * @param $res ResultWrapper
- * @return int
- */
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_num_fields( $res );
- }
-
- /**
- * @param $res ResultWrapper
- * @param $n string
- * @return string
- */
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_field_name( $res, $n );
- }
-
- /**
* @return int
*/
function insertId() {
@@ -290,18 +95,6 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @param $res ResultWrapper
- * @param $row
- * @return bool
- */
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_data_seek( $res, $row );
- }
-
- /**
* @return int
*/
function lastErrno() {
@@ -313,27 +106,6 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @return string
- */
- 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 {
- $error = mysql_error();
- }
- if( $error ) {
- $error .= ' (' . $this->mServer . ')';
- }
- return $error;
- }
-
- /**
* @return int
*/
function affectedRows() {
@@ -341,100 +113,6 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @param $table string
- * @param $uniqueIndexes
- * @param $rows array
- * @param $fname string
- * @return ResultWrapper
- */
- function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMysql::replace' ) {
- return $this->nativeReplace( $table, $rows, $fname );
- }
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
- * Takes same arguments as Database::select()
- *
- * @param $table string|array
- * @param $vars string|array
- * @param $conds string|array
- * @param $fname string
- * @param $options string|array
- * @return int
- */
- public function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMysql::estimateRowCount', $options = array() ) {
- $options['EXPLAIN'] = true;
- $res = $this->select( $table, $vars, $conds, $fname, $options );
- if ( $res === false ) {
- return false;
- }
- if ( !$this->numRows( $res ) ) {
- return 0;
- }
-
- $rows = 1;
- foreach ( $res as $plan ) {
- $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
- }
- return $rows;
- }
-
- /**
- * @param $table string
- * @param $field string
- * @return bool|MySQLField
- */
- function fieldInfo( $table, $field ) {
- $table = $this->tableName( $table );
- $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
- if ( !$res ) {
- return false;
- }
- $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 );
- }
- }
- return false;
- }
-
- /**
- * Get information about an index into an object
- * Returns false if the index does not exist
- *
- * @param $table string
- * @param $index string
- * @param $fname string
- * @return bool|array|null False or null on failure
- */
- function indexInfo( $table, $index, $fname = 'DatabaseMysql::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 );
- $index = $this->indexName( $index );
-
- $sql = 'SHOW INDEX FROM ' . $table;
- $res = $this->query( $sql, $fname );
-
- if ( !$res ) {
- return null;
- }
-
- $result = array();
-
- foreach ( $res as $row ) {
- if ( $row->Key_name == $index ) {
- $result[] = $row;
- }
- }
- return empty( $result ) ? false : $result;
- }
-
- /**
* @param $db
* @return bool
*/
@@ -444,596 +122,53 @@ class DatabaseMysql extends DatabaseBase {
}
/**
- * @param $s string
- *
- * @return string
- */
- function strencode( $s ) {
- $sQuoted = mysql_real_escape_string( $s, $this->mConn );
-
- if( $sQuoted === false ) {
- $this->ping();
- $sQuoted = mysql_real_escape_string( $s, $this->mConn );
- }
- return $sQuoted;
- }
-
- /**
- * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
- *
- * @param $s string
- *
- * @return string
- */
- public function addIdentifierQuotes( $s ) {
- return "`" . $this->strencode( $s ) . "`";
- }
-
- /**
- * @param $name string
- * @return bool
- */
- public function isQuotedIdentifier( $name ) {
- return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
- }
-
- /**
- * @return bool
- */
- function ping() {
- $ping = mysql_ping( $this->mConn );
- if ( $ping ) {
- return true;
- }
-
- mysql_close( $this->mConn );
- $this->mOpened = false;
- $this->mConn = false;
- $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
- return true;
- }
-
- /**
- * Returns slave lag.
- *
- * This will do a SHOW SLAVE STATUS
- *
- * @return int
- */
- function getLag() {
- if ( !is_null( $this->mFakeSlaveLag ) ) {
- wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
- return $this->mFakeSlaveLag;
- }
-
- return $this->getLagFromSlaveStatus();
- }
-
- /**
- * @return bool|int
- */
- function getLagFromSlaveStatus() {
- $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
- if ( !$res ) {
- return false;
- }
- $row = $res->fetchObject();
- if ( !$row ) {
- return false;
- }
- if ( strval( $row->Seconds_Behind_Master ) === '' ) {
- return false;
- } else {
- return intval( $row->Seconds_Behind_Master );
- }
- }
-
- /**
- * @deprecated in 1.19, use getLagFromSlaveStatus
- *
- * @return bool|int
- */
- function getLagFromProcesslist() {
- wfDeprecated( __METHOD__, '1.19' );
- $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ );
- if( !$res ) {
- return false;
- }
- # Find slave SQL thread
- foreach( $res as $row ) {
- /* 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' &&
- $row->State != 'Waiting to reconnect after a failed master event read' &&
- $row->State != 'Reconnecting after a failed master event read' &&
- $row->State != 'Registering slave on master'
- ) {
- # This is it, return the time (except -ve)
- if ( $row->Time > 0x7fffffff ) {
- return false;
- } else {
- return $row->Time;
- }
- }
- }
- return false;
- }
-
- /**
- * Wait for the slave to catch up to a given master position.
- *
- * @param $pos DBMasterPos object
- * @param $timeout Integer: the maximum number of seconds to wait for synchronisation
- * @return bool|string
- */
- function masterPosWait( DBMasterPos $pos, $timeout ) {
- $fname = 'DatabaseBase::masterPosWait';
- wfProfileIn( $fname );
-
- # Commit any open transactions
- if ( $this->mTrxLevel ) {
- $this->commit( __METHOD__ );
- }
-
- if ( !is_null( $this->mFakeSlaveLag ) ) {
- $status = parent::masterPosWait( $pos, $timeout );
- wfProfileOut( $fname );
- return $status;
- }
-
- # 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 ) ) {
- wfProfileOut( $fname );
- return $row[0];
- }
- wfProfileOut( $fname );
- return false;
- }
-
- /**
- * Get the position of the master from SHOW SLAVE STATUS
- *
- * @return MySQLMasterPos|bool
- */
- function getSlavePos() {
- if ( !is_null( $this->mFakeSlaveLag ) ) {
- return parent::getSlavePos();
- }
-
- $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' );
- $row = $this->fetchObject( $res );
-
- if ( $row ) {
- $pos = isset( $row->Exec_master_log_pos ) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos;
- return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
- } else {
- return false;
- }
- }
-
- /**
- * Get the position of the master from SHOW MASTER STATUS
- *
- * @return MySQLMasterPos|bool
- */
- function getMasterPos() {
- if ( $this->mFakeMaster ) {
- return parent::getMasterPos();
- }
-
- $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' );
- $row = $this->fetchObject( $res );
-
- if ( $row ) {
- return new MySQLMasterPos( $row->File, $row->Position );
- } else {
- return false;
- }
- }
-
- /**
* @return string
*/
function getServerVersion() {
return mysql_get_server_info( $this->mConn );
}
- /**
- * @param $index
- * @return string
- */
- function useIndexClause( $index ) {
- return "FORCE INDEX (" . $this->indexName( $index ) . ")";
- }
-
- /**
- * @return string
- */
- function lowPriorityOption() {
- return 'LOW_PRIORITY';
+ protected function mysqlFreeResult( $res ) {
+ return mysql_free_result( $res );
}
- /**
- * @return string
- */
- public static function getSoftwareLink() {
- return '[http://www.mysql.com/ MySQL]';
- }
-
- /**
- * @param $options array
- */
- public function setSessionOptions( array $options ) {
- if ( isset( $options['connTimeout'] ) ) {
- $timeout = (int)$options['connTimeout'];
- $this->query( "SET net_read_timeout=$timeout" );
- $this->query( "SET net_write_timeout=$timeout" );
- }
+ protected function mysqlFetchObject( $res ) {
+ return mysql_fetch_object( $res );
}
- public function streamStatementEnd( &$sql, &$newLine ) {
- if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
- preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
- $this->delimiter = $m[1];
- $newLine = '';
- }
- return parent::streamStatementEnd( $sql, $newLine );
+ protected function mysqlFetchArray( $res ) {
+ return mysql_fetch_array( $res );
}
- /**
- * Check to see if a named lock is available. This is non-blocking.
- *
- * @param string $lockName name of lock to poll
- * @param string $method name of method calling us
- * @return Boolean
- * @since 1.20
- */
- public function lockIsFree( $lockName, $method ) {
- $lockName = $this->addQuotes( $lockName );
- $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method );
- $row = $this->fetchObject( $result );
- return ( $row->lockstatus == 1 );
+ protected function mysqlNumRows( $res ) {
+ return mysql_num_rows( $res );
}
- /**
- * @param $lockName string
- * @param $method string
- * @param $timeout int
- * @return bool
- */
- public function lock( $lockName, $method, $timeout = 5 ) {
- $lockName = $this->addQuotes( $lockName );
- $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method );
- $row = $this->fetchObject( $result );
-
- if( $row->lockstatus == 1 ) {
- return true;
- } else {
- wfDebug( __METHOD__ . " failed to acquire lock\n" );
- return false;
- }
- }
-
- /**
- * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
- * @param $lockName string
- * @param $method string
- * @return bool
- */
- public function unlock( $lockName, $method ) {
- $lockName = $this->addQuotes( $lockName );
- $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method );
- $row = $this->fetchObject( $result );
- return ( $row->lockstatus == 1 );
- }
-
- /**
- * @param $read array
- * @param $write array
- * @param $method string
- * @param $lowPriority bool
- * @return bool
- */
- public function lockTables( $read, $write, $method, $lowPriority = true ) {
- $items = array();
-
- foreach( $write as $table ) {
- $tbl = $this->tableName( $table ) .
- ( $lowPriority ? ' LOW_PRIORITY' : '' ) .
- ' WRITE';
- $items[] = $tbl;
- }
- foreach( $read as $table ) {
- $items[] = $this->tableName( $table ) . ' READ';
- }
- $sql = "LOCK TABLES " . implode( ',', $items );
- $this->query( $sql, $method );
- return true;
- }
-
- /**
- * @param $method string
- * @return bool
- */
- public function unlockTables( $method ) {
- $this->query( "UNLOCK TABLES", $method );
- return true;
- }
-
- /**
- * Get search engine class. All subclasses of this
- * need to implement this if they wish to use searching.
- *
- * @return String
- */
- public function getSearchEngine() {
- return 'SearchMySQL';
- }
-
- /**
- * @param bool $value
- * @return mixed
- */
- public function setBigSelects( $value = true ) {
- if ( $value === 'default' ) {
- if ( $this->mDefaultBigSelects === null ) {
- # Function hasn't been called before so it must already be set to the default
- return;
- } else {
- $value = $this->mDefaultBigSelects;
- }
- } elseif ( $this->mDefaultBigSelects === null ) {
- $this->mDefaultBigSelects = (bool)$this->selectField( false, '@@sql_big_selects' );
- }
- $encValue = $value ? '1' : '0';
- $this->query( "SET sql_big_selects=$encValue", __METHOD__ );
- }
-
- /**
- * DELETE where the condition is a join. MySql uses multi-table deletes.
- * @param $delTable string
- * @param $joinTable string
- * @param $delVar string
- * @param $joinVar string
- * @param $conds array|string
- * @param bool|string $fname bool
- * @throws DBUnexpectedError
- * @return bool|ResultWrapper
- */
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) {
- if ( !$conds ) {
- throw new DBUnexpectedError( $this, 'DatabaseBase::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 );
- }
-
- /**
- * Determines how long the server has been up
- *
- * @return int
- */
- function getServerUptime() {
- $vars = $this->getMysqlStatus( 'Uptime' );
- return (int)$vars['Uptime'];
- }
-
- /**
- * Determines if the last failure was due to a deadlock
- *
- * @return bool
- */
- function wasDeadlock() {
- return $this->lastErrno() == 1213;
- }
-
- /**
- * Determines if the last failure was due to a lock timeout
- *
- * @return bool
- */
- function wasLockTimeout() {
- return $this->lastErrno() == 1205;
- }
-
- /**
- * Determines if the last query error was something that should be dealt
- * with by pinging the connection and reissuing the query
- *
- * @return bool
- */
- function wasErrorReissuable() {
- return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
- }
-
- /**
- * Determines if the last failure was due to the database being read-only.
- *
- * @return bool
- */
- function wasReadOnlyError() {
- return $this->lastErrno() == 1223 ||
- ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
- }
-
- /**
- * @param $oldName
- * @param $newName
- * @param $temporary bool
- * @param $fname string
- */
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseMysql::duplicateTableStructure' ) {
- $tmp = $temporary ? 'TEMPORARY ' : '';
- $newName = $this->addIdentifierQuotes( $newName );
- $oldName = $this->addIdentifierQuotes( $oldName );
- $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
- $this->query( $query, $fname );
- }
-
- /**
- * List all tables on the database
- *
- * @param string $prefix Only show tables with this prefix, e.g. mw_
- * @param string $fname calling function name
- * @return array
- */
- function listTables( $prefix = null, $fname = 'DatabaseMysql::listTables' ) {
- $result = $this->query( "SHOW TABLES", $fname);
-
- $endArray = array();
-
- foreach( $result as $table ) {
- $vars = get_object_vars( $table );
- $table = array_pop( $vars );
-
- if( !$prefix || strpos( $table, $prefix ) === 0 ) {
- $endArray[] = $table;
- }
- }
-
- return $endArray;
- }
-
- /**
- * @param $tableName
- * @param $fName string
- * @return bool|ResultWrapper
- */
- public function dropTable( $tableName, $fName = 'DatabaseMysql::dropTable' ) {
- if( !$this->tableExists( $tableName, $fName ) ) {
- return false;
- }
- return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
- }
-
- /**
- * @return array
- */
- protected function getDefaultSchemaVars() {
- $vars = parent::getDefaultSchemaVars();
- $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
- $vars['wgDBTableOptions'] = str_replace( 'CHARSET=mysql4', 'CHARSET=binary', $vars['wgDBTableOptions'] );
- return $vars;
- }
-
- /**
- * Get status information from SHOW STATUS in an associative array
- *
- * @param $which string
- * @return array
- */
- function getMysqlStatus( $which = "%" ) {
- $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
- $status = array();
-
- foreach ( $res as $row ) {
- $status[$row->Variable_name] = $row->Value;
- }
-
- return $status;
- }
-
-}
-
-/**
- * Utility class.
- * @ingroup Database
- */
-class MySQLField implements Field {
- 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 string
- */
- function name() {
- return $this->name;
- }
-
- /**
- * @return string
- */
- function tableName() {
- return $this->tableName;
- }
-
- /**
- * @return string
- */
- function type() {
- return $this->type;
+ protected function mysqlNumFields( $res ) {
+ return mysql_num_fields( $res );
}
- /**
- * @return bool
- */
- function isNullable() {
- return $this->nullable;
+ protected function mysqlFetchField( $res, $n ) {
+ return mysql_fetch_field( $res, $n );
}
- function defaultValue() {
- return $this->default;
+ protected function mysqlFieldName( $res, $n ) {
+ return mysql_field_name( $res, $n );
}
- /**
- * @return bool
- */
- function isKey() {
- return $this->is_key;
+ protected function mysqlDataSeek( $res, $row ) {
+ return mysql_data_seek( $res, $row );
}
- /**
- * @return bool
- */
- function isMultipleKey() {
- return $this->is_multiple;
+ protected function mysqlError( $conn = null ) {
+ return ( $conn !== null ) ? mysql_error( $conn ) : mysql_error(); // avoid warning
}
-}
-
-class MySQLMasterPos implements DBMasterPos {
- var $file, $pos;
- function __construct( $file, $pos ) {
- $this->file = $file;
- $this->pos = $pos;
+ protected function mysqlRealEscapeString( $s ) {
+ return mysql_real_escape_string( $s, $this->mConn );
}
- function __toString() {
- return "{$this->file}/{$this->pos}";
+ protected function mysqlPing() {
+ return mysql_ping( $this->mConn );
}
}
diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php
new file mode 100644
index 00000000..26c9d247
--- /dev/null
+++ b/includes/db/DatabaseMysqlBase.php
@@ -0,0 +1,1154 @@
+<?php
+/**
+ * This is the MySQL database abstraction layer.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * Database abstraction object for MySQL.
+ * Defines methods independent on used MySQL extension.
+ *
+ * @ingroup Database
+ * @since 1.22
+ * @see Database
+ */
+abstract class DatabaseMysqlBase extends DatabaseBase {
+ /** @var MysqlMasterPos */
+ protected $lastKnownSlavePos;
+
+ /**
+ * @return string
+ */
+ function getType() {
+ return 'mysql';
+ }
+
+ /**
+ * @param $server string
+ * @param $user string
+ * @param $password string
+ * @param $dbName string
+ * @return bool
+ * @throws DBConnectionError
+ */
+ function open( $server, $user, $password, $dbName ) {
+ global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode;
+ wfProfileIn( __METHOD__ );
+
+ # Debugging hack -- fake cluster
+ if ( $wgAllDBsAreLocalhost ) {
+ $realServer = 'localhost';
+ } else {
+ $realServer = $server;
+ }
+ $this->close();
+ $this->mServer = $server;
+ $this->mUser = $user;
+ $this->mPassword = $password;
+ $this->mDBname = $dbName;
+
+ wfProfileIn( "dbconnect-$server" );
+
+ # The kernel's default SYN retransmission period is far too slow for us,
+ # so we use a short timeout plus a manual retry. Retrying means that a small
+ # but finite rate of SYN packet loss won't cause user-visible errors.
+ $this->mConn = false;
+ $this->installErrorHandler();
+ try {
+ $this->mConn = $this->mysqlConnect( $realServer );
+ } catch ( Exception $ex ) {
+ wfProfileOut( "dbconnect-$server" );
+ wfProfileOut( __METHOD__ );
+ throw $ex;
+ }
+ $error = $this->restoreErrorHandler();
+
+ wfProfileOut( "dbconnect-$server" );
+
+ # Always log connection errors
+ if ( !$this->mConn ) {
+ if ( !$error ) {
+ $error = $this->lastError();
+ }
+ wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
+ wfDebug( "DB connection error\n" .
+ "Server: $server, User: $user, Password: " .
+ substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
+
+ wfProfileOut( __METHOD__ );
+ return $this->reportConnectionError( $error );
+ }
+
+ if ( $dbName != '' ) {
+ wfSuppressWarnings();
+ $success = $this->selectDB( $dbName );
+ wfRestoreWarnings();
+ if ( !$success ) {
+ wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" );
+ wfDebug( "Error selecting database $dbName on server {$this->mServer} " .
+ "from client host " . wfHostname() . "\n" );
+
+ wfProfileOut( __METHOD__ );
+ return $this->reportConnectionError( "Error selecting database $dbName" );
+ }
+ }
+
+ // Tell the server we're communicating with it in UTF-8.
+ // This may engage various charset conversions.
+ if ( $wgDBmysql5 ) {
+ $this->query( 'SET NAMES utf8', __METHOD__ );
+ } else {
+ $this->query( 'SET NAMES binary', __METHOD__ );
+ }
+ // Set SQL mode, default is turning them all off, can be overridden or skipped with null
+ if ( is_string( $wgSQLMode ) ) {
+ $mode = $this->addQuotes( $wgSQLMode );
+ $this->query( "SET sql_mode = $mode", __METHOD__ );
+ }
+
+ $this->mOpened = true;
+ wfProfileOut( __METHOD__ );
+ return true;
+ }
+
+ /**
+ * Open a connection to a MySQL server
+ *
+ * @param $realServer string
+ * @return mixed Raw connection
+ * @throws DBConnectionError
+ */
+ abstract protected function mysqlConnect( $realServer );
+
+ /**
+ * @param $res ResultWrapper
+ * @throws DBUnexpectedError
+ */
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ wfSuppressWarnings();
+ $ok = $this->mysqlFreeResult( $res );
+ wfRestoreWarnings();
+ if ( !$ok ) {
+ throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+ }
+ }
+
+ /**
+ * Free result memory
+ *
+ * @param $res Raw result
+ * @return bool
+ */
+ abstract protected function mysqlFreeResult( $res );
+
+ /**
+ * @param $res ResultWrapper
+ * @return object|bool
+ * @throws DBUnexpectedError
+ */
+ function fetchObject( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ wfSuppressWarnings();
+ $row = $this->mysqlFetchObject( $res );
+ wfRestoreWarnings();
+
+ $errno = $this->lastErrno();
+ // Unfortunately, mysql_fetch_object does not reset the last errno.
+ // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
+ // these are the only errors mysql_fetch_object can cause.
+ // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+ if ( $errno == 2000 || $errno == 2013 ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Fetch a result row as an object
+ *
+ * @param $res Raw result
+ * @return stdClass
+ */
+ abstract protected function mysqlFetchObject( $res );
+
+ /**
+ * @param $res ResultWrapper
+ * @return array|bool
+ * @throws DBUnexpectedError
+ */
+ function fetchRow( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ wfSuppressWarnings();
+ $row = $this->mysqlFetchArray( $res );
+ wfRestoreWarnings();
+
+ $errno = $this->lastErrno();
+ // Unfortunately, mysql_fetch_array does not reset the last errno.
+ // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
+ // these are the only errors mysql_fetch_array can cause.
+ // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+ if ( $errno == 2000 || $errno == 2013 ) {
+ throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+ }
+ return $row;
+ }
+
+ /**
+ * Fetch a result row as an associative and numeric array
+ *
+ * @param $res Raw result
+ * @return array
+ */
+ abstract protected function mysqlFetchArray( $res );
+
+ /**
+ * @throws DBUnexpectedError
+ * @param $res ResultWrapper
+ * @return int
+ */
+ function numRows( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ wfSuppressWarnings();
+ $n = $this->mysqlNumRows( $res );
+ wfRestoreWarnings();
+ // Unfortunately, mysql_num_rows does not reset the last errno.
+ // We are not checking for any errors here, since
+ // these are no errors mysql_num_rows can cause.
+ // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
+ // See https://bugzilla.wikimedia.org/42430
+ return $n;
+ }
+
+ /**
+ * Get number of rows in result
+ *
+ * @param $res Raw result
+ * @return int
+ */
+ abstract protected function mysqlNumRows( $res );
+
+ /**
+ * @param $res ResultWrapper
+ * @return int
+ */
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return $this->mysqlNumFields( $res );
+ }
+
+ /**
+ * Get number of fields in result
+ *
+ * @param $res Raw result
+ * @return int
+ */
+ abstract protected function mysqlNumFields( $res );
+
+ /**
+ * @param $res ResultWrapper
+ * @param $n string
+ * @return string
+ */
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return $this->mysqlFieldName( $res, $n );
+ }
+
+ /**
+ * Get the name of the specified field in a result
+ *
+ * @param $res Raw result
+ * @param $n int
+ * @return string
+ */
+ abstract protected function mysqlFieldName( $res, $n );
+
+ /**
+ * @param $res ResultWrapper
+ * @param $row
+ * @return bool
+ */
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return $this->mysqlDataSeek( $res, $row );
+ }
+
+ /**
+ * Move internal result pointer
+ *
+ * @param $res Raw result
+ * @param $row int
+ * @return bool
+ */
+ abstract protected function mysqlDataSeek( $res, $row );
+
+ /**
+ * @return string
+ */
+ function lastError() {
+ if ( $this->mConn ) {
+ # Even if it's non-zero, it can still be invalid
+ wfSuppressWarnings();
+ $error = $this->mysqlError( $this->mConn );
+ if ( !$error ) {
+ $error = $this->mysqlError();
+ }
+ wfRestoreWarnings();
+ } else {
+ $error = $this->mysqlError();
+ }
+ if ( $error ) {
+ $error .= ' (' . $this->mServer . ')';
+ }
+ return $error;
+ }
+
+ /**
+ * Returns the text of the error message from previous MySQL operation
+ *
+ * @param $conn Raw connection
+ * @return string
+ */
+ abstract protected function mysqlError( $conn = null );
+
+ /**
+ * @param $table string
+ * @param $uniqueIndexes
+ * @param $rows array
+ * @param $fname string
+ * @return ResultWrapper
+ */
+ function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+ return $this->nativeReplace( $table, $rows, $fname );
+ }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * Takes same arguments as Database::select()
+ *
+ * @param $table string|array
+ * @param $vars string|array
+ * @param $conds string|array
+ * @param $fname string
+ * @param $options string|array
+ * @return int
+ */
+ public function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) {
+ $options['EXPLAIN'] = true;
+ $res = $this->select( $table, $vars, $conds, $fname, $options );
+ if ( $res === false ) {
+ return false;
+ }
+ if ( !$this->numRows( $res ) ) {
+ return 0;
+ }
+
+ $rows = 1;
+ foreach ( $res as $plan ) {
+ $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
+ }
+ return $rows;
+ }
+
+ /**
+ * @param $table string
+ * @param $field string
+ * @return bool|MySQLField
+ */
+ function fieldInfo( $table, $field ) {
+ $table = $this->tableName( $table );
+ $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
+ if ( !$res ) {
+ return false;
+ }
+ $n = $this->mysqlNumFields( $res->result );
+ for ( $i = 0; $i < $n; $i++ ) {
+ $meta = $this->mysqlFetchField( $res->result, $i );
+ if ( $field == $meta->name ) {
+ return new MySQLField( $meta );
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get column information from a result
+ *
+ * @param $res Raw result
+ * @param $n int
+ * @return stdClass
+ */
+ abstract protected function mysqlFetchField( $res, $n );
+
+ /**
+ * Get information about an index into an object
+ * Returns false if the index does not exist
+ *
+ * @param $table string
+ * @param $index string
+ * @param $fname string
+ * @return bool|array|null False or null on failure
+ */
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
+ # 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 );
+ $index = $this->indexName( $index );
+
+ $sql = 'SHOW INDEX FROM ' . $table;
+ $res = $this->query( $sql, $fname );
+
+ if ( !$res ) {
+ return null;
+ }
+
+ $result = array();
+
+ foreach ( $res as $row ) {
+ if ( $row->Key_name == $index ) {
+ $result[] = $row;
+ }
+ }
+ return empty( $result ) ? false : $result;
+ }
+
+ /**
+ * @param $s string
+ *
+ * @return string
+ */
+ function strencode( $s ) {
+ $sQuoted = $this->mysqlRealEscapeString( $s );
+
+ if ( $sQuoted === false ) {
+ $this->ping();
+ $sQuoted = $this->mysqlRealEscapeString( $s );
+ }
+ return $sQuoted;
+ }
+
+ /**
+ * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
+ *
+ * @param $s string
+ *
+ * @return string
+ */
+ public function addIdentifierQuotes( $s ) {
+ // Characters in the range \u0001-\uFFFF are valid in a quoted identifier
+ // Remove NUL bytes and escape backticks by doubling
+ return '`' . str_replace( array( "\0", '`' ), array( '', '``' ), $s ) . '`';
+ }
+
+ /**
+ * @param $name string
+ * @return bool
+ */
+ public function isQuotedIdentifier( $name ) {
+ return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
+ }
+
+ /**
+ * @return bool
+ */
+ function ping() {
+ $ping = $this->mysqlPing();
+ if ( $ping ) {
+ return true;
+ }
+
+ $this->closeConnection();
+ $this->mOpened = false;
+ $this->mConn = false;
+ $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+ return true;
+ }
+
+ /**
+ * Ping a server connection or reconnect if there is no connection
+ *
+ * @return bool
+ */
+ abstract protected function mysqlPing();
+
+ /**
+ * Returns slave lag.
+ *
+ * This will do a SHOW SLAVE STATUS
+ *
+ * @return int
+ */
+ function getLag() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
+ return $this->mFakeSlaveLag;
+ }
+
+ return $this->getLagFromSlaveStatus();
+ }
+
+ /**
+ * @return bool|int
+ */
+ function getLagFromSlaveStatus() {
+ $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
+ if ( !$res ) {
+ return false;
+ }
+ $row = $res->fetchObject();
+ if ( !$row ) {
+ return false;
+ }
+ if ( strval( $row->Seconds_Behind_Master ) === '' ) {
+ return false;
+ } else {
+ return intval( $row->Seconds_Behind_Master );
+ }
+ }
+
+ /**
+ * @deprecated in 1.19, use getLagFromSlaveStatus
+ *
+ * @return bool|int
+ */
+ function getLagFromProcesslist() {
+ wfDeprecated( __METHOD__, '1.19' );
+ $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ );
+ if ( !$res ) {
+ return false;
+ }
+ # Find slave SQL thread
+ foreach ( $res as $row ) {
+ /* 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' &&
+ $row->State != 'Waiting to reconnect after a failed master event read' &&
+ $row->State != 'Reconnecting after a failed master event read' &&
+ $row->State != 'Registering slave on master'
+ ) {
+ # This is it, return the time (except -ve)
+ if ( $row->Time > 0x7fffffff ) {
+ return false;
+ } else {
+ return $row->Time;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Wait for the slave to catch up to a given master position.
+ * @TODO: return values for this and base class are rubbish
+ *
+ * @param $pos DBMasterPos object
+ * @param $timeout Integer: the maximum number of seconds to wait for synchronisation
+ * @return bool|string
+ */
+ function masterPosWait( DBMasterPos $pos, $timeout ) {
+ if ( $this->lastKnownSlavePos && $this->lastKnownSlavePos->hasReached( $pos ) ) {
+ return '0'; // http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html
+ }
+
+ wfProfileIn( __METHOD__ );
+ # Commit any open transactions
+ $this->commit( __METHOD__, 'flush' );
+
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ $status = parent::masterPosWait( $pos, $timeout );
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ # 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 );
+
+ $status = false;
+ if ( $res && $row = $this->fetchRow( $res ) ) {
+ $status = $row[0]; // can be NULL, -1, or 0+ per the MySQL manual
+ if ( ctype_digit( $status ) ) { // success
+ $this->lastKnownSlavePos = $pos;
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $status;
+ }
+
+ /**
+ * Get the position of the master from SHOW SLAVE STATUS
+ *
+ * @return MySQLMasterPos|bool
+ */
+ function getSlavePos() {
+ if ( !is_null( $this->mFakeSlaveLag ) ) {
+ return parent::getSlavePos();
+ }
+
+ $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' );
+ $row = $this->fetchObject( $res );
+
+ if ( $row ) {
+ $pos = isset( $row->Exec_master_log_pos ) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos;
+ return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get the position of the master from SHOW MASTER STATUS
+ *
+ * @return MySQLMasterPos|bool
+ */
+ function getMasterPos() {
+ if ( $this->mFakeMaster ) {
+ return parent::getMasterPos();
+ }
+
+ $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' );
+ $row = $this->fetchObject( $res );
+
+ if ( $row ) {
+ return new MySQLMasterPos( $row->File, $row->Position );
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param $index
+ * @return string
+ */
+ function useIndexClause( $index ) {
+ return "FORCE INDEX (" . $this->indexName( $index ) . ")";
+ }
+
+ /**
+ * @return string
+ */
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
+ }
+
+ /**
+ * @return string
+ */
+ public function getSoftwareLink() {
+ return '[http://www.mysql.com/ MySQL]';
+ }
+
+ /**
+ * @param $options array
+ */
+ public function setSessionOptions( array $options ) {
+ if ( isset( $options['connTimeout'] ) ) {
+ $timeout = (int)$options['connTimeout'];
+ $this->query( "SET net_read_timeout=$timeout" );
+ $this->query( "SET net_write_timeout=$timeout" );
+ }
+ }
+
+ public function streamStatementEnd( &$sql, &$newLine ) {
+ if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
+ preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
+ $this->delimiter = $m[1];
+ $newLine = '';
+ }
+ return parent::streamStatementEnd( $sql, $newLine );
+ }
+
+ /**
+ * Check to see if a named lock is available. This is non-blocking.
+ *
+ * @param string $lockName name of lock to poll
+ * @param string $method name of method calling us
+ * @return Boolean
+ * @since 1.20
+ */
+ public function lockIsFree( $lockName, $method ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method );
+ $row = $this->fetchObject( $result );
+ return ( $row->lockstatus == 1 );
+ }
+
+ /**
+ * @param $lockName string
+ * @param $method string
+ * @param $timeout int
+ * @return bool
+ */
+ public function lock( $lockName, $method, $timeout = 5 ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method );
+ $row = $this->fetchObject( $result );
+
+ if ( $row->lockstatus == 1 ) {
+ return true;
+ } else {
+ wfDebug( __METHOD__ . " failed to acquire lock\n" );
+ return false;
+ }
+ }
+
+ /**
+ * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
+ * @param $lockName string
+ * @param $method string
+ * @return bool
+ */
+ public function unlock( $lockName, $method ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method );
+ $row = $this->fetchObject( $result );
+ return ( $row->lockstatus == 1 );
+ }
+
+ /**
+ * @param $read array
+ * @param $write array
+ * @param $method string
+ * @param $lowPriority bool
+ * @return bool
+ */
+ public function lockTables( $read, $write, $method, $lowPriority = true ) {
+ $items = array();
+
+ foreach ( $write as $table ) {
+ $tbl = $this->tableName( $table ) .
+ ( $lowPriority ? ' LOW_PRIORITY' : '' ) .
+ ' WRITE';
+ $items[] = $tbl;
+ }
+ foreach ( $read as $table ) {
+ $items[] = $this->tableName( $table ) . ' READ';
+ }
+ $sql = "LOCK TABLES " . implode( ',', $items );
+ $this->query( $sql, $method );
+ return true;
+ }
+
+ /**
+ * @param $method string
+ * @return bool
+ */
+ public function unlockTables( $method ) {
+ $this->query( "UNLOCK TABLES", $method );
+ return true;
+ }
+
+ /**
+ * Get search engine class. All subclasses of this
+ * need to implement this if they wish to use searching.
+ *
+ * @return String
+ */
+ public function getSearchEngine() {
+ return 'SearchMySQL';
+ }
+
+ /**
+ * @param bool $value
+ * @return mixed
+ */
+ public function setBigSelects( $value = true ) {
+ if ( $value === 'default' ) {
+ if ( $this->mDefaultBigSelects === null ) {
+ # Function hasn't been called before so it must already be set to the default
+ return;
+ } else {
+ $value = $this->mDefaultBigSelects;
+ }
+ } elseif ( $this->mDefaultBigSelects === null ) {
+ $this->mDefaultBigSelects = (bool)$this->selectField( false, '@@sql_big_selects' );
+ }
+ $encValue = $value ? '1' : '0';
+ $this->query( "SET sql_big_selects=$encValue", __METHOD__ );
+ }
+
+ /**
+ * DELETE where the condition is a join. MySql uses multi-table deletes.
+ * @param $delTable string
+ * @param $joinTable string
+ * @param $delVar string
+ * @param $joinVar string
+ * @param $conds array|string
+ * @param bool|string $fname bool
+ * @throws DBUnexpectedError
+ * @return bool|ResultWrapper
+ */
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) {
+ if ( !$conds ) {
+ throw new DBUnexpectedError( $this, 'DatabaseBase::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 );
+ }
+
+ /**
+ * @param string $table
+ * @param array $rows
+ * @param array $uniqueIndexes
+ * @param array $set
+ * @param string $fname
+ * @param array $options
+ * @return bool
+ */
+ public function upsert(
+ $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
+ ) {
+ if ( !count( $rows ) ) {
+ return true; // nothing to do
+ }
+ $rows = is_array( reset( $rows ) ) ? $rows : array( $rows );
+
+ $table = $this->tableName( $table );
+ $columns = array_keys( $rows[0] );
+
+ $sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
+ $rowTuples = array();
+ foreach ( $rows as $row ) {
+ $rowTuples[] = '(' . $this->makeList( $row ) . ')';
+ }
+ $sql .= implode( ',', $rowTuples );
+ $sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, LIST_SET );
+
+ return (bool)$this->query( $sql, $fname );
+ }
+
+ /**
+ * Determines how long the server has been up
+ *
+ * @return int
+ */
+ function getServerUptime() {
+ $vars = $this->getMysqlStatus( 'Uptime' );
+ return (int)$vars['Uptime'];
+ }
+
+ /**
+ * Determines if the last failure was due to a deadlock
+ *
+ * @return bool
+ */
+ function wasDeadlock() {
+ return $this->lastErrno() == 1213;
+ }
+
+ /**
+ * Determines if the last failure was due to a lock timeout
+ *
+ * @return bool
+ */
+ function wasLockTimeout() {
+ return $this->lastErrno() == 1205;
+ }
+
+ /**
+ * Determines if the last query error was something that should be dealt
+ * with by pinging the connection and reissuing the query
+ *
+ * @return bool
+ */
+ function wasErrorReissuable() {
+ return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
+ }
+
+ /**
+ * Determines if the last failure was due to the database being read-only.
+ *
+ * @return bool
+ */
+ function wasReadOnlyError() {
+ return $this->lastErrno() == 1223 ||
+ ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
+ }
+
+ /**
+ * @param $oldName
+ * @param $newName
+ * @param $temporary bool
+ * @param $fname string
+ */
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
+ $tmp = $temporary ? 'TEMPORARY ' : '';
+ $newName = $this->addIdentifierQuotes( $newName );
+ $oldName = $this->addIdentifierQuotes( $oldName );
+ $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
+ $this->query( $query, $fname );
+ }
+
+ /**
+ * List all tables on the database
+ *
+ * @param string $prefix Only show tables with this prefix, e.g. mw_
+ * @param string $fname calling function name
+ * @return array
+ */
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
+ $result = $this->query( "SHOW TABLES", $fname );
+
+ $endArray = array();
+
+ foreach ( $result as $table ) {
+ $vars = get_object_vars( $table );
+ $table = array_pop( $vars );
+
+ if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
+ $endArray[] = $table;
+ }
+ }
+
+ return $endArray;
+ }
+
+ /**
+ * @param $tableName
+ * @param $fName string
+ * @return bool|ResultWrapper
+ */
+ public function dropTable( $tableName, $fName = __METHOD__ ) {
+ if ( !$this->tableExists( $tableName, $fName ) ) {
+ return false;
+ }
+ return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
+ }
+
+ /**
+ * @return array
+ */
+ protected function getDefaultSchemaVars() {
+ $vars = parent::getDefaultSchemaVars();
+ $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
+ $vars['wgDBTableOptions'] = str_replace( 'CHARSET=mysql4', 'CHARSET=binary', $vars['wgDBTableOptions'] );
+ return $vars;
+ }
+
+ /**
+ * Get status information from SHOW STATUS in an associative array
+ *
+ * @param $which string
+ * @return array
+ */
+ function getMysqlStatus( $which = "%" ) {
+ $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
+ $status = array();
+
+ foreach ( $res as $row ) {
+ $status[$row->Variable_name] = $row->Value;
+ }
+
+ return $status;
+ }
+
+ /**
+ * Lists VIEWs in the database
+ *
+ * @param string $prefix Only show VIEWs with this prefix, eg.
+ * unit_test_, or $wgDBprefix. Default: null, would return all views.
+ * @param string $fname Name of calling function
+ * @return array
+ * @since 1.22
+ */
+ public function listViews( $prefix = null, $fname = __METHOD__ ) {
+
+ if ( !isset( $this->allViews ) ) {
+
+ // The name of the column containing the name of the VIEW
+ $propertyName = 'Tables_in_' . $this->mDBname;
+
+ // Query for the VIEWS
+ $result = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
+ $this->allViews = array();
+ while ( ($row = $this->fetchRow($result)) !== false ) {
+ array_push( $this->allViews, $row[$propertyName] );
+ }
+ }
+
+ if ( is_null($prefix) || $prefix === '' ) {
+ return $this->allViews;
+ }
+
+ $filteredViews = array();
+ foreach ( $this->allViews as $viewName ) {
+ // Does the name of this VIEW start with the table-prefix?
+ if ( strpos( $viewName, $prefix ) === 0 ) {
+ array_push( $filteredViews, $viewName );
+ }
+ }
+ return $filteredViews;
+ }
+
+ /**
+ * Differentiates between a TABLE and a VIEW.
+ *
+ * @param $name string: Name of the TABLE/VIEW to test
+ * @return bool
+ * @since 1.22
+ */
+ public function isView( $name, $prefix = null ) {
+ return in_array( $name, $this->listViews( $prefix ) );
+ }
+
+}
+
+
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class MySQLField implements Field {
+ private $name, $tablename, $default, $max_length, $nullable,
+ $is_pk, $is_unique, $is_multiple, $is_key, $type, $binary;
+
+ 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;
+ $this->binary = isset( $info->binary ) ? $info->binary : false;
+ }
+
+ /**
+ * @return string
+ */
+ function name() {
+ return $this->name;
+ }
+
+ /**
+ * @return string
+ */
+ function tableName() {
+ return $this->tableName;
+ }
+
+ /**
+ * @return string
+ */
+ function type() {
+ return $this->type;
+ }
+
+ /**
+ * @return bool
+ */
+ function isNullable() {
+ return $this->nullable;
+ }
+
+ function defaultValue() {
+ return $this->default;
+ }
+
+ /**
+ * @return bool
+ */
+ function isKey() {
+ return $this->is_key;
+ }
+
+ /**
+ * @return bool
+ */
+ function isMultipleKey() {
+ return $this->is_multiple;
+ }
+
+ function isBinary() {
+ return $this->binary;
+ }
+}
+
+class MySQLMasterPos implements DBMasterPos {
+ var $file, $pos;
+
+ function __construct( $file, $pos ) {
+ $this->file = $file;
+ $this->pos = $pos;
+ }
+
+ function __toString() {
+ // e.g db1034-bin.000976/843431247
+ return "{$this->file}/{$this->pos}";
+ }
+
+ /**
+ * @return array|false (int, int)
+ */
+ protected function getCoordinates() {
+ $m = array();
+ if ( preg_match( '!\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
+ return array( (int)$m[1], (int)$m[2] );
+ }
+ return false;
+ }
+
+ function hasReached( MySQLMasterPos $pos ) {
+ $thisPos = $this->getCoordinates();
+ $thatPos = $pos->getCoordinates();
+ return ( $thisPos && $thatPos && $thisPos >= $thatPos );
+ }
+}
diff --git a/includes/db/DatabaseMysqli.php b/includes/db/DatabaseMysqli.php
new file mode 100644
index 00000000..7761abe9
--- /dev/null
+++ b/includes/db/DatabaseMysqli.php
@@ -0,0 +1,194 @@
+<?php
+/**
+ * This is the MySQLi database abstraction layer.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * Database abstraction object for PHP extension mysqli.
+ *
+ * @ingroup Database
+ * @since 1.22
+ * @see Database
+ */
+class DatabaseMysqli extends DatabaseMysqlBase {
+
+ /**
+ * @param $sql string
+ * @return resource
+ */
+ protected function doQuery( $sql ) {
+ if ( $this->bufferResults() ) {
+ $ret = $this->mConn->query( $sql );
+ } else {
+ $ret = $this->mConn->query( $sql, MYSQLI_USE_RESULT );
+ }
+ return $ret;
+ }
+
+ protected function mysqlConnect( $realServer ) {
+ # Fail now
+ # Otherwise we get a suppressed fatal error, which is very hard to track down
+ if ( !function_exists( 'mysqli_init' ) ) {
+ throw new DBConnectionError( $this, "MySQLi functions missing,"
+ . " have you compiled PHP with the --with-mysqli option?\n" );
+ }
+
+ $connFlags = 0;
+ if ( $this->mFlags & DBO_SSL ) {
+ $connFlags |= MYSQLI_CLIENT_SSL;
+ }
+ if ( $this->mFlags & DBO_COMPRESS ) {
+ $connFlags |= MYSQLI_CLIENT_COMPRESS;
+ }
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ $realServer = 'p:' . $realServer;
+ }
+
+ $mysqli = mysqli_init();
+ $numAttempts = 2;
+
+ for ( $i = 0; $i < $numAttempts; $i++ ) {
+ if ( $i > 1 ) {
+ usleep( 1000 );
+ }
+ if ( $mysqli->real_connect( $realServer, $this->mUser,
+ $this->mPassword, $this->mDBname, null, null, $connFlags ) )
+ {
+ return $mysqli;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function closeConnection() {
+ return $this->mConn->close();
+ }
+
+ /**
+ * @return int
+ */
+ function insertId() {
+ return $this->mConn->insert_id;
+ }
+
+ /**
+ * @return int
+ */
+ function lastErrno() {
+ if ( $this->mConn ) {
+ return $this->mConn->errno;
+ } else {
+ return mysqli_connect_errno();
+ }
+ }
+
+ /**
+ * @return int
+ */
+ function affectedRows() {
+ return $this->mConn->affected_rows;
+ }
+
+ /**
+ * @param $db
+ * @return bool
+ */
+ function selectDB( $db ) {
+ $this->mDBname = $db;
+ return $this->mConn->select_db( $db );
+ }
+
+ /**
+ * @return string
+ */
+ function getServerVersion() {
+ return $this->mConn->server_info;
+ }
+
+ protected function mysqlFreeResult( $res ) {
+ $res->free_result();
+ return true;
+ }
+
+ protected function mysqlFetchObject( $res ) {
+ $object = $res->fetch_object();
+ if ( $object === null ) {
+ return false;
+ }
+ return $object;
+ }
+
+ protected function mysqlFetchArray( $res ) {
+ $array = $res->fetch_array();
+ if ( $array === null ) {
+ return false;
+ }
+ return $array;
+ }
+
+ protected function mysqlNumRows( $res ) {
+ return $res->num_rows;
+ }
+
+ protected function mysqlNumFields( $res ) {
+ return $res->field_count;
+ }
+
+ protected function mysqlFetchField( $res, $n ) {
+ $field = $res->fetch_field_direct( $n );
+ $field->not_null = $field->flags & MYSQLI_NOT_NULL_FLAG;
+ $field->primary_key = $field->flags & MYSQLI_PRI_KEY_FLAG;
+ $field->unique_key = $field->flags & MYSQLI_UNIQUE_KEY_FLAG;
+ $field->multiple_key = $field->flags & MYSQLI_MULTIPLE_KEY_FLAG;
+ $field->binary = $field->flags & MYSQLI_BINARY_FLAG;
+ return $field;
+ }
+
+ protected function mysqlFieldName( $res, $n ) {
+ $field = $res->fetch_field_direct( $n );
+ return $field->name;
+ }
+
+ protected function mysqlDataSeek( $res, $row ) {
+ return $res->data_seek( $row );
+ }
+
+ protected function mysqlError( $conn = null ) {
+ if ($conn === null) {
+ return mysqli_connect_error();
+ } else {
+ return $conn->error;
+ }
+ }
+
+ protected function mysqlRealEscapeString( $s ) {
+ return $this->mConn->real_escape_string( $s );
+ }
+
+ protected function mysqlPing() {
+ return $this->mConn->ping();
+ }
+
+}
diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php
index 75b3550a..fbaa4da5 100644
--- a/includes/db/DatabaseOracle.php
+++ b/includes/db/DatabaseOracle.php
@@ -206,7 +206,7 @@ class DatabaseOracle extends DatabaseBase {
}
function __destruct() {
- if ($this->mOpened) {
+ if ( $this->mOpened ) {
wfSuppressWarnings();
$this->close();
wfRestoreWarnings();
@@ -249,6 +249,7 @@ class DatabaseOracle extends DatabaseBase {
* @return DatabaseBase|null
*/
function open( $server, $user, $password, $dbName ) {
+ global $wgDBOracleDRCP;
if ( !function_exists( 'oci_connect' ) ) {
throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
}
@@ -276,9 +277,16 @@ class DatabaseOracle extends DatabaseBase {
return;
}
+ if ( $wgDBOracleDRCP ) {
+ $this->setFlag( DBO_PERSISTENT );
+ }
+
$session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT;
+
wfSuppressWarnings();
- if ( $this->mFlags & DBO_DEFAULT ) {
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ $this->mConn = oci_pconnect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode );
+ } elseif ( $this->mFlags & DBO_DEFAULT ) {
$this->mConn = oci_new_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode );
} else {
$this->mConn = oci_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode );
@@ -324,7 +332,7 @@ class DatabaseOracle extends DatabaseBase {
// handle some oracle specifics
// remove AS column/table/subquery namings
- if( !$this->getFlag( DBO_DDLMODE ) ) {
+ if ( !$this->getFlag( DBO_DDLMODE ) ) {
$sql = preg_replace( '/ as /i', ' ', $sql );
}
@@ -333,7 +341,7 @@ class DatabaseOracle extends DatabaseBase {
$union_unique = ( preg_match( '/\/\* UNION_UNIQUE \*\/ /', $sql ) != 0 );
// EXPLAIN syntax in Oracle is EXPLAIN PLAN FOR and it return nothing
// you have to select data from plan table after explain
- $explain_id = date( 'dmYHis' );
+ $explain_id = MWTimestamp::getLocalInstance()->format( 'dmYHis' );
$sql = preg_replace( '/^EXPLAIN /', 'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR', $sql, 1, $explain_count );
@@ -456,15 +464,15 @@ class DatabaseOracle extends DatabaseBase {
* If errors are explicitly ignored, returns NULL on failure
* @return bool
*/
- function indexInfo( $table, $index, $fname = 'DatabaseOracle::indexExists' ) {
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
return false;
}
- function indexUnique( $table, $index, $fname = 'DatabaseOracle::indexUnique' ) {
+ function indexUnique( $table, $index, $fname = __METHOD__ ) {
return false;
}
- function insert( $table, $a, $fname = 'DatabaseOracle::insert', $options = array() ) {
+ function insert( $table, $a, $fname = __METHOD__, $options = array() ) {
if ( !count( $a ) ) {
return true;
}
@@ -493,7 +501,7 @@ class DatabaseOracle extends DatabaseBase {
return $retVal;
}
- private function fieldBindStatement ( $table, $col, &$val, $includeCol = false ) {
+ private function fieldBindStatement( $table, $col, &$val, $includeCol = false ) {
$col_info = $this->fieldInfoMulti( $table, $col );
$col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
@@ -624,7 +632,7 @@ class DatabaseOracle extends DatabaseBase {
oci_free_statement( $stmt );
}
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseOracle::insertSelect',
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
$insertOptions = array(), $selectOptions = array() )
{
$destTable = $this->tableName( $destTable );
@@ -677,7 +685,7 @@ class DatabaseOracle extends DatabaseBase {
Using uppercase because that's the only way Oracle can handle
quoted tablenames
*/
- switch( $name ) {
+ switch ( $name ) {
case 'user':
$name = 'MWUSER';
break;
@@ -686,12 +694,12 @@ class DatabaseOracle extends DatabaseBase {
break;
}
- return parent::tableName( strtoupper( $name ), $format );
+ return strtoupper( parent::tableName( $name, $format ) );
}
function tableNameInternal( $name ) {
$name = $this->tableName( $name );
- return preg_replace( '/.*\.(.*)/', '$1', $name);
+ return preg_replace( '/.*\.(.*)/', '$1', $name );
}
/**
* Return the next in a sequence, save the value for retrieval via insertId()
@@ -756,14 +764,14 @@ class DatabaseOracle extends DatabaseBase {
function unionQueries( $sqls, $all ) {
$glue = ' UNION ALL ';
- return 'SELECT * ' . ( $all ? '':'/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')';
+ return 'SELECT * ' . ( $all ? '' : '/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')';
}
function wasDeadlock() {
return $this->lastErrno() == 'OCI-00060';
}
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseOracle::duplicateTableStructure' ) {
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
$temporary = $temporary ? 'TRUE' : 'FALSE';
$newName = strtoupper( $newName );
@@ -776,7 +784,7 @@ class DatabaseOracle extends DatabaseBase {
return $this->doQuery( "BEGIN DUPLICATE_TABLE( '$tabName', '$oldPrefix', '$newPrefix', $temporary ); END;" );
}
- function listTables( $prefix = null, $fname = 'DatabaseOracle::listTables' ) {
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
$listWhere = '';
if ( !empty( $prefix ) ) {
$listWhere = ' AND table_name LIKE \'' . strtoupper( $prefix ) . '%\'';
@@ -791,17 +799,18 @@ class DatabaseOracle extends DatabaseBase {
$endArray[] = strtoupper( $prefix . 'PAGE' );
$endArray[] = strtoupper( $prefix . 'IMAGE' );
$fixedOrderTabs = $endArray;
- while ( ($row = $result->fetchRow()) !== false ) {
- if ( !in_array( $row['table_name'], $fixedOrderTabs ) )
+ while ( ( $row = $result->fetchRow() ) !== false ) {
+ if ( !in_array( $row['table_name'], $fixedOrderTabs ) ) {
$endArray[] = $row['table_name'];
+ }
}
return $endArray;
}
- public function dropTable( $tableName, $fName = 'DatabaseOracle::dropTable' ) {
+ public function dropTable( $tableName, $fName = __METHOD__ ) {
$tableName = $this->tableName( $tableName );
- if( !$this->tableExists( $tableName ) ) {
+ if ( !$this->tableExists( $tableName ) ) {
return false;
}
@@ -836,7 +845,7 @@ class DatabaseOracle extends DatabaseBase {
/**
* @return string wikitext of a link to the server software's web site
*/
- public static function getSoftwareLink() {
+ public function getSoftwareLink() {
return '[http://www.oracle.com/ Oracle]';
}
@@ -856,7 +865,7 @@ class DatabaseOracle extends DatabaseBase {
* Query whether a given index exists
* @return bool
*/
- function indexExists( $table, $index, $fname = 'DatabaseOracle::indexExists' ) {
+ function indexExists( $table, $index, $fname = __METHOD__ ) {
$table = $this->tableName( $table );
$table = strtoupper( $this->removeIdentifierQuotes( $table ) );
$index = strtoupper( $index );
@@ -874,7 +883,7 @@ class DatabaseOracle extends DatabaseBase {
/**
* Query whether a given table exists (in the given schema, or the default mw one if not given)
- * @return int
+ * @return bool
*/
function tableExists( $table, $fname = __METHOD__ ) {
$table = $this->tableName( $table );
@@ -882,13 +891,14 @@ class DatabaseOracle extends DatabaseBase {
$owner = $this->addQuotes( strtoupper( $this->mDBname ) );
$SQL = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table";
$res = $this->doQuery( $SQL );
- if ( $res ) {
- $count = $res->numRows();
- $res->free();
+ if ( $res && $res->numRows() > 0 ) {
+ $exists = true;
} else {
- $count = 0;
+ $exists = false;
}
- return $count;
+
+ $res->free();
+ return $exists;
}
/**
@@ -906,7 +916,7 @@ class DatabaseOracle extends DatabaseBase {
if ( is_array( $table ) ) {
$table = array_map( array( &$this, 'tableNameInternal' ), $table );
$tableWhere = 'IN (';
- foreach( $table as &$singleTable ) {
+ foreach ( $table as &$singleTable ) {
$singleTable = $this->removeIdentifierQuotes( $singleTable );
if ( isset( $this->mFieldInfoCache["$singleTable.$field"] ) ) {
return $this->mFieldInfoCache["$singleTable.$field"];
@@ -919,7 +929,7 @@ class DatabaseOracle extends DatabaseBase {
if ( isset( $this->mFieldInfoCache["$table.$field"] ) ) {
return $this->mFieldInfoCache["$table.$field"];
}
- $tableWhere = '= \''.$table.'\'';
+ $tableWhere = '= \'' . $table . '\'';
}
$fieldInfoStmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name ' . $tableWhere . ' and column_name = \'' . $field . '\'' );
@@ -931,7 +941,7 @@ class DatabaseOracle extends DatabaseBase {
$res = new ORAResult( $this, $fieldInfoStmt );
if ( $res->numRows() == 0 ) {
if ( is_array( $table ) ) {
- foreach( $table as &$singleTable ) {
+ foreach ( $table as &$singleTable ) {
$this->mFieldInfoCache["$singleTable.$field"] = false;
}
} else {
@@ -960,12 +970,12 @@ class DatabaseOracle extends DatabaseBase {
return $this->fieldInfoMulti( $table, $field );
}
- protected function doBegin( $fname = 'DatabaseOracle::begin' ) {
+ protected function doBegin( $fname = __METHOD__ ) {
$this->mTrxLevel = 1;
$this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' );
}
- protected function doCommit( $fname = 'DatabaseOracle::commit' ) {
+ protected function doCommit( $fname = __METHOD__ ) {
if ( $this->mTrxLevel ) {
$ret = oci_commit( $this->mConn );
if ( !$ret ) {
@@ -976,7 +986,7 @@ class DatabaseOracle extends DatabaseBase {
}
}
- protected function doRollback( $fname = 'DatabaseOracle::rollback' ) {
+ protected function doRollback( $fname = __METHOD__ ) {
if ( $this->mTrxLevel ) {
oci_rollback( $this->mConn );
$this->mTrxLevel = 0;
@@ -986,7 +996,7 @@ class DatabaseOracle extends DatabaseBase {
/* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */
function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
- $fname = 'DatabaseOracle::sourceStream', $inputCallback = false ) {
+ $fname = __METHOD__, $inputCallback = false ) {
$cmd = '';
$done = false;
$dollarquote = false;
@@ -1121,16 +1131,16 @@ class DatabaseOracle extends DatabaseBase {
}
}
- private function wrapConditionsForWhere ( $table, $conds, $parentCol = null ) {
+ private function wrapConditionsForWhere( $table, $conds, $parentCol = null ) {
$conds2 = array();
foreach ( $conds as $col => $val ) {
if ( is_array( $val ) ) {
- $conds2[$col] = $this->wrapConditionsForWhere ( $table, $val, $col );
+ $conds2[$col] = $this->wrapConditionsForWhere( $table, $val, $col );
} else {
if ( is_numeric( $col ) && $parentCol != null ) {
- $this->wrapFieldForWhere ( $table, $parentCol, $val );
+ $this->wrapFieldForWhere( $table, $parentCol, $val );
} else {
- $this->wrapFieldForWhere ( $table, $col, $val );
+ $this->wrapFieldForWhere( $table, $col, $val );
}
$conds2[$col] = $val;
}
@@ -1138,7 +1148,7 @@ class DatabaseOracle extends DatabaseBase {
return $conds2;
}
- function selectRow( $table, $vars, $conds, $fname = 'DatabaseOracle::selectRow', $options = array(), $join_conds = array() ) {
+ function selectRow( $table, $vars, $conds, $fname = __METHOD__, $options = array(), $join_conds = array() ) {
if ( is_array( $conds ) ) {
$conds = $this->wrapConditionsForWhere( $table, $conds );
}
@@ -1187,7 +1197,7 @@ class DatabaseOracle extends DatabaseBase {
return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
}
- public function delete( $table, $conds, $fname = 'DatabaseOracle::delete' ) {
+ public function delete( $table, $conds, $fname = __METHOD__ ) {
if ( is_array( $conds ) ) {
$conds = $this->wrapConditionsForWhere( $table, $conds );
}
@@ -1210,7 +1220,7 @@ class DatabaseOracle extends DatabaseBase {
return parent::delete( $table, $conds, $fname );
}
- function update( $table, $values, $conds, $fname = 'DatabaseOracle::update', $options = array() ) {
+ function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) {
global $wgContLang;
$table = $this->tableName( $table );
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
index f32d7758..e564a162 100644
--- a/includes/db/DatabasePostgres.php
+++ b/includes/db/DatabasePostgres.php
@@ -117,7 +117,7 @@ SQL;
* @since 1.19
*/
function defaultValue() {
- if( $this->has_default ) {
+ if ( $this->has_default ) {
return $this->default;
} else {
return false;
@@ -139,15 +139,15 @@ class PostgresTransactionState {
array(
"desc" => "%s: Connection state changed from %s -> %s\n",
"states" => array(
- PGSQL_CONNECTION_OK => "OK",
- PGSQL_CONNECTION_BAD => "BAD"
+ PGSQL_CONNECTION_OK => "OK",
+ PGSQL_CONNECTION_BAD => "BAD"
)
),
array(
"desc" => "%s: Transaction state changed from %s -> %s\n",
"states" => array(
- PGSQL_TRANSACTION_IDLE => "IDLE",
- PGSQL_TRANSACTION_ACTIVE => "ACTIVE",
+ PGSQL_TRANSACTION_IDLE => "IDLE",
+ PGSQL_TRANSACTION_ACTIVE => "ACTIVE",
PGSQL_TRANSACTION_INTRANS => "TRANS",
PGSQL_TRANSACTION_INERROR => "ERROR",
PGSQL_TRANSACTION_UNKNOWN => "UNKNOWN"
@@ -189,7 +189,7 @@ class PostgresTransactionState {
}
protected function describe_changed( $status, $desc_table ) {
- if( isset( $desc_table[$status] ) ) {
+ if ( isset( $desc_table[$status] ) ) {
return $desc_table[$status];
} else {
return "STATUS " . $status;
@@ -218,7 +218,7 @@ class SavepointPostgres {
protected $id;
protected $didbegin;
- public function __construct ( $dbw, $id ) {
+ public function __construct( $dbw, $id ) {
$this->dbw = $dbw;
$this->id = $id;
$this->didbegin = false;
@@ -320,7 +320,7 @@ class DatabasePostgres extends DatabaseBase {
function hasConstraint( $name ) {
$SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" .
- pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) ."'";
+ pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) . "'";
$res = $this->doQuery( $SQL );
return $this->numRows( $res );
}
@@ -438,7 +438,7 @@ class DatabasePostgres extends DatabaseBase {
$sql = mb_convert_encoding( $sql, 'UTF-8' );
}
$this->mTransactionState->check();
- if( pg_send_query( $this->mConn, $sql ) === false ) {
+ if ( pg_send_query( $this->mConn, $sql ) === false ) {
throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" );
}
$this->mLastResult = pg_get_result( $this->mConn );
@@ -450,7 +450,7 @@ class DatabasePostgres extends DatabaseBase {
return $this->mLastResult;
}
- protected function dumpError () {
+ protected function dumpError() {
$diags = array( PGSQL_DIAG_SEVERITY,
PGSQL_DIAG_SQLSTATE,
PGSQL_DIAG_MESSAGE_PRIMARY,
@@ -464,7 +464,7 @@ class DatabasePostgres extends DatabaseBase {
PGSQL_DIAG_SOURCE_LINE,
PGSQL_DIAG_SOURCE_FUNCTION );
foreach ( $diags as $d ) {
- wfDebug( sprintf("PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) );
+ wfDebug( sprintf( "PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) );
}
}
@@ -482,7 +482,7 @@ class DatabasePostgres extends DatabaseBase {
parent::reportQueryError( $error, $errno, $sql, $fname, false );
}
- function queryIgnore( $sql, $fname = 'DatabasePostgres::queryIgnore' ) {
+ function queryIgnore( $sql, $fname = __METHOD__ ) {
return $this->query( $sql, $fname, true );
}
@@ -509,7 +509,7 @@ class DatabasePostgres extends DatabaseBase {
# @todo hashar: not sure if the following test really trigger if the object
# fetching failed.
- if( pg_last_error( $this->mConn ) ) {
+ if ( pg_last_error( $this->mConn ) ) {
throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
}
return $row;
@@ -522,7 +522,7 @@ class DatabasePostgres extends DatabaseBase {
wfSuppressWarnings();
$row = pg_fetch_array( $res );
wfRestoreWarnings();
- if( pg_last_error( $this->mConn ) ) {
+ if ( pg_last_error( $this->mConn ) ) {
throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
}
return $row;
@@ -535,7 +535,7 @@ class DatabasePostgres extends DatabaseBase {
wfSuppressWarnings();
$n = pg_num_rows( $res );
wfRestoreWarnings();
- if( pg_last_error( $this->mConn ) ) {
+ if ( pg_last_error( $this->mConn ) ) {
throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) );
}
return $n;
@@ -556,8 +556,10 @@ class DatabasePostgres extends DatabaseBase {
}
/**
- * This must be called after nextSequenceVal
- * @return null
+ * Return the result of the last call to nextSequenceValue();
+ * This must be called after nextSequenceValue().
+ *
+ * @return integer|null
*/
function insertId() {
return $this->mInsertId;
@@ -594,7 +596,7 @@ class DatabasePostgres extends DatabaseBase {
// Forced result for simulated queries
return $this->mAffectedRows;
}
- if( empty( $this->mLastResult ) ) {
+ if ( empty( $this->mLastResult ) ) {
return 0;
}
return pg_affected_rows( $this->mLastResult );
@@ -608,14 +610,14 @@ class DatabasePostgres extends DatabaseBase {
* Takes same arguments as Database::select()
* @return int
*/
- function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) {
+ function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) {
$options['EXPLAIN'] = true;
$res = $this->select( $table, $vars, $conds, $fname, $options );
$rows = -1;
if ( $res ) {
$row = $this->fetchRow( $res );
$count = array();
- if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
+ if ( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
$rows = $count[1];
}
}
@@ -627,7 +629,7 @@ class DatabasePostgres extends DatabaseBase {
* If errors are explicitly ignored, returns NULL on failure
* @return bool|null
*/
- function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) {
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
$res = $this->query( $sql, $fname );
if ( !$res ) {
@@ -647,9 +649,10 @@ class DatabasePostgres extends DatabaseBase {
* @since 1.19
* @return Array
*/
- function indexAttributes ( $index, $schema = false ) {
- if ( $schema === false )
+ function indexAttributes( $index, $schema = false ) {
+ if ( $schema === false ) {
$schema = $this->getCoreSchema();
+ }
/*
* A subquery would be not needed if we didn't care about the order
* of attributes, but we do
@@ -702,8 +705,8 @@ __INDEXATTR__;
return $a;
}
- function indexUnique( $table, $index, $fname = 'DatabasePostgres::indexUnique' ) {
- $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
+ function indexUnique( $table, $index, $fname = __METHOD__ ) {
+ $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'" .
" AND indexdef LIKE 'CREATE UNIQUE%(" .
$this->strencode( $this->indexName( $index ) ) .
")'";
@@ -730,7 +733,7 @@ __INDEXATTR__;
*
* @return bool Success of insert operation. IGNORE always returns true.
*/
- function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
+ function insert( $table, $args, $fname = __METHOD__, $options = array() ) {
if ( !count( $args ) ) {
return true;
}
@@ -847,12 +850,12 @@ __INDEXATTR__;
* @todo FIXME: Implement this a little better (seperate select/insert)?
* @return bool
*/
- function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect',
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__,
$insertOptions = array(), $selectOptions = array() )
{
$destTable = $this->tableName( $destTable );
- if( !is_array( $insertOptions ) ) {
+ if ( !is_array( $insertOptions ) ) {
$insertOptions = array( $insertOptions );
}
@@ -868,11 +871,11 @@ __INDEXATTR__;
$savepoint->savepoint();
}
- if( !is_array( $selectOptions ) ) {
+ if ( !is_array( $selectOptions ) ) {
$selectOptions = array( $selectOptions );
}
list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
- if( is_array( $srcTable ) ) {
+ if ( is_array( $srcTable ) ) {
$srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
} else {
$srcTable = $this->tableName( $srcTable );
@@ -889,9 +892,9 @@ __INDEXATTR__;
$sql .= " $tailOpts";
$res = (bool)$this->query( $sql, $fname, $savepoint );
- if( $savepoint ) {
+ if ( $savepoint ) {
$bar = pg_last_error();
- if( $bar != false ) {
+ if ( $bar != false ) {
$savepoint->rollback();
} else {
$savepoint->release();
@@ -912,7 +915,7 @@ __INDEXATTR__;
function tableName( $name, $format = 'quoted' ) {
# Replace reserved words with better ones
- switch( $name ) {
+ switch ( $name ) {
case 'user':
return $this->realTableName( 'mwuser', $format );
case 'text':
@@ -958,7 +961,7 @@ __INDEXATTR__;
FROM pg_class c, pg_attribute a, pg_type t
WHERE relname='$table' AND a.attrelid=c.oid AND
a.atttypid=t.oid and a.attname='$field'";
- $res =$this->query( $sql );
+ $res = $this->query( $sql );
$row = $this->fetchObject( $res );
if ( $row->ftype == 'varchar' ) {
$size = $row->size - 4;
@@ -976,21 +979,21 @@ __INDEXATTR__;
return $this->lastErrno() == '40P01';
}
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabasePostgres::duplicateTableStructure' ) {
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
$newName = $this->addIdentifierQuotes( $newName );
$oldName = $this->addIdentifierQuotes( $oldName );
return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname );
}
- function listTables( $prefix = null, $fname = 'DatabasePostgres::listTables' ) {
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
$eschema = $this->addQuotes( $this->getCoreSchema() );
$result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname );
$endArray = array();
- foreach( $result as $table ) {
+ foreach ( $result as $table ) {
$vars = get_object_vars( $table );
$table = array_pop( $vars );
- if( !$prefix || strpos( $table, $prefix ) === 0 ) {
+ if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
$endArray[] = $table;
}
}
@@ -1021,26 +1024,26 @@ __INDEXATTR__;
* @return string
*/
function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) {
- if( false === $limit ) {
- $limit = strlen( $text )-1;
+ if ( false === $limit ) {
+ $limit = strlen( $text ) - 1;
$output = array();
}
- if( '{}' == $text ) {
+ if ( '{}' == $text ) {
return $output;
}
do {
- if ( '{' != $text{$offset} ) {
+ if ( '{' != $text[$offset] ) {
preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/",
$text, $match, 0, $offset );
$offset += strlen( $match[0] );
- $output[] = ( '"' != $match[1]{0}
+ $output[] = ( '"' != $match[1][0]
? $match[1]
: stripcslashes( substr( $match[1], 1, -1 ) ) );
if ( '},' == $match[3] ) {
return $output;
}
} else {
- $offset = $this->pg_array_parse( $text, $output, $limit, $offset+1 );
+ $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 );
}
} while ( $limit > $offset );
return $output;
@@ -1056,7 +1059,7 @@ __INDEXATTR__;
/**
* @return string wikitext of a link to the server software's web site
*/
- public static function getSoftwareLink() {
+ public function getSoftwareLink() {
return '[http://www.postgresql.org/ PostgreSQL]';
}
@@ -1406,7 +1409,8 @@ SQL;
return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
}
- function setFakeMaster( $enabled = true ) {}
+ function setFakeMaster( $enabled = true ) {
+ }
function getDBname() {
return $this->mDBname;
@@ -1473,7 +1477,7 @@ SQL;
sleep( 1 );
}
}
- wfDebug( __METHOD__." failed to acquire lock\n" );
+ wfDebug( __METHOD__ . " failed to acquire lock\n" );
return false;
}
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
index 0789e1b1..4a51226f 100644
--- a/includes/db/DatabaseSqlite.php
+++ b/includes/db/DatabaseSqlite.php
@@ -52,9 +52,9 @@ class DatabaseSqlite extends DatabaseBase {
$this->mName = $dbName;
parent::__construct( $server, $user, $password, $dbName, $flags );
// parent doesn't open when $user is false, but we can work with $dbName
- if( $dbName ) {
+ if ( $dbName && !$this->isOpen() ) {
global $wgSharedDB;
- if( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) {
+ if ( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) {
$this->attachDatabase( $wgSharedDB );
}
}
@@ -68,7 +68,7 @@ class DatabaseSqlite extends DatabaseBase {
}
/**
- * @todo: check if it should be true like parent class
+ * @todo Check if it should be true like parent class
*
* @return bool
*/
@@ -90,6 +90,7 @@ class DatabaseSqlite extends DatabaseBase {
function open( $server, $user, $pass, $dbName ) {
global $wgSQLiteDataDir;
+ $this->close();
$fileName = self::generateFileName( $wgSQLiteDataDir, $dbName );
if ( !is_readable( $fileName ) ) {
$this->mConn = false;
@@ -200,7 +201,7 @@ class DatabaseSqlite extends DatabaseBase {
*
* @return ResultWrapper
*/
- function attachDatabase( $name, $file = false, $fname = 'DatabaseSqlite::attachDatabase' ) {
+ function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
global $wgSQLiteDataDir;
if ( !$file ) {
$file = self::generateFileName( $wgSQLiteDataDir, $name );
@@ -420,7 +421,7 @@ class DatabaseSqlite extends DatabaseBase {
*
* @return array
*/
- function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) {
+ function indexInfo( $table, $index, $fname = __METHOD__ ) {
$sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
$res = $this->query( $sql, $fname );
if ( !$res ) {
@@ -442,7 +443,7 @@ class DatabaseSqlite extends DatabaseBase {
* @param $fname string
* @return bool|null
*/
- function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) {
+ function indexUnique( $table, $index, $fname = __METHOD__ ) {
$row = $this->selectRow( 'sqlite_master', '*',
array(
'type' => 'index',
@@ -471,7 +472,7 @@ class DatabaseSqlite extends DatabaseBase {
*/
function makeSelectOptions( $options ) {
foreach ( $options as $k => $v ) {
- if ( is_numeric( $k ) && ($v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE') ) {
+ if ( is_numeric( $k ) && ( $v == 'FOR UPDATE' || $v == 'LOCK IN SHARE MODE' ) ) {
$options[$k] = '';
}
}
@@ -514,7 +515,7 @@ class DatabaseSqlite extends DatabaseBase {
* Based on generic method (parent) with some prior SQLite-sepcific adjustments
* @return bool
*/
- function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) {
+ function insert( $table, $a, $fname = __METHOD__, $options = array() ) {
if ( !count( $a ) ) {
return true;
}
@@ -541,8 +542,10 @@ class DatabaseSqlite extends DatabaseBase {
* @param $fname string
* @return bool|ResultWrapper
*/
- function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) {
- if ( !count( $rows ) ) return true;
+ function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+ if ( !count( $rows ) ) {
+ return true;
+ }
# SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
@@ -610,7 +613,7 @@ class DatabaseSqlite extends DatabaseBase {
/**
* @return string wikitext of a link to the server software's web site
*/
- public static function getSoftwareLink() {
+ public function getSoftwareLink() {
return "[http://sqlite.org/ SQLite]";
}
@@ -653,7 +656,11 @@ class DatabaseSqlite extends DatabaseBase {
if ( $this->mTrxLevel == 1 ) {
$this->commit( __METHOD__ );
}
- $this->mConn->beginTransaction();
+ try {
+ $this->mConn->beginTransaction();
+ } catch ( PDOException $e ) {
+ throw new DBUnexpectedError( $this, 'Error in BEGIN query: ' . $e->getMessage() );
+ }
$this->mTrxLevel = 1;
}
@@ -661,7 +668,11 @@ class DatabaseSqlite extends DatabaseBase {
if ( $this->mTrxLevel == 0 ) {
return;
}
- $this->mConn->commit();
+ try {
+ $this->mConn->commit();
+ } catch ( PDOException $e ) {
+ throw new DBUnexpectedError( $this, 'Error in COMMIT query: ' . $e->getMessage() );
+ }
$this->mTrxLevel = 0;
}
@@ -707,7 +718,9 @@ class DatabaseSqlite extends DatabaseBase {
function addQuotes( $s ) {
if ( $s instanceof Blob ) {
return "x'" . bin2hex( $s->fetch() ) . "'";
- } else if ( strpos( $s, "\0" ) !== false ) {
+ } elseif ( is_bool( $s ) ) {
+ return (int)$s;
+ } elseif ( strpos( $s, "\0" ) !== false ) {
// SQLite doesn't support \0 in strings, so use the hex representation as a workaround.
// This is a known limitation of SQLite's mprintf function which PDO should work around,
// but doesn't. I have reported this to php.net as bug #63419:
@@ -813,7 +826,7 @@ class DatabaseSqlite extends DatabaseBase {
* @param $fname string
* @return bool|ResultWrapper
*/
- function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) {
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
$res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . $this->addQuotes( $oldName ) . " AND type='table'", $fname );
$obj = $this->fetchObject( $res );
if ( !$obj ) {
@@ -839,7 +852,7 @@ class DatabaseSqlite extends DatabaseBase {
*
* @return array
*/
- function listTables( $prefix = null, $fname = 'DatabaseSqlite::listTables' ) {
+ function listTables( $prefix = null, $fname = __METHOD__ ) {
$result = $this->select(
'sqlite_master',
'name',
@@ -848,11 +861,11 @@ class DatabaseSqlite extends DatabaseBase {
$endArray = array();
- foreach( $result as $table ) {
+ foreach ( $result as $table ) {
$vars = get_object_vars( $table );
$table = array_pop( $vars );
- if( !$prefix || strpos( $table, $prefix ) === 0 ) {
+ if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
if ( strpos( $table, 'sqlite_' ) !== 0 ) {
$endArray[] = $table;
}
diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php
index 9a1c8bde..de58bab6 100644
--- a/includes/db/DatabaseUtility.php
+++ b/includes/db/DatabaseUtility.php
@@ -253,7 +253,8 @@ class FakeResultWrapper extends ResultWrapper {
$this->pos = $row;
}
- function free() {}
+ function free() {
+ }
// Callers want to be able to access fields with $this->fieldName
function fetchObject() {
diff --git a/includes/db/IORMRow.php b/includes/db/IORMRow.php
index 6bc0cdd2..39411791 100644
--- a/includes/db/IORMRow.php
+++ b/includes/db/IORMRow.php
@@ -34,20 +34,10 @@
interface IORMRow {
/**
- * Constructor.
- *
- * @since 1.20
- *
- * @param IORMTable $table
- * @param array|null $fields
- * @param boolean $loadDefaults
- */
- public function __construct( IORMTable $table, $fields = null, $loadDefaults = false );
-
- /**
* Load the specified fields from the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param array|null $fields
* @param boolean $override
@@ -74,8 +64,9 @@ interface IORMRow {
* Gets the value of a field but first loads it if not done so already.
*
* @since 1.20
+ * @deprecated since 1.22
*
- * @param string$name
+ * @param string $name
*
* @return mixed
*/
@@ -155,6 +146,7 @@ interface IORMRow {
* Load the default values, via getDefaults.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $override
*/
@@ -167,6 +159,7 @@ interface IORMRow {
* @since 1.20
*
* @param string|null $functionName
+ * @deprecated since 1.22
*
* @return boolean Success indicator
*/
@@ -176,6 +169,7 @@ interface IORMRow {
* Removes the object from the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return boolean Success indicator
*/
@@ -215,9 +209,9 @@ interface IORMRow {
/**
* Add an amount (can be negative) to the specified field (needs to be numeric).
- * TODO: most off this stuff makes more sense in the table class
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param string $field
* @param integer $amount
@@ -239,6 +233,7 @@ interface IORMRow {
* Computes and updates the values of the summary fields.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param array|string|null $summaryFields
*/
@@ -248,6 +243,7 @@ interface IORMRow {
* Sets the value for the @see $updateSummaries field.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $update
*/
@@ -257,6 +253,7 @@ interface IORMRow {
* Sets the value for the @see $inSummaryMode field.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $summaryMode
*/
@@ -266,6 +263,7 @@ interface IORMRow {
* Returns the table this IORMRow is a row in.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return IORMTable
*/
diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php
index d469e867..16c43a00 100644
--- a/includes/db/LBFactory.php
+++ b/includes/db/LBFactory.php
@@ -134,7 +134,8 @@ abstract class LBFactory {
* Prepare all tracked load balancers for shutdown
* STUB
*/
- function shutdown() {}
+ function shutdown() {
+ }
/**
* Call a method of each tracked load balancer
@@ -201,7 +202,7 @@ class LBFactory_Simple extends LBFactory {
$flags |= DBO_COMPRESS;
}
- $servers = array(array(
+ $servers = array( array(
'host' => $wgDBserver,
'user' => $wgDBuser,
'password' => $wgDBpassword,
@@ -256,6 +257,7 @@ class LBFactory_Simple extends LBFactory {
if ( !isset( $this->extLBs[$cluster] ) ) {
$this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
$this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
+ $this->chronProt->initLB( $this->extLBs[$cluster] );
}
return $this->extLBs[$cluster];
}
@@ -280,6 +282,9 @@ class LBFactory_Simple extends LBFactory {
if ( $this->mainLB ) {
$this->chronProt->shutdownLB( $this->mainLB );
}
+ foreach ( $this->extLBs as $extLB ) {
+ $this->chronProt->shutdownLB( $extLB );
+ }
$this->chronProt->shutdown();
$this->commitMasterChanges();
}
@@ -292,21 +297,27 @@ class LBFactory_Simple extends LBFactory {
* LBFactory::enableBackend() to return to normal behavior
*/
class LBFactory_Fake extends LBFactory {
- function __construct( $conf ) {}
+ function __construct( $conf ) {
+ }
- function newMainLB( $wiki = false) {
+ function newMainLB( $wiki = false ) {
throw new DBAccessError;
}
+
function getMainLB( $wiki = false ) {
throw new DBAccessError;
}
+
function newExternalLB( $cluster, $wiki = false ) {
throw new DBAccessError;
}
+
function &getExternalLB( $cluster, $wiki = false ) {
throw new DBAccessError;
}
- function forEachLB( $callback, $params = array() ) {}
+
+ function forEachLB( $callback, $params = array() ) {
+ }
}
/**
@@ -317,76 +328,3 @@ class DBAccessError extends MWException {
parent::__construct( "Mediawiki tried to access the database via wfGetDB(). This is not allowed." );
}
}
-
-/**
- * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
- * Kind of like Hawking's [[Chronology Protection Agency]].
- */
-class ChronologyProtector {
- var $startupPos;
- var $shutdownPos = array();
-
- /**
- * Initialise a LoadBalancer to give it appropriate chronology protection.
- *
- * @param $lb LoadBalancer
- */
- function initLB( $lb ) {
- if ( $this->startupPos === null ) {
- if ( !empty( $_SESSION[__CLASS__] ) ) {
- $this->startupPos = $_SESSION[__CLASS__];
- }
- }
- if ( !$this->startupPos ) {
- return;
- }
- $masterName = $lb->getServerName( 0 );
-
- if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) {
- $info = $lb->parentInfo();
- $pos = $this->startupPos[$masterName];
- wfDebug( __METHOD__ . ": LB " . $info['id'] . " waiting for master pos $pos\n" );
- $lb->waitFor( $this->startupPos[$masterName] );
- }
- }
-
- /**
- * Notify the ChronologyProtector that the LoadBalancer is about to shut
- * down. Saves replication positions.
- *
- * @param $lb LoadBalancer
- */
- function shutdownLB( $lb ) {
- // Don't start a session, don't bother with non-replicated setups
- if ( strval( session_id() ) == '' || $lb->getServerCount() <= 1 ) {
- return;
- }
- $masterName = $lb->getServerName( 0 );
- if ( isset( $this->shutdownPos[$masterName] ) ) {
- // Already done
- return;
- }
- // Only save the position if writes have been done on the connection
- $db = $lb->getAnyOpenConnection( 0 );
- $info = $lb->parentInfo();
- if ( !$db || !$db->doneWrites() ) {
- wfDebug( __METHOD__ . ": LB {$info['id']}, no writes done\n" );
- return;
- }
- $pos = $db->getMasterPos();
- wfDebug( __METHOD__ . ": LB {$info['id']} has master pos $pos\n" );
- $this->shutdownPos[$masterName] = $pos;
- }
-
- /**
- * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
- * May commit chronology data to persistent storage.
- */
- function shutdown() {
- if ( session_id() != '' && count( $this->shutdownPos ) ) {
- wfDebug( __METHOD__ . ": saving master pos for " .
- count( $this->shutdownPos ) . " master(s)\n" );
- $_SESSION[__CLASS__] = $this->shutdownPos;
- }
- }
-}
diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php
index 2e4963d4..3043946a 100644
--- a/includes/db/LBFactory_Multi.php
+++ b/includes/db/LBFactory_Multi.php
@@ -145,8 +145,8 @@ class LBFactory_Multi extends LBFactory {
$section = $this->getSectionForWiki( $wiki );
if ( !isset( $this->mainLBs[$section] ) ) {
$lb = $this->newMainLB( $wiki, $section );
- $this->chronProt->initLB( $lb );
$lb->parentInfo( array( 'id' => "main-$section" ) );
+ $this->chronProt->initLB( $lb );
$this->mainLBs[$section] = $lb;
}
return $this->mainLBs[$section];
@@ -181,6 +181,7 @@ class LBFactory_Multi extends LBFactory {
if ( !isset( $this->extLBs[$cluster] ) ) {
$this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki );
$this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
+ $this->chronProt->initLB( $this->extLBs[$cluster] );
}
return $this->extLBs[$cluster];
}
@@ -296,6 +297,9 @@ class LBFactory_Multi extends LBFactory {
foreach ( $this->mainLBs as $lb ) {
$this->chronProt->shutdownLB( $lb );
}
+ foreach ( $this->extLBs as $extLB ) {
+ $this->chronProt->shutdownLB( $extLB );
+ }
$this->chronProt->shutdown();
$this->commitMasterChanges();
}
diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php
index 1e859278..857109db 100644
--- a/includes/db/LoadBalancer.php
+++ b/includes/db/LoadBalancer.php
@@ -78,7 +78,7 @@ class LoadBalancer {
}
}
- foreach( $params['servers'] as $i => $server ) {
+ foreach ( $params['servers'] as $i => $server ) {
$this->mLoads[$i] = $server['load'];
if ( isset( $server['groupLoads'] ) ) {
foreach ( $server['groupLoads'] as $group => $ratio ) {
@@ -117,7 +117,7 @@ class LoadBalancer {
* Given an array of non-normalised probabilities, this function will select
* an element and return the appropriate key
*
- * @deprecated 1.21, use ArrayUtils::pickRandom()
+ * @deprecated since 1.21, use ArrayUtils::pickRandom()
*
* @param $weights array
*
@@ -167,7 +167,7 @@ class LoadBalancer {
#wfDebugLog( 'connect', var_export( $loads, true ) );
# Return a random representative of the remainder
- return $this->pickRandom( $loads );
+ return ArrayUtils::pickRandom( $loads );
}
/**
@@ -219,6 +219,7 @@ class LoadBalancer {
}
if ( !$nonErrorLoads ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( "Empty server array given to LoadBalancer" );
}
@@ -235,7 +236,7 @@ class LoadBalancer {
$currentLoads = $nonErrorLoads;
while ( count( $currentLoads ) ) {
if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) {
- $i = $this->pickRandom( $currentLoads );
+ $i = ArrayUtils::pickRandom( $currentLoads );
} else {
$i = $this->getRandomNonLagged( $currentLoads, $wiki );
if ( $i === false && count( $currentLoads ) != 0 ) {
@@ -243,7 +244,7 @@ class LoadBalancer {
wfDebugLog( 'replication', "All slaves lagged. Switch to read-only mode\n" );
$wgReadOnly = 'The database has been automatically locked ' .
'while the slave database servers catch up to the master';
- $i = $this->pickRandom( $currentLoads );
+ $i = ArrayUtils::pickRandom( $currentLoads );
$laggedSlaveMode = true;
}
}
@@ -348,7 +349,7 @@ class LoadBalancer {
* Set the master wait position
* If a DB_SLAVE connection has been opened already, waits
* Otherwise sets a variable telling it to wait if such a connection is opened
- * @param $pos int
+ * @param $pos DBMasterPos
*/
public function waitFor( $pos ) {
wfProfileIn( __METHOD__ );
@@ -366,7 +367,7 @@ class LoadBalancer {
/**
* Set the master wait position and wait for ALL slaves to catch up to it
- * @param $pos int
+ * @param $pos DBMasterPos
*/
public function waitForAll( $pos ) {
wfProfileIn( __METHOD__ );
@@ -399,7 +400,7 @@ class LoadBalancer {
* @param $open bool
* @return bool
*/
- function doWait( $index, $open = false ) {
+ protected function doWait( $index, $open = false ) {
# Find a connection to wait on
$conn = $this->getAnyOpenConnection( $index );
if ( !$conn ) {
@@ -407,7 +408,7 @@ class LoadBalancer {
wfDebug( __METHOD__ . ": no connection open\n" );
return false;
} else {
- $conn = $this->openConnection( $index );
+ $conn = $this->openConnection( $index, '' );
if ( !$conn ) {
wfDebug( __METHOD__ . ": failed to open connection\n" );
return false;
@@ -443,8 +444,10 @@ class LoadBalancer {
wfProfileIn( __METHOD__ );
if ( $i == DB_LAST ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( 'Attempt to call ' . __METHOD__ . ' with deprecated server index DB_LAST' );
} elseif ( $i === null || $i === false ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( 'Attempt to call ' . __METHOD__ . ' with invalid server index' );
}
@@ -476,6 +479,7 @@ class LoadBalancer {
# Operation-based index
if ( $i == DB_SLAVE ) {
+ $this->mLastError = 'Unknown error'; // reset error string
$i = $this->getReaderIndex( false, $wiki );
# Couldn't find a working server in getReaderIndex()?
if ( $i === false ) {
@@ -542,6 +546,38 @@ class LoadBalancer {
}
/**
+ * Get a database connection handle reference
+ *
+ * The handle's methods wrap simply wrap those of a DatabaseBase handle
+ *
+ * @see LoadBalancer::getConnection() for parameter information
+ *
+ * @param integer $db
+ * @param mixed $groups
+ * @param string $wiki
+ * @return DBConnRef
+ */
+ public function getConnectionRef( $db, $groups = array(), $wiki = false ) {
+ return new DBConnRef( $this, $this->getConnection( $db, $groups, $wiki ) );
+ }
+
+ /**
+ * Get a database connection handle reference without connecting yet
+ *
+ * The handle's methods wrap simply wrap those of a DatabaseBase handle
+ *
+ * @see LoadBalancer::getConnection() for parameter information
+ *
+ * @param integer $db
+ * @param mixed $groups
+ * @param string $wiki
+ * @return DBConnRef
+ */
+ public function getLazyConnectionRef( $db, $groups = array(), $wiki = false ) {
+ return new DBConnRef( $this, array( $db, $groups, $wiki ) );
+ }
+
+ /**
* Open a connection to the server given by the specified index
* Index must be an actual index into the array.
* If the server is already open, returns it.
@@ -633,6 +669,7 @@ class LoadBalancer {
$server = $this->mServers[$i];
$server['serverIndex'] = $i;
$server['foreignPoolRefCount'] = 0;
+ $server['foreign'] = true;
$conn = $this->reallyOpenConnection( $server, $dbName );
if ( !$conn->isOpen() ) {
wfDebug( __METHOD__ . ": error opening connection for $i/$wiki\n" );
@@ -662,7 +699,7 @@ class LoadBalancer {
* @return bool
*/
function isOpen( $index ) {
- if( !is_integer( $index ) ) {
+ if ( !is_integer( $index ) ) {
return false;
}
return (bool)$this->getAnyOpenConnection( $index );
@@ -679,7 +716,7 @@ class LoadBalancer {
* @return DatabaseBase
*/
function reallyOpenConnection( $server, $dbNameOverride = false ) {
- if( !is_array( $server ) ) {
+ if ( !is_array( $server ) ) {
throw new MWException( 'You must update your load-balancing configuration. ' .
'See DefaultSettings.php entry for $wgDBservers.' );
}
@@ -830,7 +867,7 @@ class LoadBalancer {
*/
function closeAll() {
foreach ( $this->mConns as $conns2 ) {
- foreach ( $conns2 as $conns3 ) {
+ foreach ( $conns2 as $conns3 ) {
foreach ( $conns3 as $conn ) {
$conn->close();
}
@@ -1064,3 +1101,46 @@ class LoadBalancer {
$this->mLagTimes = null;
}
}
+
+/**
+ * Helper class to handle automatically marking connectons as reusable (via RAII pattern)
+ * as well handling deferring the actual network connection until the handle is used
+ *
+ * @ingroup Database
+ * @since 1.22
+ */
+class DBConnRef implements IDatabase {
+ /** @var LoadBalancer */
+ protected $lb;
+ /** @var DatabaseBase|null */
+ protected $conn;
+ /** @var Array|null */
+ protected $params;
+
+ /**
+ * @param $lb LoadBalancer
+ * @param $conn DatabaseBase|array Connection or (server index, group, wiki ID) array
+ */
+ public function __construct( LoadBalancer $lb, $conn ) {
+ $this->lb = $lb;
+ if ( $conn instanceof DatabaseBase ) {
+ $this->conn = $conn;
+ } else {
+ $this->params = $conn;
+ }
+ }
+
+ public function __call( $name, $arguments ) {
+ if ( $this->conn === null ) {
+ list( $db, $groups, $wiki ) = $this->params;
+ $this->conn = $this->lb->getConnection( $db, $groups, $wiki );
+ }
+ return call_user_func_array( array( $this->conn, $name ), $arguments );
+ }
+
+ function __destruct() {
+ if ( $this->conn !== null ) {
+ $this->lb->reuseConnection( $this->conn );
+ }
+ }
+}
diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php
index ad7b3b2f..519e2dfd 100644
--- a/includes/db/LoadMonitor.php
+++ b/includes/db/LoadMonitor.php
@@ -135,18 +135,19 @@ class LoadMonitor_MySQL implements LoadMonitor {
$requestRate = 10;
global $wgMemc;
- if ( empty( $wgMemc ) )
+ if ( empty( $wgMemc ) ) {
$wgMemc = wfGetMainCache();
+ }
$masterName = $this->parent->getServerName( 0 );
$memcKey = wfMemcKey( 'lag_times', $masterName );
$times = $wgMemc->get( $memcKey );
- if ( $times ) {
+ if ( is_array( $times ) ) {
# Randomly recache with probability rising over $expiry
$elapsed = time() - $times['timestamp'];
$chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
if ( mt_rand( 0, $chance ) != 0 ) {
- unset( $times['timestamp'] );
+ unset( $times['timestamp'] ); // hide from caller
wfProfileOut( __METHOD__ );
return $times;
}
@@ -156,6 +157,17 @@ class LoadMonitor_MySQL implements LoadMonitor {
}
# Cache key missing or expired
+ if ( $wgMemc->add( "$memcKey:lock", 1, 10 ) ) {
+ # Let this process alone update the cache value
+ $unlocker = new ScopedCallback( function() use ( $wgMemc, $memcKey ) {
+ $wgMemc->delete( $memcKey );
+ } );
+ } elseif ( is_array( $times ) ) {
+ # Could not acquire lock but an old cache exists, so use it
+ unset( $times['timestamp'] ); // hide from caller
+ wfProfileOut( __METHOD__ );
+ return $times;
+ }
$times = array();
foreach ( $serverIndexes as $i ) {
@@ -170,14 +182,11 @@ class LoadMonitor_MySQL implements LoadMonitor {
# Add a timestamp key so we know when it was cached
$times['timestamp'] = time();
- $wgMemc->set( $memcKey, $times, $expiry );
-
- # But don't give the timestamp to the caller
- unset( $times['timestamp'] );
- $lagTimes = $times;
+ $wgMemc->set( $memcKey, $times, $expiry + 10 );
+ unset( $times['timestamp'] ); // hide from caller
wfProfileOut( __METHOD__ );
- return $lagTimes;
+ return $times;
}
/**
diff --git a/includes/db/ORMRow.php b/includes/db/ORMRow.php
index 6c1f27ff..5ce3794d 100644
--- a/includes/db/ORMRow.php
+++ b/includes/db/ORMRow.php
@@ -43,17 +43,12 @@ class ORMRow implements IORMRow {
protected $fields = array( 'id' => null );
/**
- * @since 1.20
- * @var ORMTable
- */
- protected $table;
-
- /**
* If the object should update summaries of linked items when changed.
* For example, update the course_count field in universities when a course in courses is deleted.
* Settings this to false can prevent needless updating work in situations
* such as deleting a university, which will then delete all it's courses.
*
+ * @deprecated since 1.22
* @since 1.20
* @var bool
*/
@@ -64,21 +59,29 @@ class ORMRow implements IORMRow {
* This mode indicates that only summary fields got updated,
* which allows for optimizations.
*
+ * @deprecated since 1.22
* @since 1.20
* @var bool
*/
protected $inSummaryMode = false;
/**
+ * @deprecated since 1.22
+ * @since 1.20
+ * @var ORMTable|null
+ */
+ protected $table;
+
+ /**
* Constructor.
*
* @since 1.20
*
- * @param IORMTable $table
+ * @param IORMTable|null $table Deprecated since 1.22
* @param array|null $fields
- * @param boolean $loadDefaults
+ * @param boolean $loadDefaults Deprecated since 1.22
*/
- public function __construct( IORMTable $table, $fields = null, $loadDefaults = false ) {
+ public function __construct( IORMTable $table = null, $fields = null, $loadDefaults = false ) {
$this->table = $table;
if ( !is_array( $fields ) ) {
@@ -96,6 +99,7 @@ class ORMRow implements IORMRow {
* Load the specified fields from the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param array|null $fields
* @param boolean $override
@@ -160,6 +164,7 @@ class ORMRow implements IORMRow {
* Gets the value of a field but first loads it if not done so already.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param $name string
*
@@ -232,25 +237,10 @@ class ORMRow implements IORMRow {
}
/**
- * Sets multiple fields.
- *
- * @since 1.20
- *
- * @param array $fields The fields to set
- * @param boolean $override Override already set fields with the provided values?
- */
- public function setFields( array $fields, $override = true ) {
- foreach ( $fields as $name => $value ) {
- if ( $override || !$this->hasField( $name ) ) {
- $this->setField( $name, $value );
- }
- }
- }
-
- /**
* Gets the fields => values to write to the table.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return array
*/
@@ -269,10 +259,10 @@ class ORMRow implements IORMRow {
switch ( $type ) {
case 'array':
$value = (array)$value;
- // fall-through!
+ // fall-through!
case 'blob':
$value = serialize( $value );
- // fall-through!
+ // fall-through!
}
$values[$this->table->getPrefixedField( $name )] = $value;
@@ -283,6 +273,22 @@ class ORMRow implements IORMRow {
}
/**
+ * Sets multiple fields.
+ *
+ * @since 1.20
+ *
+ * @param array $fields The fields to set
+ * @param boolean $override Override already set fields with the provided values?
+ */
+ public function setFields( array $fields, $override = true ) {
+ foreach ( $fields as $name => $value ) {
+ if ( $override || !$this->hasField( $name ) ) {
+ $this->setField( $name, $value );
+ }
+ }
+ }
+
+ /**
* Serializes the object to an associative array which
* can then easily be converted into JSON or similar.
*
@@ -320,6 +326,7 @@ class ORMRow implements IORMRow {
* Load the default values, via getDefaults.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $override
*/
@@ -332,6 +339,7 @@ class ORMRow implements IORMRow {
* when it already exists, or inserting it when it doesn't.
*
* @since 1.20
+ * @deprecated since 1.22 Use IORMTable->updateRow or ->insertRow
*
* @param string|null $functionName
*
@@ -339,9 +347,9 @@ class ORMRow implements IORMRow {
*/
public function save( $functionName = null ) {
if ( $this->hasIdField() ) {
- return $this->saveExisting( $functionName );
+ return $this->table->updateRow( $this, $functionName );
} else {
- return $this->insert( $functionName );
+ return $this->table->insertRow( $this, $functionName );
}
}
@@ -349,6 +357,7 @@ class ORMRow implements IORMRow {
* Updates the object in the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param string|null $functionName
*
@@ -386,6 +395,7 @@ class ORMRow implements IORMRow {
* Inserts the object into the database.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param string|null $functionName
* @param array|null $options
@@ -418,16 +428,14 @@ class ORMRow implements IORMRow {
* Removes the object from the database.
*
* @since 1.20
+ * @deprecated since 1.22, use IORMTable->removeRow
*
* @return boolean Success indicator
*/
public function remove() {
$this->beforeRemove();
- $success = $this->table->delete( array( 'id' => $this->getId() ), __METHOD__ );
-
- // DatabaseBase::delete does not always return true for success as documented...
- $success = $success !== false;
+ $success = $this->table->removeRow( $this, __METHOD__ );
if ( $success ) {
$this->onRemoved();
@@ -440,6 +448,7 @@ class ORMRow implements IORMRow {
* Gets called before an object is removed from the database.
*
* @since 1.20
+ * @deprecated since 1.22
*/
protected function beforeRemove() {
$this->loadFields( $this->getBeforeRemoveFields(), false, true );
@@ -463,6 +472,7 @@ class ORMRow implements IORMRow {
* Can be overridden to get rid of linked data.
*
* @since 1.20
+ * @deprecated since 1.22
*/
protected function onRemoved() {
$this->setField( 'id', null );
@@ -503,51 +513,14 @@ class ORMRow implements IORMRow {
* @throws MWException
*/
public function setField( $name, $value ) {
- $fields = $this->table->getFields();
-
- if ( array_key_exists( $name, $fields ) ) {
- switch ( $fields[$name] ) {
- case 'int':
- $value = (int)$value;
- break;
- case 'float':
- $value = (float)$value;
- break;
- case 'bool':
- $value = (bool)$value;
- break;
- case 'array':
- if ( is_string( $value ) ) {
- $value = unserialize( $value );
- }
-
- if ( !is_array( $value ) ) {
- $value = array();
- }
- break;
- case 'blob':
- if ( is_string( $value ) ) {
- $value = unserialize( $value );
- }
- break;
- case 'id':
- if ( is_string( $value ) ) {
- $value = (int)$value;
- }
- break;
- }
-
- $this->fields[$name] = $value;
- } else {
- throw new MWException( 'Attempted to set unknown field ' . $name );
- }
+ $this->fields[$name] = $value;
}
/**
* Add an amount (can be negative) to the specified field (needs to be numeric).
- * TODO: most off this stuff makes more sense in the table class
*
* @since 1.20
+ * @deprecated since 1.22, use IORMTable->addToField
*
* @param string $field
* @param integer $amount
@@ -555,41 +528,14 @@ class ORMRow implements IORMRow {
* @return boolean Success indicator
*/
public function addToField( $field, $amount ) {
- if ( $amount == 0 ) {
- return true;
- }
-
- if ( !$this->hasIdField() ) {
- return false;
- }
-
- $absoluteAmount = abs( $amount );
- $isNegative = $amount < 0;
-
- $dbw = $this->table->getWriteDbConnection();
-
- $fullField = $this->table->getPrefixedField( $field );
-
- $success = $dbw->update(
- $this->table->getName(),
- array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ),
- array( $this->table->getPrefixedField( 'id' ) => $this->getId() ),
- __METHOD__
- );
-
- if ( $success && $this->hasField( $field ) ) {
- $this->setField( $field, $this->getField( $field ) + $amount );
- }
-
- $this->table->releaseConnection( $dbw );
-
- return $success;
+ return $this->table->addToField( $this->getUpdateConditions(), $field, $amount );
}
/**
* Return the names of the fields.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return array
*/
@@ -601,6 +547,7 @@ class ORMRow implements IORMRow {
* Computes and updates the values of the summary fields.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param array|string|null $summaryFields
*/
@@ -612,6 +559,7 @@ class ORMRow implements IORMRow {
* Sets the value for the @see $updateSummaries field.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $update
*/
@@ -623,6 +571,7 @@ class ORMRow implements IORMRow {
* Sets the value for the @see $inSummaryMode field.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @param boolean $summaryMode
*/
@@ -631,39 +580,10 @@ class ORMRow implements IORMRow {
}
/**
- * Return if any fields got changed.
- *
- * @since 1.20
- *
- * @param IORMRow $object
- * @param boolean|array $excludeSummaryFields
- * When set to true, summary field changes are ignored.
- * Can also be an array of fields to ignore.
- *
- * @return boolean
- */
- protected function fieldsChanged( IORMRow $object, $excludeSummaryFields = false ) {
- $exclusionFields = array();
-
- if ( $excludeSummaryFields !== false ) {
- $exclusionFields = is_array( $excludeSummaryFields ) ? $excludeSummaryFields : $this->table->getSummaryFields();
- }
-
- foreach ( $this->fields as $name => $value ) {
- $excluded = $excludeSummaryFields && in_array( $name, $exclusionFields );
-
- if ( !$excluded && $object->getField( $name ) !== $value ) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
* Returns the table this IORMRow is a row in.
*
* @since 1.20
+ * @deprecated since 1.22
*
* @return IORMTable
*/
diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php
index bcbe94a3..5f6723b9 100644
--- a/includes/db/ORMTable.php
+++ b/includes/db/ORMTable.php
@@ -814,10 +814,59 @@ class ORMTable extends DBAccessBase implements IORMTable {
*/
public function getFieldsFromDBResult( stdClass $result ) {
$result = (array)$result;
- return array_combine(
+
+ $rawFields = array_combine(
$this->unprefixFieldNames( array_keys( $result ) ),
array_values( $result )
);
+
+ $fieldDefinitions = $this->getFields();
+ $fields = array();
+
+ foreach ( $rawFields as $name => $value ) {
+ if ( array_key_exists( $name, $fieldDefinitions ) ) {
+ switch ( $fieldDefinitions[$name] ) {
+ case 'int':
+ $value = (int)$value;
+ break;
+ case 'float':
+ $value = (float)$value;
+ break;
+ case 'bool':
+ if ( is_string( $value ) ) {
+ $value = $value !== '0';
+ } elseif ( is_int( $value ) ) {
+ $value = $value !== 0;
+ }
+ break;
+ case 'array':
+ if ( is_string( $value ) ) {
+ $value = unserialize( $value );
+ }
+
+ if ( !is_array( $value ) ) {
+ $value = array();
+ }
+ break;
+ case 'blob':
+ if ( is_string( $value ) ) {
+ $value = unserialize( $value );
+ }
+ break;
+ case 'id':
+ if ( is_string( $value ) ) {
+ $value = (int)$value;
+ }
+ break;
+ }
+
+ $fields[$name] = $value;
+ } else {
+ throw new MWException( 'Attempted to set unknown field ' . $name );
+ }
+ }
+
+ return $fields;
}
/**
@@ -867,14 +916,15 @@ class ORMTable extends DBAccessBase implements IORMTable {
*
* @since 1.20
*
- * @param array $data
+ * @param array $fields
* @param boolean $loadDefaults
*
* @return IORMRow
*/
- public function newRow( array $data, $loadDefaults = false ) {
+ public function newRow( array $fields, $loadDefaults = false ) {
$class = $this->getRowClass();
- return new $class( $this, $data, $loadDefaults );
+
+ return new $class( $this, $fields, $loadDefaults );
}
/**
@@ -901,4 +951,157 @@ class ORMTable extends DBAccessBase implements IORMTable {
return array_key_exists( $name, $this->getFields() );
}
+ /**
+ * Updates the provided row in the database.
+ *
+ * @since 1.22
+ *
+ * @param IORMRow $row The row to save
+ * @param string|null $functionName
+ *
+ * @return boolean Success indicator
+ */
+ public function updateRow( IORMRow $row, $functionName = null ) {
+ $dbw = $this->getWriteDbConnection();
+
+ $success = $dbw->update(
+ $this->getName(),
+ $this->getWriteValues( $row ),
+ $this->getPrefixedValues( array( 'id' => $row->getId() ) ),
+ is_null( $functionName ) ? __METHOD__ : $functionName
+ );
+
+ $this->releaseConnection( $dbw );
+
+ // DatabaseBase::update does not always return true for success as documented...
+ return $success !== false;
+ }
+
+ /**
+ * Inserts the provided row into the database.
+ *
+ * @since 1.22
+ *
+ * @param IORMRow $row
+ * @param string|null $functionName
+ * @param array|null $options
+ *
+ * @return boolean Success indicator
+ */
+ public function insertRow( IORMRow $row, $functionName = null, array $options = null ) {
+ $dbw = $this->getWriteDbConnection();
+
+ $success = $dbw->insert(
+ $this->getName(),
+ $this->getWriteValues( $row ),
+ is_null( $functionName ) ? __METHOD__ : $functionName,
+ $options
+ );
+
+ // DatabaseBase::insert does not always return true for success as documented...
+ $success = $success !== false;
+
+ if ( $success ) {
+ $row->setField( 'id', $dbw->insertId() );
+ }
+
+ $this->releaseConnection( $dbw );
+
+ return $success;
+ }
+
+ /**
+ * Gets the fields => values to write to the table.
+ *
+ * @since 1.22
+ *
+ * @param IORMRow $row
+ *
+ * @return array
+ */
+ protected function getWriteValues( IORMRow $row ) {
+ $values = array();
+
+ $rowFields = $row->getFields();
+
+ foreach ( $this->getFields() as $name => $type ) {
+ if ( array_key_exists( $name, $rowFields ) ) {
+ $value = $rowFields[$name];
+
+ switch ( $type ) {
+ case 'array':
+ $value = (array)$value;
+ // fall-through!
+ case 'blob':
+ $value = serialize( $value );
+ // fall-through!
+ }
+
+ $values[$this->getPrefixedField( $name )] = $value;
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Removes the provided row from the database.
+ *
+ * @since 1.22
+ *
+ * @param IORMRow $row
+ * @param string|null $functionName
+ *
+ * @return boolean Success indicator
+ */
+ public function removeRow( IORMRow $row, $functionName = null ) {
+ $success = $this->delete(
+ array( 'id' => $row->getId() ),
+ is_null( $functionName ) ? __METHOD__ : $functionName
+ );
+
+ // DatabaseBase::delete does not always return true for success as documented...
+ return $success !== false;
+ }
+
+ /**
+ * Add an amount (can be negative) to the specified field (needs to be numeric).
+ *
+ * @since 1.22
+ *
+ * @param array $conditions
+ * @param string $field
+ * @param integer $amount
+ *
+ * @return boolean Success indicator
+ * @throws MWException
+ */
+ public function addToField( array $conditions, $field, $amount ) {
+ if ( !array_key_exists( $field, $this->fields ) ) {
+ throw new MWException( 'Unknown field "' . $field . '" provided' );
+ }
+
+ if ( $amount == 0 ) {
+ return true;
+ }
+
+ $absoluteAmount = abs( $amount );
+ $isNegative = $amount < 0;
+
+ $fullField = $this->getPrefixedField( $field );
+
+ $dbw = $this->getWriteDbConnection();
+
+ $success = $dbw->update(
+ $this->getName(),
+ array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ),
+ $this->getPrefixedValues( $conditions ),
+ __METHOD__
+ ) !== false; // DatabaseBase::update does not always return true for success as documented...
+
+ $this->releaseConnection( $dbw );
+
+ return $success;
+ }
+
}