diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2013-12-08 09:55:49 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2013-12-08 09:55:49 +0100 |
commit | 4ac9fa081a7c045f6a9f1cfc529d82423f485b2e (patch) | |
tree | af68743f2f4a47d13f2b0eb05f5c4aaf86d8ea37 /includes/db | |
parent | af4da56f1ad4d3ef7b06557bae365da2ea27a897 (diff) |
Update to MediaWiki 1.22.0
Diffstat (limited to 'includes/db')
-rw-r--r-- | includes/db/ChronologyProtector.php | 106 | ||||
-rw-r--r-- | includes/db/CloneDatabase.php | 18 | ||||
-rw-r--r-- | includes/db/Database.php | 517 | ||||
-rw-r--r-- | includes/db/DatabaseError.php | 202 | ||||
-rw-r--r-- | includes/db/DatabaseMssql.php | 190 | ||||
-rw-r--r-- | includes/db/DatabaseMysql.php | 933 | ||||
-rw-r--r-- | includes/db/DatabaseMysqlBase.php | 1154 | ||||
-rw-r--r-- | includes/db/DatabaseMysqli.php | 194 | ||||
-rw-r--r-- | includes/db/DatabaseOracle.php | 92 | ||||
-rw-r--r-- | includes/db/DatabasePostgres.php | 98 | ||||
-rw-r--r-- | includes/db/DatabaseSqlite.php | 49 | ||||
-rw-r--r-- | includes/db/DatabaseUtility.php | 3 | ||||
-rw-r--r-- | includes/db/IORMRow.php | 24 | ||||
-rw-r--r-- | includes/db/LBFactory.php | 94 | ||||
-rw-r--r-- | includes/db/LBFactory_Multi.php | 6 | ||||
-rw-r--r-- | includes/db/LoadBalancer.php | 104 | ||||
-rw-r--r-- | includes/db/LoadMonitor.php | 27 | ||||
-rw-r--r-- | includes/db/ORMRow.php | 182 | ||||
-rw-r--r-- | includes/db/ORMTable.php | 211 |
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; + } + } |