diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2014-12-27 15:41:37 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2014-12-31 11:43:28 +0100 |
commit | c1f9b1f7b1b77776192048005dcc66dcf3df2bfb (patch) | |
tree | 2b38796e738dd74cb42ecd9bfd151803108386bc /includes/db | |
parent | b88ab0086858470dd1f644e64cb4e4f62bb2be9b (diff) |
Update to MediaWiki 1.24.1
Diffstat (limited to 'includes/db')
23 files changed, 3849 insertions, 2277 deletions
diff --git a/includes/db/ChronologyProtector.php b/includes/db/ChronologyProtector.php index de5e72c3..0c7b612e 100644 --- a/includes/db/ChronologyProtector.php +++ b/includes/db/ChronologyProtector.php @@ -26,12 +26,14 @@ * Kind of like Hawking's [[Chronology Protection Agency]]. */ class ChronologyProtector { - /** @var Array (DB master name => position) */ + /** @var array (DB master name => position) */ protected $startupPositions = array(); - /** @var Array (DB master name => position) */ + + /** @var array (DB master name => position) */ protected $shutdownPositions = array(); - protected $initialized = false; // bool; whether the session data was loaded + /** @var bool Whether the session data was loaded */ + protected $initialized = false; /** * Initialise a LoadBalancer to give it appropriate chronology protection. @@ -41,7 +43,7 @@ class ChronologyProtector { * to that position by delaying execution. The delay may timeout and allow stale * data if no non-lagged slaves are available. * - * @param $lb LoadBalancer + * @param LoadBalancer $lb * @return void */ public function initLB( LoadBalancer $lb ) { @@ -67,7 +69,7 @@ class ChronologyProtector { * Notify the ChronologyProtector that the LoadBalancer is about to shut * down. Saves replication positions. * - * @param $lb LoadBalancer + * @param LoadBalancer $lb * @return void */ public function shutdownLB( LoadBalancer $lb ) { @@ -83,6 +85,7 @@ class ChronologyProtector { $info = $lb->parentInfo(); if ( !$db || !$db->doneWrites() ) { wfDebug( __METHOD__ . ": LB {$info['id']}, no writes done\n" ); + return; } $pos = $db->getMasterPos(); diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index 819925cb..9eb3e2fa 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -3,7 +3,7 @@ * Helper class for making a copy of the database, mostly for unit testing. * * Copyright © 2010 Chad Horohoe <chad@anyonecanedit.org> - * http://www.mediawiki.org/ + * https://www.mediawiki.org/ * * 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 @@ -25,49 +25,33 @@ */ class CloneDatabase { - - /** - * Table prefix for cloning - * @var String - */ + /** @var string Table prefix for cloning */ private $newTablePrefix = ''; - /** - * Current table prefix - * @var String - */ + /** @var string Current table prefix */ private $oldTablePrefix = ''; - /** - * List of tables to be cloned - * @var Array - */ + /** @var array List of tables to be cloned */ private $tablesToClone = array(); - /** - * Should we DROP tables containing the new names? - * @var Bool - */ + /** @var bool Should we DROP tables containing the new names? */ private $dropCurrentTables = true; - /** - * Whether to use temporary tables or not - * @var Bool - */ + /** @var bool Whether to use temporary tables or not */ private $useTemporaryTables = true; /** * Constructor * - * @param $db DatabaseBase A database subclass + * @param DatabaseBase $db A database subclass * @param array $tablesToClone An array of tables to clone, unprefixed * @param string $newTablePrefix Prefix to assign to the tables * @param string $oldTablePrefix Prefix on current tables, if not $wgDBprefix - * @param $dropCurrentTables bool + * @param bool $dropCurrentTables */ public function __construct( DatabaseBase $db, array $tablesToClone, - $newTablePrefix, $oldTablePrefix = '', $dropCurrentTables = true ) - { + $newTablePrefix, $oldTablePrefix = '', $dropCurrentTables = true + ) { $this->db = $db; $this->tablesToClone = $tablesToClone; $this->newTablePrefix = $newTablePrefix; @@ -87,7 +71,13 @@ class CloneDatabase { * Clone the table structure */ public function cloneTableStructure() { + global $wgSharedTables, $wgSharedDB; foreach ( $this->tablesToClone as $tbl ) { + if ( $wgSharedDB && in_array( $tbl, $wgSharedTables, true ) ) { + // Shared tables don't work properly when cloning due to + // how prefixes are handled (bug 65654) + throw new MWException( "Cannot clone shared table $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,14 +88,21 @@ 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' ) ) + ) { + if ( $oldTableName === $newTableName ) { + // Last ditch check to avoid data loss + throw new MWException( "Not dropping new table, as '$newTableName'" + . " is name of both the old and the new table." ); + } $this->db->dropTable( $tbl, __METHOD__ ); - wfDebug( __METHOD__ . " dropping {$newTableName}\n", true ); + wfDebug( __METHOD__ . " dropping {$newTableName}\n" ); //Dropping the oldTable because the prefix was changed } # Create new table - wfDebug( __METHOD__ . " duplicating $oldTableName to $newTableName\n", true ); + wfDebug( __METHOD__ . " duplicating $oldTableName to $newTableName\n" ); $this->db->duplicateTableStructure( $oldTableName, $newTableName, $this->useTemporaryTables ); } } @@ -127,7 +124,7 @@ class CloneDatabase { /** * Change the table prefix on all open DB connections/ * - * @param $prefix + * @param string $prefix * @return void */ public static function changePrefix( $prefix ) { @@ -137,8 +134,8 @@ class CloneDatabase { } /** - * @param $lb LoadBalancer - * @param $prefix + * @param LoadBalancer $lb + * @param string $prefix * @return void */ public static function changeLBPrefix( $lb, $prefix ) { @@ -146,8 +143,8 @@ class CloneDatabase { } /** - * @param $db DatabaseBase - * @param $prefix + * @param DatabaseBase $db + * @param string $prefix * @return void */ public static function changeDBPrefix( $db, $prefix ) { diff --git a/includes/db/Database.php b/includes/db/Database.php index 10645608..9b783a99 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -1,4 +1,5 @@ <?php + /** * @defgroup Database Database * @@ -42,10 +43,10 @@ interface DatabaseType { /** * Open a connection to the database. Usually aborts on failure * - * @param string $server database server host - * @param string $user database user name - * @param string $password database user password - * @param string $dbName database name + * @param string $server Database server host + * @param string $user Database user name + * @param string $password Database user password + * @param string $dbName Database name * @return bool * @throws DBConnectionError */ @@ -57,18 +58,18 @@ interface DatabaseType { * member variables. * If no more rows are available, false is returned. * - * @param $res ResultWrapper|object as returned from DatabaseBase::query(), etc. - * @return object|bool + * @param ResultWrapper|stdClass $res Object as returned from DatabaseBase::query(), etc. + * @return stdClass|bool * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchObject( $res ); /** * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. + * form. Fields are retrieved with $row['fieldname']. * If no more rows are available, false is returned. * - * @param $res ResultWrapper result object as returned from DatabaseBase::query(), etc. + * @param ResultWrapper $res Result object as returned from DatabaseBase::query(), etc. * @return array|bool * @throws DBUnexpectedError Thrown if the database returns an error */ @@ -77,7 +78,7 @@ interface DatabaseType { /** * Get the number of rows in a result object * - * @param $res Mixed: A SQL result + * @param mixed $res A SQL result * @return int */ function numRows( $res ); @@ -86,7 +87,7 @@ interface DatabaseType { * Get the number of fields in a result object * @see http://www.php.net/mysql_num_fields * - * @param $res Mixed: A SQL result + * @param mixed $res A SQL result * @return int */ function numFields( $res ); @@ -95,8 +96,8 @@ interface DatabaseType { * Get a field name in a result object * @see http://www.php.net/mysql_field_name * - * @param $res Mixed: A SQL result - * @param $n Integer + * @param mixed $res A SQL result + * @param int $n * @return string */ function fieldName( $res, $n ); @@ -119,8 +120,8 @@ interface DatabaseType { * Change the position of the cursor in a result object * @see http://www.php.net/mysql_data_seek * - * @param $res Mixed: A SQL result - * @param $row Mixed: Either MySQL row or ResultWrapper + * @param mixed $res A SQL result + * @param int $row */ function dataSeek( $res, $row ); @@ -144,8 +145,8 @@ interface DatabaseType { * mysql_fetch_field() wrapper * Returns false if the field doesn't exist * - * @param string $table table name - * @param string $field field name + * @param string $table Table name + * @param string $field Field name * * @return Field */ @@ -156,7 +157,7 @@ interface DatabaseType { * @param string $table Table name * @param string $index Index name * @param string $fname Calling function name - * @return Mixed: Database-specific index description class or false if the index does not exist + * @return mixed Database-specific index description class or false if the index does not exist */ function indexInfo( $table, $index, $fname = __METHOD__ ); @@ -171,18 +172,18 @@ interface DatabaseType { /** * Wrapper for addslashes() * - * @param string $s to be slashed. - * @return string: slashed string. + * @param string $s String to be slashed. + * @return string Slashed string. */ function strencode( $s ); /** * Returns a wikitext link to the DB's website, e.g., - * return "[http://www.mysql.com/ MySQL]"; + * return "[http://www.mysql.com/ MySQL]"; * Should at least contain plain text, if for some reason * your database has no website. * - * @return string: wikitext of a link to the server software's web site + * @return string Wikitext of a link to the server software's web site */ function getSoftwareLink(); @@ -190,16 +191,16 @@ interface DatabaseType { * A string describing the current software version, like from * mysql_get_server_info(). * - * @return string: Version information from the database server. + * @return string Version information from the database server. */ function getServerVersion(); /** * A string describing the current software version, and possibly - * other details in a user-friendly way. Will be listed on Special:Version, etc. + * other details in a user-friendly way. Will be listed on Special:Version, etc. * Use getServerVersion() to get machine-friendly information. * - * @return string: Version information from the database server + * @return string Version information from the database server */ function getServerInfo(); } @@ -208,7 +209,8 @@ interface DatabaseType { * Interface for classes that implement or wrap DatabaseBase * @ingroup Database */ -interface IDatabase {} +interface IDatabase { +} /** * Database abstraction object @@ -217,8 +219,10 @@ interface IDatabase {} 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; @@ -232,6 +236,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { protected $mServer, $mUser, $mPassword, $mDBname; + /** @var resource Database connection */ protected $mConn = null; protected $mOpened = false; @@ -241,12 +246,11 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { protected $mTrxPreCommitCallbacks = array(); protected $mTablePrefix; + protected $mSchema; protected $mFlags; protected $mForeign; - protected $mTrxLevel = 0; protected $mErrorCount = 0; protected $mLBInfo = array(); - protected $mFakeSlaveLag = null, $mFakeMaster = false; protected $mDefaultBigSelects = null; protected $mSchemaVars = false; @@ -257,10 +261,25 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { protected $delimiter = ';'; /** + * Either 1 if a transaction is active or 0 otherwise. + * The other Trx fields may not be meaningfull if this is 0. + * + * @var int + */ + protected $mTrxLevel = 0; + + /** + * Either a short hexidecimal string if a transaction is active or "" + * + * @var string + */ + protected $mTrxShortId = ''; + + /** * Remembers the function name given for starting the most recent transaction via begin(). * Used to provide additional context for error reporting. * - * @var String + * @var string * @see DatabaseBase::mTrxLevel */ private $mTrxFname = null; @@ -268,7 +287,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Record if possible write queries were done in the last transaction started * - * @var Bool + * @var bool * @see DatabaseBase::mTrxLevel */ private $mTrxDoneWrites = false; @@ -276,20 +295,34 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Record if the current transaction was started implicitly due to DBO_TRX being set. * - * @var Bool + * @var bool * @see DatabaseBase::mTrxLevel */ private $mTrxAutomatic = false; /** + * Array of levels of atomicity within transactions + * + * @var SplStack + */ + private $mTrxAtomicLevels; + + /** + * Record if the current transaction was started implicitly by DatabaseBase::startAtomic + * + * @var bool + */ + private $mTrxAutomaticAtomic = false; + + /** * @since 1.21 - * @var file handle for upgrade + * @var resource File handle for upgrade */ protected $fileHandle = null; /** * @since 1.22 - * @var Process cache of VIEWs names in the database + * @var string[] Process cache of VIEWs names in the database */ protected $allViews = null; @@ -300,17 +333,17 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * A string describing the current software version, and possibly - * other details in a user-friendly way. Will be listed on Special:Version, etc. + * other details in a user-friendly way. Will be listed on Special:Version, etc. * Use getServerVersion() to get machine-friendly information. * - * @return string: Version information from the database server + * @return string Version information from the database server */ public function getServerInfo() { return $this->getServerVersion(); } /** - * @return string: command delimiter used by this database engine + * @return string Command delimiter used by this database engine */ public function getDelimiter() { return $this->delimiter; @@ -318,12 +351,12 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Boolean, controls output of large amounts of debug information. - * @param $debug bool|null + * @param bool|null $debug * - true to enable debugging * - false to disable debugging * - omitted or null to do nothing * - * @return bool|null previous value of the flag + * @return bool|null Previous value of the flag */ public function debug( $debug = null ) { return wfSetBit( $this->mFlags, DBO_DEBUG, $debug ); @@ -347,8 +380,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * split up queries into batches using a LIMIT clause than to switch off * buffering. * - * @param $buffer null|bool - * + * @param null|bool $buffer * @return null|bool The previous value of the flag */ public function bufferResults( $buffer = null ) { @@ -368,8 +400,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * Do not use this function outside of the Database classes. * - * @param $ignoreErrors bool|null - * + * @param null|bool $ignoreErrors * @return bool The previous value of the flag. */ public function ignoreErrors( $ignoreErrors = null ) { @@ -377,16 +408,15 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * Gets or sets the current transaction level. + * Gets the current transaction level. * * Historically, transactions were allowed to be "nested". This is no * longer supported, so this function really only returns a boolean. * - * @param int $level An integer (0 or 1), or omitted to leave it unchanged. * @return int The previous value */ - public function trxLevel( $level = null ) { - return wfSetVar( $this->mTrxLevel, $level ); + public function trxLevel() { + return $this->mTrxLevel; } /** @@ -408,9 +438,18 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** + * Get/set the db schema. + * @param string $schema The database schema to set, or omitted to leave it unchanged. + * @return string The previous db schema. + */ + public function dbSchema( $schema = null ) { + return wfSetVar( $this->mSchema, $schema ); + } + + /** * Set the filehandle to copy write statements to. * - * @param $fh filehandle + * @param resource $fh File handle */ public function setFileHandle( $fh ) { $this->fileHandle = $fh; @@ -423,7 +462,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @param string $name The entry of the info array to get, or null to get the * whole array * - * @return LoadBalancer|null + * @return array|mixed|null */ public function getLBInfo( $name = null ) { if ( is_null( $name ) ) { @@ -442,8 +481,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * the LB info array is set to that parameter. If it is called with two * parameters, the member with the given name is set to the given value. * - * @param $name - * @param $value + * @param string $name + * @param array $value */ public function setLBInfo( $name, $value = null ) { if ( is_null( $value ) ) { @@ -456,19 +495,19 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Set lag time in seconds for a fake slave * - * @param $lag int + * @param mixed $lag Valid values for this parameter are determined by the + * subclass, but should be a PHP scalar or array that would be sensible + * as part of $wgLBFactoryConf. */ public function setFakeSlaveLag( $lag ) { - $this->mFakeSlaveLag = $lag; } /** * Make this connection a fake master * - * @param $enabled bool + * @param bool $enabled */ public function setFakeMaster( $enabled = true ) { - $this->mFakeMaster = $enabled; } /** @@ -548,7 +587,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Return the last query that went through DatabaseBase::query() - * @return String + * @return string */ public function lastQuery() { return $this->mLastQuery; @@ -561,7 +600,18 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @return bool */ public function doneWrites() { - return $this->mDoneWrites; + return (bool)$this->mDoneWrites; + } + + /** + * Returns the last time the connection may have been used for write queries. + * Should return a timestamp if unsure. + * + * @return int|float UNIX timestamp or false + * @since 1.24 + */ + public function lastDoneWrites() { + return $this->mDoneWrites ?: false; } /** @@ -578,7 +628,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Is a connection to the database open? - * @return Boolean + * @return bool */ public function isOpen() { return $this->mOpened; @@ -587,7 +637,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Set a flag for this connection * - * @param $flag Integer: DBO_* constants from Defines.php: + * @param int $flag DBO_* constants from Defines.php: * - DBO_DEBUG: output some debug info (same as debug()) * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults()) * - DBO_TRX: automatically start transactions @@ -598,15 +648,21 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { public function setFlag( $flag ) { global $wgDebugDBTransactions; $this->mFlags |= $flag; - if ( ( $flag & DBO_TRX ) & $wgDebugDBTransactions ) { - wfDebug( "Implicit transactions are now disabled.\n" ); + if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) { + wfDebug( "Implicit transactions are now enabled.\n" ); } } /** * Clear a flag for this connection * - * @param $flag: same as setFlag()'s $flag param + * @param int $flag DBO_* constants from Defines.php: + * - DBO_DEBUG: output some debug info (same as debug()) + * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults()) + * - DBO_TRX: automatically start transactions + * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode + * and removes it in command line mode + * - DBO_PERSISTENT: use persistant database connection */ public function clearFlag( $flag ) { global $wgDebugDBTransactions; @@ -619,8 +675,12 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Returns a boolean whether the flag $flag is set for this connection * - * @param $flag: same as setFlag()'s $flag param - * @return Boolean + * @param int $flag DBO_* constants from Defines.php: + * - DBO_DEBUG: output some debug info (same as debug()) + * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults()) + * - DBO_TRX: automatically start transactions + * - DBO_PERSISTENT: use persistant database connection + * @return bool */ public function getFlag( $flag ) { return !!( $this->mFlags & $flag ); @@ -629,8 +689,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * General read-only accessor * - * @param $name string - * + * @param string $name * @return string */ public function getProperty( $name ) { @@ -649,19 +708,42 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * Return a path to the DBMS-specific schema file, otherwise default to tables.sql + * Return a path to the DBMS-specific SQL file if it exists, + * otherwise default SQL file * + * @param string $filename * @return string */ - public function getSchemaPath() { + private function getSqlFilePath( $filename ) { global $IP; - if ( file_exists( "$IP/maintenance/" . $this->getType() . "/tables.sql" ) ) { - return "$IP/maintenance/" . $this->getType() . "/tables.sql"; + $dbmsSpecificFilePath = "$IP/maintenance/" . $this->getType() . "/$filename"; + if ( file_exists( $dbmsSpecificFilePath ) ) { + return $dbmsSpecificFilePath; } else { - return "$IP/maintenance/tables.sql"; + return "$IP/maintenance/$filename"; } } + /** + * Return a path to the DBMS-specific schema file, + * otherwise default to tables.sql + * + * @return string + */ + public function getSchemaPath() { + return $this->getSqlFilePath( 'tables.sql' ); + } + + /** + * Return a path to the DBMS-specific update key file, + * otherwise default to update-keys.sql + * + * @return string + */ + public function getUpdateKeysPath() { + return $this->getSqlFilePath( 'update-keys.sql' ); + } + # ------------------------------------------------------------------------------ # Other functions # ------------------------------------------------------------------------------ @@ -673,28 +755,39 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * 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 + * @param array $params Parameters passed from DatabaseBase::factory() */ - function __construct( $server = false, $user = false, $password = false, $dbName = false, - $flags = 0, $tablePrefix = 'get from global', $foreign = false - ) { - global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions; + function __construct( $params = null ) { + global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode, $wgDebugDBTransactions; - $this->mFlags = $flags; + $this->mTrxAtomicLevels = new SplStack; + + if ( is_array( $params ) ) { // MW 1.22 + $server = $params['host']; + $user = $params['user']; + $password = $params['password']; + $dbName = $params['dbname']; + $flags = $params['flags']; + $tablePrefix = $params['tablePrefix']; + $schema = $params['schema']; + $foreign = $params['foreign']; + } else { // legacy calling pattern + wfDeprecated( __METHOD__ . " method called without parameter array.", "1.23" ); + $args = func_get_args(); + $server = isset( $args[0] ) ? $args[0] : false; + $user = isset( $args[1] ) ? $args[1] : false; + $password = isset( $args[2] ) ? $args[2] : false; + $dbName = isset( $args[3] ) ? $args[3] : false; + $flags = isset( $args[4] ) ? $args[4] : 0; + $tablePrefix = isset( $args[5] ) ? $args[5] : 'get from global'; + $schema = 'get from global'; + $foreign = isset( $args[6] ) ? $args[6] : false; + } + $this->mFlags = $flags; if ( $this->mFlags & DBO_DEFAULT ) { if ( $wgCommandLineMode ) { $this->mFlags &= ~DBO_TRX; @@ -716,6 +809,13 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->mTablePrefix = $tablePrefix; } + /** Get the database schema*/ + if ( $schema == 'get from global' ) { + $this->mSchema = $wgDBmwschema; + } else { + $this->mSchema = $schema; + } + $this->mForeign = $foreign; if ( $user ) { @@ -729,13 +829,14 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * not restored on unserialize. */ public function __sleep() { - throw new MWException( 'Database serialization may cause problems, since the connection is not restored on wakeup.' ); + throw new MWException( 'Database serialization may cause problems, since ' . + 'the connection is not restored on wakeup.' ); } /** * 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( $dbType ) ); + * $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 @@ -744,22 +845,23 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * database. Example uses in core: * @see LoadBalancer::reallyOpenConnection() * @see ForeignDBRepo::getMasterDB() - * @see WebInstaller_DBConnect::execute() + * @see WebInstallerDBConnect::execute() * * @since 1.18 * * @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, driver - * @return DatabaseBase subclass or null + * Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver + * @throws MWException If the database driver or extension cannot be found + * @return DatabaseBase|null DatabaseBase subclass or null */ final public static function factory( $dbType, $p = array() ) { $canonicalDBTypes = array( - 'mysql' => array( 'mysqli', 'mysql' ), + 'mysql' => array( 'mysqli', 'mysql' ), 'postgres' => array(), - 'sqlite' => array(), - 'oracle' => array(), - 'mssql' => array(), + 'sqlite' => array(), + 'oracle' => array(), + 'mssql' => array(), ); $driver = false; @@ -789,17 +891,32 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { " no viable database extension found for type '$dbType'" ); } + // Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema, + // and everything else doesn't use a schema (e.g. null) + // Although postgres and oracle support schemas, we don't use them (yet) + // to maintain backwards compatibility + $defaultSchemas = array( + 'mysql' => null, + 'postgres' => null, + 'sqlite' => null, + 'oracle' => null, + 'mssql' => 'get from global', + ); + $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['foreign'] ) ? $p['foreign'] : false + $params = array( + 'host' => isset( $p['host'] ) ? $p['host'] : false, + 'user' => isset( $p['user'] ) ? $p['user'] : false, + 'password' => isset( $p['password'] ) ? $p['password'] : false, + 'dbname' => isset( $p['dbname'] ) ? $p['dbname'] : false, + 'flags' => isset( $p['flags'] ) ? $p['flags'] : 0, + 'tablePrefix' => isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global', + 'schema' => isset( $p['schema'] ) ? $p['schema'] : $defaultSchemas[$dbType], + 'foreign' => isset( $p['foreign'] ) ? $p['foreign'] : false ); + + return new $class( $params ); } else { return null; } @@ -822,6 +939,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { if ( $this->mPHPError ) { $error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError ); $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error ); + return $error; } else { return false; @@ -829,9 +947,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * @param $errno - * @param $errstr - * @access private + * @param int $errno + * @param string $errstr */ public function connectionErrorHandler( $errno, $errstr ) { $this->mPHPError = $errstr; @@ -842,13 +959,12 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * if it is open : commits any open transactions * * @throws MWException - * @return Bool operation success. true if already closed. + * @return bool Operation success. true if already closed. */ public function close() { if ( count( $this->mTrxIdleCallbacks ) ) { // sanity throw new MWException( "Transaction idle callbacks still pending." ); } - $this->mOpened = false; if ( $this->mConn ) { if ( $this->trxLevel() ) { if ( !$this->mTrxAutomatic ) { @@ -859,23 +975,25 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->commit( __METHOD__, 'flush' ); } - $ret = $this->closeConnection(); + $closed = $this->closeConnection(); $this->mConn = false; - return $ret; } else { - return true; + $closed = true; } + $this->mOpened = false; + + return $closed; } /** * Closes underlying database connection * @since 1.20 - * @return bool: Whether connection was closed successfully + * @return bool Whether connection was closed successfully */ abstract protected function closeConnection(); /** - * @param string $error fallback error message, used if none is given by DB + * @param string $error Fallback error message, used if none is given by DB * @throws DBConnectionError */ function reportConnectionError( $error = 'Unknown error' ) { @@ -891,8 +1009,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * The DBMS-dependent part of query() * - * @param $sql String: SQL query. - * @return ResultWrapper Result object to feed to fetchObject, fetchRow, ...; or false on failure + * @param string $sql SQL query. + * @return ResultWrapper|bool Result object to feed to fetchObject, + * fetchRow, ...; or false on failure */ abstract protected function doQuery( $sql ); @@ -900,8 +1019,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Determine whether a query writes to the DB. * Should return true if unsure. * - * @param $sql string - * + * @param string $sql * @return bool */ public function isWriteQuery( $sql ) { @@ -921,23 +1039,23 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * However, the query wrappers themselves should call this function. * - * @param $sql String: SQL query - * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST + * @param string $sql SQL query + * @param string $fname Name of the calling function, for profiling/SHOW PROCESSLIST * comment (you can use __METHOD__ or add some extra info) - * @param $tempIgnore Boolean: Whether to avoid throwing an exception on errors... + * @param bool $tempIgnore Whether to avoid throwing an exception on errors... * maybe best to catch the exception instead? * @throws MWException - * @return boolean|ResultWrapper. true for a successful write query, ResultWrapper object + * @return bool|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 = __METHOD__, $tempIgnore = false ) { - global $wgUser, $wgDebugDBTransactions; + global $wgUser, $wgDebugDBTransactions, $wgDebugDumpSqlLength; $this->mLastQuery = $sql; - if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) { + if ( $this->isWriteQuery( $sql ) ) { # Set a flag indicating that writes have been done - wfDebug( __METHOD__ . ": Writes done: $sql\n" ); - $this->mDoneWrites = true; + wfDebug( __METHOD__ . ': Writes done: ' . DatabaseBase::generalizeSQL( $sql ) . "\n" ); + $this->mDoneWrites = microtime( true ); } # Add a comment for easy SHOW PROCESSLIST interpretation @@ -957,8 +1075,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { # If DBO_TRX is set, start a transaction if ( ( $this->mFlags & DBO_TRX ) && !$this->mTrxLevel && - $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' ) - { + $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' + ) { # Avoid establishing transactions for SHOW and SET statements too - # that would delay transaction initializations to once connection # is really used by application @@ -975,10 +1093,14 @@ abstract class DatabaseBase implements IDatabase, 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 ); + Profiler::instance()->transactionWritingIn( + $this->mServer, $this->mDBname, $this->mTrxShortId ); } + $queryProf = ''; + $totalProf = ''; $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. @@ -989,6 +1111,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); $totalProf = 'DatabaseBase::query'; } + # Include query transaction state + $queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : ""; + + $trx = $this->mTrxLevel ? 'TRX=yes' : 'TRX=no'; wfProfileIn( $totalProf ); wfProfileIn( $queryProf ); } @@ -997,7 +1123,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { static $cnt = 0; $cnt++; - $sqlx = substr( $commentedSql, 0, 500 ); + $sqlx = $wgDebugDumpSqlLength ? substr( $commentedSql, 0, $wgDebugDumpSqlLength ) + : $commentedSql; $sqlx = strtr( $sqlx, "\t\n", ' ' ); $master = $isMaster ? 'master' : 'slave'; @@ -1006,6 +1133,11 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $queryId = MWDebug::query( $sql, $fname, $isMaster ); + # Avoid fatals if close() was called + if ( !$this->isOpen() ) { + throw new DBUnexpectedError( $this, "DB connection was already closed." ); + } + # Do the query and handle errors $ret = $this->doQuery( $commentedSql ); @@ -1014,22 +1146,33 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { # Try reconnecting if the connection was lost if ( false === $ret && $this->wasErrorReissuable() ) { # Transaction is gone, like it or not + $hadTrx = $this->mTrxLevel; // possible lost transaction $this->mTrxLevel = 0; - $this->mTrxIdleCallbacks = array(); // cancel - $this->mTrxPreCommitCallbacks = array(); // cancel + $this->mTrxIdleCallbacks = array(); // bug 65263 + $this->mTrxPreCommitCallbacks = array(); // bug 65263 wfDebug( "Connection lost, reconnecting...\n" ); - + # Stash the last error values since ping() might clear them + $lastError = $this->lastError(); + $lastErrno = $this->lastErrno(); if ( $this->ping() ) { + global $wgRequestTime; wfDebug( "Reconnected\n" ); - $sqlx = substr( $commentedSql, 0, 500 ); + $sqlx = $wgDebugDumpSqlLength ? substr( $commentedSql, 0, $wgDebugDumpSqlLength ) + : $commentedSql; $sqlx = strtr( $sqlx, "\t\n", ' ' ); - global $wgRequestTime; $elapsed = round( microtime( true ) - $wgRequestTime, 3 ); if ( $elapsed < 300 ) { # Not a database error to lose a transaction after a minute or two - wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" ); + wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx" ); + } + if ( $hadTrx ) { + # Leave $ret as false and let an error be reported. + # Callers may catch the exception and continue to use the DB. + $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore ); + } else { + # Should be safe to silently retry (no trx and thus no callbacks) + $ret = $this->doQuery( $commentedSql ); } - $ret = $this->doQuery( $commentedSql ); } else { wfDebug( "Failed\n" ); } @@ -1051,11 +1194,11 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Report a query error. Log the error, and if neither the object ignore * flag nor the $tempIgnore flag is set, throw a DBQueryError. * - * @param $error String - * @param $errno Integer - * @param $sql String - * @param $fname String - * @param $tempIgnore Boolean + * @param string $error + * @param int $errno + * @param string $sql + * @param string $fname + * @param bool $tempIgnore * @throws DBQueryError */ public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { @@ -1067,8 +1210,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { wfDebug( "SQL ERROR (ignored): $error\n" ); $this->ignoreErrors( $ignore ); } else { - $sql1line = str_replace( "\n", "\\n", $sql ); - wfLogDBError( "$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n" ); + $sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ); + wfLogDBError( "$fname\t{$this->mServer}\t$errno\t$error\t$sql1line" ); wfDebug( "SQL ERROR: " . $error . "\n" ); throw new DBQueryError( $this, $error, $errno, $sql, $fname ); } @@ -1083,21 +1226,22 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * & = filename; reads the file and inserts as a blob * (we don't use this though...) * - * @param $sql string - * @param $func string + * @param string $sql + * @param string $func * * @return array */ protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) { /* MySQL doesn't support prepared statements (yet), so just - pack up the query for reference. We'll manually replace - the bits later. */ + * pack up the query for reference. We'll manually replace + * the bits later. + */ return array( 'query' => $sql, 'func' => $func ); } /** * Free a prepared query, generated by prepare(). - * @param $prepared + * @param string $prepared */ protected function freePrepared( $prepared ) { /* No-op by default */ @@ -1105,8 +1249,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Execute a prepared query with the various arguments - * @param string $prepared the prepared sql - * @param $args Mixed: Either an array here, or put scalars as varargs + * @param string $prepared The prepared sql + * @param mixed $args Either an array here, or put scalars as varargs * * @return ResultWrapper */ @@ -1125,9 +1269,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * For faking prepared SQL statements on DBs that don't support it directly. * - * @param string $preparedQuery a 'preparable' SQL statement - * @param array $args of arguments to fill it with - * @return string executable SQL + * @param string $preparedQuery A 'preparable' SQL statement + * @param array $args Array of Arguments to fill it with + * @return string Executable SQL */ public function fillPrepared( $preparedQuery, $args ) { reset( $args ); @@ -1142,9 +1286,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * The arguments should be in $this->preparedArgs and must not be touched * while we're doing this. * - * @param $matches Array + * @param array $matches * @throws DBUnexpectedError - * @return String + * @return string */ protected function fillPreparedArg( $matches ) { switch ( $matches[1] ) { @@ -1165,9 +1309,15 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { 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.' ); + throw new DBUnexpectedError( + $this, + '& mode is not implemented. If it\'s really needed, uncomment the line above.' + ); default: - throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' ); + throw new DBUnexpectedError( + $this, + 'Received invalid match. This should never happen!' + ); } } @@ -1176,7 +1326,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * necessary to call this, just use unset() or let the variable holding * the result object go out of scope. * - * @param $res Mixed: A SQL result + * @param mixed $res A SQL result */ public function freeResult( $res ) { } @@ -1226,9 +1376,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Returns an optional USE INDEX clause to go after the table, and a * string to go at the end of the query. * - * @param array $options associative array of options to be turned into - * an SQL query, valid keys are listed in the function. - * @return Array + * @param array $options Associative array of options to be turned into + * an SQL query, valid keys are listed in the function. + * @return array * @see DatabaseBase::select() */ public function makeSelectOptions( $options ) { @@ -1310,7 +1460,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Returns an optional GROUP BY with an optional HAVING * - * @param array $options associative array of options + * @param array $options Associative array of options * @return string * @see DatabaseBase::select() * @since 1.21 @@ -1329,13 +1479,14 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { : $options['HAVING']; $sql .= ' HAVING ' . $having; } + return $sql; } /** * Returns an optional ORDER BY * - * @param array $options associative array of options + * @param array $options Associative array of options * @return string * @see DatabaseBase::select() * @since 1.21 @@ -1345,8 +1496,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $ob = is_array( $options['ORDER BY'] ) ? implode( ',', $options['ORDER BY'] ) : $options['ORDER BY']; + return ' ORDER BY ' . $ob; } + return ''; } @@ -1359,9 +1512,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @param string|array $conds Conditions * @param string $fname Caller function name * @param array $options Query options - * @param $join_conds Array Join conditions + * @param array $join_conds Join conditions * - * @param $table string|array + * + * @param string|array $table * * May be either an array of table names, or a single string holding a table * name. If an array is given, table aliases can be specified, for example: @@ -1376,7 +1530,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * added, and various other table name mappings to be performed. * * - * @param $vars string|array + * @param string|array $vars * * May be either a field name or an array of field names. The field names * can be complete fragments of SQL, for direct inclusion into the SELECT @@ -1390,7 +1544,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * DBMS-independent. * * - * @param $conds string|array + * @param string|array $conds * * May be either a string containing a single condition, or an array of * conditions. If an array is given, the conditions constructed from each @@ -1415,7 +1569,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * - DatabaseBase::conditional() * * - * @param $options string|array + * @param string|array $options * * Optional: Array of query options. Boolean options are specified by * including them in the array as a string value with a numeric key, for @@ -1471,7 +1625,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * - SQL_NO_CACHE * * - * @param $join_conds string|array + * @param string|array $join_conds * * Optional associative array of table-specific join conditions. In the * most common case, this is unnecessary, since the join condition can be @@ -1484,7 +1638,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * array( 'page' => array( 'LEFT JOIN', 'page_latest=rev_id' ) ) * - * @return ResultWrapper. If the query returned no rows, a ResultWrapper + * @return ResultWrapper|bool If the query returned no rows, a ResultWrapper * with no rows in it will be returned. If there was a query error, a * DBQueryError exception will be thrown, except if the "ignore errors" * option was set, in which case false will be returned. @@ -1507,14 +1661,14 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @param string|array $conds Conditions * @param string $fname Caller function name * @param string|array $options Query options - * @param $join_conds string|array Join conditions + * @param string|array $join_conds Join conditions * * @return string SQL query string. * @see DatabaseBase::select() */ public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, - $options = array(), $join_conds = array() ) - { + $options = array(), $join_conds = array() + ) { if ( is_array( $vars ) ) { $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) ); } @@ -1573,13 +1727,13 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @param array $conds Conditions * @param string $fname Caller function name * @param string|array $options Query options - * @param $join_conds array|string Join conditions + * @param array|string $join_conds Join conditions * - * @return object|bool + * @return stdClass|bool */ public function selectRow( $table, $vars, $conds, $fname = __METHOD__, - $options = array(), $join_conds = array() ) - { + $options = array(), $join_conds = array() + ) { $options = (array)$options; $options['LIMIT'] = 1; $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds ); @@ -1598,7 +1752,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * Estimate rows in dataset. + * Estimate the number of rows in dataset * * MySQL allows you to estimate the number of rows that would be returned * by a SELECT query, using EXPLAIN SELECT. The estimate is provided using @@ -1610,16 +1764,16 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * Takes the same arguments as DatabaseBase::select(). * - * @param string $table table name - * @param array|string $vars : unused - * @param array|string $conds : filters on the table - * @param string $fname function name for profiling - * @param array $options options for select - * @return Integer: row count - */ - public function estimateRowCount( $table, $vars = '*', $conds = '', - $fname = __METHOD__, $options = array() ) - { + * @param string $table Table name + * @param string $vars Unused + * @param array|string $conds Filters on the table + * @param string $fname Function name for profiling + * @param array $options Options for select + * @return int Row count + */ + public function estimateRowCount( + $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() + ) { $rows = 0; $res = $this->select( $table, array( 'rowcount' => 'COUNT(*)' ), $conds, $fname, $options ); @@ -1632,6 +1786,36 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** + * Get the number of rows in dataset + * + * This is useful when trying to do COUNT(*) but with a LIMIT for performance. + * + * Takes the same arguments as DatabaseBase::select(). + * + * @param string $table Table name + * @param string $vars Unused + * @param array|string $conds Filters on the table + * @param string $fname Function name for profiling + * @param array $options Options for select + * @return int Row count + * @since 1.24 + */ + public function selectRowCount( + $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() + ) { + $rows = 0; + $sql = $this->selectSQLText( $table, '1', $conds, $fname, $options ); + $res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count" ); + + if ( $res ) { + $row = $this->fetchRow( $res ); + $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0; + } + + return $rows; + } + + /** * Removes most variables from an SQL query and replaces them with X or N for numbers. * It's only slightly flawed. Don't use for anything important. * @@ -1653,9 +1837,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { # All newlines, tabs, etc replaced by single space $sql = preg_replace( '/\s+/', ' ', $sql ); - # All numbers => N + # All numbers => N, + # except the ones surrounded by characters, e.g. l10n $sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql ); - $sql = preg_replace( '/-?\d+/s', 'N', $sql ); + $sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql ); return $sql; } @@ -1663,10 +1848,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Determines whether a field exists in a table * - * @param string $table table name - * @param string $field filed to check on that table - * @param string $fname calling function name (optional) - * @return Boolean: whether $table has filed $field + * @param string $table Table name + * @param string $field Filed to check on that table + * @param string $fname Calling function name (optional) + * @return bool Whether $table has filed $field */ public function fieldExists( $table, $field, $fname = __METHOD__ ) { $info = $this->fieldInfo( $table, $field ); @@ -1679,10 +1864,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Usually throws a DBQueryError on failure * If errors are explicitly ignored, returns NULL on failure * - * @param $table - * @param $index - * @param $fname string - * + * @param string $table + * @param string $index + * @param string $fname * @return bool|null */ public function indexExists( $table, $index, $fname = __METHOD__ ) { @@ -1701,9 +1885,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Query whether a given table exists * - * @param $table string - * @param $fname string - * + * @param string $table + * @param string $fname * @return bool */ public function tableExists( $table, $fname = __METHOD__ ) { @@ -1716,24 +1899,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * mysql_field_type() wrapper - * @param $res - * @param $index - * @return string - */ - public function fieldType( $res, $index ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - - return mysql_field_type( $res, $index ); - } - - /** * Determines if a given index is unique * - * @param $table string - * @param $index string + * @param string $table + * @param string $index * * @return bool */ @@ -1750,7 +1919,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Helper for DatabaseBase::insert(). * - * @param $options array + * @param array $options * @return string */ protected function makeInsertOptions( $options ) { @@ -1782,11 +1951,11 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * possible to determine how many rows were successfully inserted using * DatabaseBase::affectedRows(). * - * @param $table String Table name. This will be passed through - * DatabaseBase::tableName(). - * @param $a Array of rows to insert - * @param $fname String Calling function name (use __METHOD__) for logs/profiling - * @param array $options of options + * @param string $table Table name. This will be passed through + * DatabaseBase::tableName(). + * @param array $a Array of rows to insert + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @param array $options Array of options * * @return bool */ @@ -1843,12 +2012,12 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * Make UPDATE options for the DatabaseBase::update function + * Make UPDATE options array for DatabaseBase::makeUpdateOptions * - * @param array $options The options passed to DatabaseBase::update - * @return string + * @param array $options + * @return array */ - protected function makeUpdateOptions( $options ) { + protected function makeUpdateOptionsArray( $options ) { if ( !is_array( $options ) ) { $options = array( $options ); } @@ -1863,31 +2032,38 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $opts[] = 'IGNORE'; } + return $opts; + } + + /** + * Make UPDATE options for the DatabaseBase::update function + * + * @param array $options The options passed to DatabaseBase::update + * @return string + */ + protected function makeUpdateOptions( $options ) { + $opts = $this->makeUpdateOptionsArray( $options ); + return implode( ' ', $opts ); } /** * UPDATE wrapper. Takes a condition array and a SET array. * - * @param $table String name of the table to UPDATE. This will be passed through - * DatabaseBase::tableName(). - * - * @param array $values 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 $conds Array: An array of conditions (WHERE). See - * DatabaseBase::select() for the details of the format of - * condition arrays. Use '*' to update all rows. - * - * @param $fname String: The function name of the caller (from __METHOD__), - * for logging and profiling. - * + * @param string $table Name of the table to UPDATE. This will be passed through + * DatabaseBase::tableName(). + * @param array $values 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 array $conds An array of conditions (WHERE). See + * DatabaseBase::select() for the details of the format of condition + * arrays. Use '*' to update all rows. + * @param string $fname The function name of the caller (from __METHOD__), + * for logging and profiling. * @param array $options An array of UPDATE options, can be: - * - IGNORE: Ignore unique key conflicts - * - LOW_PRIORITY: MySQL-specific, see MySQL manual. - * @return Boolean + * - IGNORE: Ignore unique key conflicts + * - LOW_PRIORITY: MySQL-specific, see MySQL manual. + * @return bool */ function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { $table = $this->tableName( $table ); @@ -1903,15 +2079,15 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Makes an encoded list of strings from an array - * @param array $a containing the data - * @param int $mode Constant - * - LIST_COMMA: comma separated, no field names - * - LIST_AND: ANDed WHERE clause (without the WHERE). See - * the documentation for $conds in DatabaseBase::select(). - * - LIST_OR: ORed WHERE clause (without the WHERE) - * - LIST_SET: comma separated with field names, like a SET clause - * - LIST_NAMES: comma separated field names * + * @param array $a Containing the data + * @param int $mode Constant + * - LIST_COMMA: Comma separated, no field names + * - LIST_AND: ANDed WHERE clause (without the WHERE). See the + * documentation for $conds in DatabaseBase::select(). + * - LIST_OR: ORed WHERE clause (without the WHERE) + * - LIST_SET: Comma separated with field names, like a SET clause + * - LIST_NAMES: Comma separated field names * @throws MWException|DBUnexpectedError * @return string */ @@ -1974,11 +2150,11 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Build a partial where clause from a 2-d array such as used for LinkBatch. * The keys on each level may be either integers or strings. * - * @param array $data organized as 2-d - * array(baseKeyVal => array(subKeyVal => [ignored], ...), ...) - * @param string $baseKey field name to match the base-level keys to (eg 'pl_namespace') - * @param string $subKey field name to match the sub-level keys to (eg 'pl_title') - * @return Mixed: string SQL fragment, or false if no items in array. + * @param array $data Organized as 2-d + * array(baseKeyVal => array(subKeyVal => [ignored], ...), ...) + * @param string $baseKey Field name to match the base-level keys to (eg 'pl_namespace') + * @param string $subKey Field name to match the sub-level keys to (eg 'pl_title') + * @return string|bool SQL fragment, or false if no items in array */ public function makeWhereFrom2d( $data, $baseKey, $subKey ) { $conds = array(); @@ -2002,8 +2178,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Return aggregated value alias * - * @param $valuedata - * @param $valuename string + * @param array $valuedata + * @param string $valuename * * @return string */ @@ -2012,7 +2188,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * @param $field + * @param string $field * @return string */ public function bitNot( $field ) { @@ -2020,8 +2196,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * @param $fieldLeft - * @param $fieldRight + * @param string $fieldLeft + * @param string $fieldRight * @return string */ public function bitAnd( $fieldLeft, $fieldRight ) { @@ -2029,8 +2205,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * @param $fieldLeft - * @param $fieldRight + * @param string $fieldLeft + * @param string $fieldRight * @return string */ public function bitOr( $fieldLeft, $fieldRight ) { @@ -2039,32 +2215,59 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Build a concatenation list to feed into a SQL query - * @param array $stringList list of raw SQL expressions; caller is responsible for any quoting - * @return String + * @param array $stringList List of raw SQL expressions; caller is + * responsible for any quoting + * @return string */ public function buildConcat( $stringList ) { return 'CONCAT(' . implode( ',', $stringList ) . ')'; } /** + * Build a GROUP_CONCAT or equivalent statement for a query. + * + * This is useful for combining a field for several rows into a single string. + * NULL values will not appear in the output, duplicated values will appear, + * and the resulting delimiter-separated values have no defined sort order. + * Code using the results may need to use the PHP unique() or sort() methods. + * + * @param string $delim Glue to bind the results together + * @param string|array $table Table name + * @param string $field Field name + * @param string|array $conds Conditions + * @param string|array $join_conds Join conditions + * @return string SQL text + * @since 1.23 + */ + public function buildGroupConcatField( + $delim, $table, $field, $conds = '', $join_conds = array() + ) { + $fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')'; + + return '(' . $this->selectSQLText( $table, $fld, $conds, null, array(), $join_conds ) . ')'; + } + + /** * Change the current database * * @todo Explain what exactly will fail if this is not overridden. * - * @param $db + * @param string $db * * @return bool Success or failure */ public function selectDB( $db ) { - # Stub. Shouldn't cause serious problems if it's not overridden, but + # Stub. Shouldn't cause serious problems if it's not overridden, but # if your database engine supports a concept similar to MySQL's # databases you may as well. $this->mDBname = $db; + return true; } /** * Get the current DB name + * @return string */ public function getDBname() { return $this->mDBname; @@ -2072,6 +2275,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Get the server hostname or IP address + * @return string */ public function getServer() { return $this->mServer; @@ -2087,15 +2291,15 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * themselves. Pass the canonical name to such functions. This is only needed * when calling query() directly. * - * @param string $name database table name + * @param string $name Database table name * @param string $format One of: * quoted - Automatically pass the table name through addIdentifierQuotes() * so that it can be used in a query. * raw - Do not add identifier quotes to the table name - * @return String: full database name + * @return string Full database name */ public function tableName( $name, $format = 'quoted' ) { - global $wgSharedDB, $wgSharedPrefix, $wgSharedTables; + global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema; # Skip the entire process when we have a string quoted on both ends. # Note that we check the end so that we will still quote any use of # use of `database`.table. But won't break things if someone wants @@ -2119,31 +2323,49 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { # We reverse the explode so that database.table and table both output # the correct table. $dbDetails = explode( '.', $name, 2 ); - if ( count( $dbDetails ) == 2 ) { + if ( count( $dbDetails ) == 3 ) { + list( $database, $schema, $table ) = $dbDetails; + # We don't want any prefix added in this case + $prefix = ''; + } elseif ( count( $dbDetails ) == 2 ) { list( $database, $table ) = $dbDetails; # We don't want any prefix added in this case + # In dbs that support it, $database may actually be the schema + # but that doesn't affect any of the functionality here $prefix = ''; + $schema = null; } 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`' + && !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`' && in_array( $table, $wgSharedTables ) # A shared table is selected ) { $database = $wgSharedDB; + $schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema; $prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix; } else { $database = null; + $schema = $this->mSchema; # Default schema $prefix = $this->mTablePrefix; # Default prefix } } # Quote $table and apply the prefix if not quoted. + # $tableName might be empty if this is called from Database::replaceVars() $tableName = "{$prefix}{$table}"; - if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) ) { + if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) { $tableName = $this->addIdentifierQuotes( $tableName ); } + # Quote $schema and merge it with the table name if needed + if ( $schema !== null ) { + if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) { + $schema = $this->addIdentifierQuotes( $schema ); + } + $tableName = $schema . '.' . $tableName; + } + # Quote $database and merge it with the table name if needed if ( $database !== null ) { if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) { @@ -2218,8 +2440,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Gets an array of aliased table names * - * @param $tables array( [alias] => table ) - * @return array of strings, see tableNameWithAlias() + * @param array $tables Array( [alias] => table ) + * @return string[] See tableNameWithAlias() */ public function tableNamesWithAlias( $tables ) { $retval = array(); @@ -2229,6 +2451,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } $retval[] = $this->tableNameWithAlias( $table, $alias ); } + return $retval; } @@ -2251,8 +2474,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Gets an array of aliased field names * - * @param $fields array( [alias] => field ) - * @return array of strings, see fieldNameWithAlias() + * @param array $fields Array( [alias] => field ) + * @return string[] See fieldNameWithAlias() */ public function fieldNamesWithAlias( $fields ) { $retval = array(); @@ -2262,6 +2485,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } $retval[] = $this->fieldNameWithAlias( $field, $alias ); } + return $retval; } @@ -2270,8 +2494,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * which might have a JOIN and/or USE INDEX clause * * @param array $tables ( [alias] => table ) - * @param $use_index array Same as for select() - * @param $join_conds array Same as for select() + * @param array $use_index Same as for select() + * @param array $join_conds Same as for select() * @return string */ protected function tableNamesWithUseIndexOrJOIN( @@ -2304,11 +2528,12 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } $retJOIN[] = $tableClause; - // Is there an INDEX clause for this table? } elseif ( isset( $use_index[$alias] ) ) { + // Is there an INDEX clause for this table? $tableClause = $this->tableNameWithAlias( $table, $alias ); $tableClause .= ' ' . $this->useIndexClause( - implode( ',', (array)$use_index[$alias] ) ); + implode( ',', (array)$use_index[$alias] ) + ); $ret[] = $tableClause; } else { @@ -2329,8 +2554,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Get the name of an index in a given table * - * @param $index - * + * @param string $index * @return string */ protected function indexName( $index ) { @@ -2351,8 +2575,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Adds quotes and backslashes. * - * @param $s string - * + * @param string $s * @return string */ public function addQuotes( $s ) { @@ -2373,8 +2596,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Since MySQL is the odd one out here the double quotes are our generic * and we implement backticks in DatabaseMysql. * - * @param $s string - * + * @param string $s * @return string */ public function addIdentifierQuotes( $s ) { @@ -2385,37 +2607,36 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Returns if the given identifier looks quoted or not according to * the database convention for quoting identifiers . * - * @param $name string - * - * @return boolean + * @param string $name + * @return bool */ public function isQuotedIdentifier( $name ) { return $name[0] == '"' && substr( $name, -1, 1 ) == '"'; } /** - * @param $s string + * @param string $s * @return string */ protected function escapeLikeInternal( $s ) { - $s = str_replace( '\\', '\\\\', $s ); - $s = $this->strencode( $s ); - $s = str_replace( array( '%', '_' ), array( '\%', '\_' ), $s ); - - return $s; + return addcslashes( $s, '\%_' ); } /** - * LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match - * containing either string literals that will be escaped or tokens returned by anyChar() or anyString(). - * Alternatively, the function could be provided with an array of aforementioned parameters. + * LIKE statement wrapper, receives a variable-length argument list with + * parts of pattern to match containing either string literals that will be + * escaped or tokens returned by anyChar() or anyString(). Alternatively, + * the function could be provided with an array of aforementioned + * parameters. * - * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns a LIKE clause that searches - * for subpages of 'My page title'. - * Alternatively: $pattern = array( 'My_page_title/', $dbr->anyString() ); $query .= $dbr->buildLike( $pattern ); + * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns + * a LIKE clause that searches for subpages of 'My page title'. + * Alternatively: + * $pattern = array( 'My_page_title/', $dbr->anyString() ); + * $query .= $dbr->buildLike( $pattern ); * * @since 1.16 - * @return String: fully built LIKE statement + * @return string Fully built LIKE statement */ public function buildLike() { $params = func_get_args(); @@ -2434,7 +2655,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } } - return " LIKE '" . $s . "' "; + return " LIKE {$this->addQuotes( $s )} "; } /** @@ -2463,21 +2684,21 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Any implementation of this function should *not* involve reusing * sequence numbers created for rolled-back transactions. * See http://bugs.mysql.com/bug.php?id=30767 for details. - * @param $seqName string - * @return null + * @param string $seqName + * @return null|int */ public function nextSequenceValue( $seqName ) { return null; } /** - * USE INDEX clause. Unlikely to be useful for anything but MySQL. This + * USE INDEX clause. Unlikely to be useful for anything but MySQL. This * is only needed because a) MySQL must be as efficient as possible due to * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about - * which index to pick. Anyway, other databases might have different - * indexes on a given table. So don't bother overriding this unless you're + * which index to pick. Anyway, other databases might have different + * indexes on a given table. So don't bother overriding this unless you're * MySQL. - * @param $index + * @param string $index * @return string */ public function useIndexClause( $index ) { @@ -2500,10 +2721,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * errors which wouldn't have occurred in MySQL. * * @param string $table The table to replace the row(s) in. + * @param array $uniqueIndexes Is an array of indexes. Each element may be either + * a field name or an array of field names * @param array $rows Can be either a single row to insert, or multiple rows, * in the same format as for DatabaseBase::insert() - * @param array $uniqueIndexes is an array of indexes. Each element may be either - * 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 = __METHOD__ ) { @@ -2558,7 +2779,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * statement. * * @param string $table Table name - * @param array $rows Rows to insert + * @param array|string $rows Row(s) to insert * @param string $fname Caller function name * * @return ResultWrapper @@ -2609,26 +2830,28 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Usually throws a DBQueryError on failure. If errors are explicitly ignored, * returns success. * + * @since 1.22 + * * @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 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 - * + * @throws Exception * @return bool - * @since 1.22 */ - public function upsert( - $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__ + 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 ( !is_array( reset( $rows ) ) ) { + $rows = array( $rows ); + } if ( count( $uniqueIndexes ) ) { $clauses = array(); // list WHERE clauses that each identify a single row @@ -2684,19 +2907,18 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * DO NOT put the join condition in $conds. * - * @param $delTable String: The table to delete from. - * @param $joinTable String: The other table. - * @param $delVar String: The variable to join on, in the first table. - * @param $joinVar String: The variable to join on, in the second table. - * @param $conds Array: Condition array of field names mapped to variables, - * ANDed together in the WHERE clause - * @param $fname String: Calling function name (use __METHOD__) for - * logs/profiling + * @param string $delTable The table to delete from. + * @param string $joinTable The other table. + * @param string $delVar The variable to join on, in the first table. + * @param string $joinVar The variable to join on, in the second table. + * @param array $conds Condition array of field names mapped to variables, + * ANDed together in the WHERE clause + * @param string $fname Calling function name (use __METHOD__) for logs/profiling * @throws DBUnexpectedError */ public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, - $fname = __METHOD__ ) - { + $fname = __METHOD__ + ) { if ( !$conds ) { throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' ); @@ -2716,9 +2938,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Returns the size of a text field, or -1 for "unlimited" * - * @param $table string - * @param $field string - * + * @param string $table + * @param string $field * @return int */ public function textFieldSize( $table, $field ) { @@ -2740,7 +2961,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * A string to insert into queries to show that they're low-priority, like - * MySQL's LOW_PRIORITY. If no such feature exists, return an empty + * MySQL's LOW_PRIORITY. If no such feature exists, return an empty * string and nothing bad should happen. * * @return string Returns the text of the low priority option if it is @@ -2754,10 +2975,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * DELETE query wrapper. * * @param array $table Table name - * @param string|array $conds of conditions. See $conds in DatabaseBase::select() for - * the format. Use $conds == "*" to delete all rows - * @param string $fname name of the calling function - * + * @param string|array $conds Array of conditions. See $conds in DatabaseBase::select() + * for the format. Use $conds == "*" to delete all rows + * @param string $fname Name of the calling function * @throws DBUnexpectedError * @return bool|ResultWrapper */ @@ -2787,7 +3007,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @param string|array $srcTable May be either a table name, or an array of table names * to include in a join. * - * @param array $varMap must be an associative array of the form + * @param array $varMap Must be an associative array of the form * array( 'dest1' => 'source1', ...). Source items may be literals * rather than field names, but strings should be quoted with * DatabaseBase::addQuotes() @@ -2807,14 +3027,16 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { */ public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = array(), $selectOptions = array() ) - { + $insertOptions = array(), $selectOptions = array() + ) { $destTable = $this->tableName( $destTable ); - if ( is_array( $insertOptions ) ) { - $insertOptions = implode( ' ', $insertOptions ); + if ( !is_array( $insertOptions ) ) { + $insertOptions = array( $insertOptions ); } + $insertOptions = $this->makeInsertOptions( $insertOptions ); + if ( !is_array( $selectOptions ) ) { $selectOptions = array( $selectOptions ); } @@ -2844,22 +3066,21 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * Construct a LIMIT query with optional offset. This is used for query - * pages. The SQL should be adjusted so that only the first $limit rows - * are returned. If $offset is provided as well, then the first $offset + * Construct a LIMIT query with optional offset. This is used for query + * pages. The SQL should be adjusted so that only the first $limit rows + * are returned. If $offset is provided as well, then the first $offset * rows should be discarded, and the next $limit rows should be returned. * If the result of the query is not ordered, then the rows to be returned * are theoretically arbitrary. * * $sql is expected to be a SELECT, if that makes a difference. * - * The version provided by default works in MySQL and SQLite. It will very + * The version provided by default works in MySQL and SQLite. It will very * likely need to be overridden for most other DBMSes. * * @param string $sql SQL query we will append the limit too - * @param $limit Integer the SQL limit - * @param $offset Integer|bool the SQL offset (default false) - * + * @param int $limit The SQL limit + * @param int|bool $offset The SQL offset (default false) * @throws DBUnexpectedError * @return string */ @@ -2867,6 +3088,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { if ( !is_numeric( $limit ) ) { throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" ); } + return "$sql LIMIT " . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" ) . "{$limit} "; @@ -2875,7 +3097,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries * within the UNION construct. - * @return Boolean + * @return bool */ public function unionSupportsOrderAndLimit() { return true; // True for almost every DB supported @@ -2886,27 +3108,29 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * This is used for providing overload point for other DB abstractions * not compatible with the MySQL syntax. * @param array $sqls SQL statements to combine - * @param $all Boolean: use UNION ALL - * @return String: SQL fragment + * @param bool $all Use UNION ALL + * @return string SQL fragment */ public function unionQueries( $sqls, $all ) { $glue = $all ? ') UNION ALL (' : ') UNION ('; + return '(' . implode( $glue, $sqls ) . ')'; } /** - * Returns an SQL expression for a simple conditional. This doesn't need + * Returns an SQL expression for a simple conditional. This doesn't need * to be overridden unless CASE isn't supported in your DBMS. * * @param string|array $cond SQL expression which will result in a boolean value * @param string $trueVal SQL expression to return if true * @param string $falseVal SQL expression to return if false - * @return String: SQL fragment + * @return string SQL fragment */ public function conditional( $cond, $trueVal, $falseVal ) { if ( is_array( $cond ) ) { $cond = $this->makeList( $cond, LIST_AND ); } + return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; } @@ -2914,9 +3138,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Returns a comand for str_replace function in SQL query. * Uses REPLACE() in MySQL * - * @param string $orig column to modify - * @param string $old column to seek - * @param string $new column to replace with + * @param string $orig Column to modify + * @param string $old Column to seek + * @param string $new Column to replace with * * @return string */ @@ -3027,9 +3251,11 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { if ( $tries <= 0 ) { $this->rollback( __METHOD__ ); $this->reportQueryError( $error, $errno, $sql, $fname ); + return false; } else { $this->commit( __METHOD__ ); + return $retVal; } } @@ -3037,38 +3263,14 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * 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 + * @param DBMasterPos $pos + * @param int $timeout The maximum number of seconds to wait for * synchronisation - * - * @return integer: zero if the slave was past that position already, + * @return int Zero if the slave was past that position already, * greater than zero if we waited for some period of time, less than * zero if we timed out. */ public function masterPosWait( DBMasterPos $pos, $timeout ) { - wfProfileIn( __METHOD__ ); - - if ( !is_null( $this->mFakeSlaveLag ) ) { - $wait = intval( ( $pos->pos - microtime( true ) + $this->mFakeSlaveLag ) * 1e6 ); - - if ( $wait > $timeout * 1e6 ) { - wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" ); - wfProfileOut( __METHOD__ ); - return -1; - } elseif ( $wait > 0 ) { - wfDebug( "Fake slave waiting $wait us\n" ); - usleep( $wait ); - wfProfileOut( __METHOD__ ); - return 1; - } else { - wfDebug( "Fake slave up to date ($wait us)\n" ); - wfProfileOut( __METHOD__ ); - return 0; - } - } - - wfProfileOut( __METHOD__ ); - # Real waits are implemented in the subclass. return 0; } @@ -3076,30 +3278,21 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Get the replication position of this slave * - * @return DBMasterPos, or false if this is not a slave. + * @return DBMasterPos|bool False if this is not a slave. */ public function getSlavePos() { - if ( !is_null( $this->mFakeSlaveLag ) ) { - $pos = new MySQLMasterPos( 'fake', microtime( true ) - $this->mFakeSlaveLag ); - wfDebug( __METHOD__ . ": fake slave pos = $pos\n" ); - return $pos; - } else { - # Stub - return false; - } + # Stub + return false; } /** * Get the position of this master * - * @return DBMasterPos, or false if this is not a master + * @return DBMasterPos|bool False if this is not a master */ public function getMasterPos() { - if ( $this->mFakeMaster ) { - return new MySQLMasterPos( 'fake', microtime( true ) ); - } else { - return false; - } + # Stub + return false; } /** @@ -3150,7 +3343,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { protected function runOnTransactionIdleCallbacks() { $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled? - $e = null; // last exception + $e = $ePrior = null; // last exception do { // callbacks may add callbacks :) $callbacks = $this->mTrxIdleCallbacks; $this->mTrxIdleCallbacks = array(); // recursion guard @@ -3160,7 +3353,12 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->clearFlag( DBO_TRX ); // make each query its own transaction call_user_func( $phpCallback ); $this->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() - } catch ( Exception $e ) {} + } catch ( Exception $e ) { + if ( $ePrior ) { + MWExceptionHandler::logException( $ePrior ); + } + $ePrior = $e; + } } } while ( count( $this->mTrxIdleCallbacks ) ); @@ -3175,7 +3373,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @since 1.22 */ protected function runOnTransactionPreCommitCallbacks() { - $e = null; // last exception + $e = $ePrior = null; // last exception do { // callbacks may add callbacks :) $callbacks = $this->mTrxPreCommitCallbacks; $this->mTrxPreCommitCallbacks = array(); // recursion guard @@ -3183,7 +3381,12 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { try { list( $phpCallback ) = $callback; call_user_func( $phpCallback ); - } catch ( Exception $e ) {} + } catch ( Exception $e ) { + if ( $ePrior ) { + MWExceptionHandler::logException( $ePrior ); + } + $ePrior = $e; + } } } while ( count( $this->mTrxPreCommitCallbacks ) ); @@ -3193,22 +3396,91 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { } /** - * Begin a transaction. If a transaction is already in progress, that transaction will be committed before the - * new transaction is started. + * Begin an atomic section of statements + * + * If a transaction has been started already, just keep track of the given + * section name to make sure the transaction is not committed pre-maturely. + * This function can be used in layers (with sub-sections), so use a stack + * to keep track of the different atomic sections. If there is no transaction, + * start one implicitly. + * + * The goal of this function is to create an atomic section of SQL queries + * without having to start a new transaction if it already exists. * - * Note that when the DBO_TRX flag is set (which is usually the case for web requests, but not for maintenance scripts), - * any previous database query will have started a transaction automatically. + * Atomic sections are more strict than transactions. With transactions, + * attempting to begin a new transaction when one is already running results + * in MediaWiki issuing a brief warning and doing an implicit commit. All + * atomic levels *must* be explicitly closed using DatabaseBase::endAtomic(), + * and any database transactions cannot be began or committed until all atomic + * levels are closed. There is no such thing as implicitly opening or closing + * an atomic section. + * + * @since 1.23 + * @param string $fname + * @throws DBError + */ + final public function startAtomic( $fname = __METHOD__ ) { + if ( !$this->mTrxLevel ) { + $this->begin( $fname ); + $this->mTrxAutomatic = true; + $this->mTrxAutomaticAtomic = true; + } + + $this->mTrxAtomicLevels->push( $fname ); + } + + /** + * Ends an atomic section of SQL statements * - * Nesting of transactions is not supported. Attempts to nest transactions will cause a warning, unless the current - * transaction was started automatically because of the DBO_TRX flag. + * Ends the next section of atomic SQL statements and commits the transaction + * if necessary. * - * @param $fname string + * @since 1.23 + * @see DatabaseBase::startAtomic + * @param string $fname + * @throws DBError + */ + final public function endAtomic( $fname = __METHOD__ ) { + if ( !$this->mTrxLevel ) { + throw new DBUnexpectedError( $this, 'No atomic transaction is open.' ); + } + if ( $this->mTrxAtomicLevels->isEmpty() || + $this->mTrxAtomicLevels->pop() !== $fname + ) { + throw new DBUnexpectedError( $this, 'Invalid atomic section ended.' ); + } + + if ( $this->mTrxAtomicLevels->isEmpty() && $this->mTrxAutomaticAtomic ) { + $this->commit( $fname, 'flush' ); + } + } + + /** + * Begin a transaction. If a transaction is already in progress, + * that transaction will be committed before the new transaction is started. + * + * Note that when the DBO_TRX flag is set (which is usually the case for web + * requests, but not for maintenance scripts), any previous database query + * will have started a transaction automatically. + * + * Nesting of transactions is not supported. Attempts to nest transactions + * will cause a warning, unless the current transaction was started + * automatically because of the DBO_TRX flag. + * + * @param string $fname + * @throws DBError */ final public function begin( $fname = __METHOD__ ) { global $wgDebugDBTransactions; if ( $this->mTrxLevel ) { // implicit commit - if ( !$this->mTrxAutomatic ) { + if ( !$this->mTrxAtomicLevels->isEmpty() ) { + // If the current transaction was an automatic atomic one, then we definitely have + // a problem. Same if there is any unclosed atomic level. + throw new DBUnexpectedError( $this, + "Attempted to start explicit transaction when atomic levels are still open." + ); + } elseif ( !$this->mTrxAutomatic ) { // We want to warn about inadvertently nested begin/commit pairs, but not about // auto-committing implicit transactions that were started by query() via DBO_TRX $msg = "$fname: Transaction already in progress (from {$this->mTrxFname}), " . @@ -3228,22 +3500,33 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $this->runOnTransactionPreCommitCallbacks(); $this->doCommit( $fname ); if ( $this->mTrxDoneWrites ) { - Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname ); + Profiler::instance()->transactionWritingOut( + $this->mServer, $this->mDBname, $this->mTrxShortId ); } $this->runOnTransactionIdleCallbacks(); } + # Avoid fatals if close() was called + if ( !$this->isOpen() ) { + throw new DBUnexpectedError( $this, "DB connection was already closed." ); + } + $this->doBegin( $fname ); $this->mTrxFname = $fname; $this->mTrxDoneWrites = false; $this->mTrxAutomatic = false; + $this->mTrxAutomaticAtomic = false; + $this->mTrxAtomicLevels = new SplStack; + $this->mTrxIdleCallbacks = array(); + $this->mTrxPreCommitCallbacks = array(); + $this->mTrxShortId = wfRandomString( 12 ); } /** * Issues the BEGIN command to the database server. * * @see DatabaseBase::begin() - * @param type $fname + * @param string $fname */ protected function doBegin( $fname ) { $this->query( 'BEGIN', $fname ); @@ -3256,33 +3539,49 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * Nesting of transactions is not supported. * - * @param $fname string - * @param string $flush Flush flag, set to 'flush' to disable warnings about explicitly committing implicit - * transactions, or calling commit when no transaction is in progress. - * 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. + * @param string $fname + * @param string $flush Flush flag, set to 'flush' to disable warnings about + * explicitly committing implicit transactions, or calling commit when no + * transaction is in progress. 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. + * @throws DBUnexpectedError */ final public function commit( $fname = __METHOD__, $flush = '' ) { - if ( $flush != 'flush' ) { + if ( !$this->mTrxAtomicLevels->isEmpty() ) { + // There are still atomic sections open. This cannot be ignored + throw new DBUnexpectedError( + $this, + "Attempted to commit transaction while atomic sections are still open" + ); + } + + if ( $flush === 'flush' ) { if ( !$this->mTrxLevel ) { - wfWarn( "$fname: No transaction to commit, something got out of sync!" ); - } elseif ( $this->mTrxAutomatic ) { - wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" ); + return; // nothing to do + } elseif ( !$this->mTrxAutomatic ) { + wfWarn( "$fname: Flushing an explicit transaction, getting out of sync!" ); } } else { if ( !$this->mTrxLevel ) { + wfWarn( "$fname: No transaction to commit, something got out of sync!" ); return; // nothing to do - } elseif ( !$this->mTrxAutomatic ) { - wfWarn( "$fname: Flushing an explicit transaction, getting out of sync!" ); + } elseif ( $this->mTrxAutomatic ) { + wfWarn( "$fname: Explicit commit of implicit transaction. Something may be out of sync!" ); } } + # Avoid fatals if close() was called + if ( !$this->isOpen() ) { + throw new DBUnexpectedError( $this, "DB connection was already closed." ); + } + $this->runOnTransactionPreCommitCallbacks(); $this->doCommit( $fname ); if ( $this->mTrxDoneWrites ) { - Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname ); + Profiler::instance()->transactionWritingOut( + $this->mServer, $this->mDBname, $this->mTrxShortId ); } - $this->mTrxDoneWrites = false; $this->runOnTransactionIdleCallbacks(); } @@ -3290,7 +3589,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Issues the COMMIT command to the database server. * * @see DatabaseBase::commit() - * @param type $fname + * @param string $fname */ protected function doCommit( $fname ) { if ( $this->mTrxLevel ) { @@ -3305,26 +3604,49 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * No-op on non-transactional databases. * - * @param $fname string + * @param string $fname + * @param string $flush Flush flag, set to 'flush' to disable warnings about + * calling rollback when no transaction is in progress. 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. + * @since 1.23 Added $flush parameter */ - final public function rollback( $fname = __METHOD__ ) { - if ( !$this->mTrxLevel ) { - wfWarn( "$fname: No transaction to rollback, something got out of sync!" ); + final public function rollback( $fname = __METHOD__, $flush = '' ) { + if ( $flush !== 'flush' ) { + if ( !$this->mTrxLevel ) { + wfWarn( "$fname: No transaction to rollback, something got out of sync!" ); + return; // nothing to do + } elseif ( $this->mTrxAutomatic ) { + wfWarn( "$fname: Explicit rollback of implicit transaction. Something may be out of sync!" ); + } + } else { + if ( !$this->mTrxLevel ) { + return; // nothing to do + } elseif ( !$this->mTrxAutomatic ) { + wfWarn( "$fname: Flushing an explicit transaction, getting out of sync!" ); + } } + + # Avoid fatals if close() was called + if ( !$this->isOpen() ) { + throw new DBUnexpectedError( $this, "DB connection was already closed." ); + } + $this->doRollback( $fname ); $this->mTrxIdleCallbacks = array(); // cancel $this->mTrxPreCommitCallbacks = array(); // cancel + $this->mTrxAtomicLevels = new SplStack; if ( $this->mTrxDoneWrites ) { - Profiler::instance()->transactionWritingOut( $this->mServer, $this->mDBname ); + Profiler::instance()->transactionWritingOut( + $this->mServer, $this->mDBname, $this->mTrxShortId ); } - $this->mTrxDoneWrites = false; } /** * Issues the ROLLBACK command to the database server. * * @see DatabaseBase::rollback() - * @param type $fname + * @param string $fname */ protected function doRollback( $fname ) { if ( $this->mTrxLevel ) { @@ -3341,12 +3663,12 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * The table names passed to this function shall not be quoted (this * function calls addIdentifierQuotes when needed). * - * @param string $oldName name of table whose structure should be copied - * @param string $newName name of table to be created - * @param $temporary Boolean: whether the new table should be temporary - * @param string $fname calling function name + * @param string $oldName Name of table whose structure should be copied + * @param string $newName Name of table to be created + * @param bool $temporary Whether the new table should be temporary + * @param string $fname Calling function name * @throws MWException - * @return Boolean: true if operation was successful + * @return bool True if operation was successful */ public function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ @@ -3359,7 +3681,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * List all tables on the database * * @param string $prefix Only show tables with this prefix, e.g. mw_ - * @param string $fname calling function name + * @param string $fname Calling function name * @throws MWException */ function listTables( $prefix = null, $fname = __METHOD__ ) { @@ -3380,8 +3702,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * 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 + * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_ + * @param string $fname Name of calling function * @throws MWException * @since 1.22 */ @@ -3392,7 +3714,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Differentiates between a TABLE and a VIEW * - * @param $name string: Name of the database-structure to test. + * @param string $name Name of the database-structure to test. * @throws MWException * @since 1.22 */ @@ -3407,7 +3729,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * The result is unquoted, and needs to be passed through addQuotes() * before it can be included in raw SQL. * - * @param $ts string|int + * @param string|int $ts * * @return string */ @@ -3424,7 +3746,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * The result is unquoted, and needs to be passed through addQuotes() * before it can be included in raw SQL. * - * @param $ts string|int + * @param string|int $ts * * @return string */ @@ -3447,8 +3769,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * callers, so this is unnecessary in external code. For compatibility with * old code, ResultWrapper objects are passed through unaltered. * - * @param $result bool|ResultWrapper - * + * @param bool|ResultWrapper|resource $result * @return bool|ResultWrapper */ public function resultObject( $result ) { @@ -3470,7 +3791,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @return bool Success or failure */ public function ping() { - # Stub. Not essential to override. + # Stub. Not essential to override. return true; } @@ -3484,7 +3805,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @return int Database replication lag in seconds */ public function getLag() { - return intval( $this->mFakeSlaveLag ); + return 0; } /** @@ -3501,7 +3822,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * don't allow simple quoted strings to be inserted. To insert into such * a field, pass the data through this function before passing it to * DatabaseBase::insert(). - * @param $b string + * + * @param string $b * @return string */ public function encodeBlob( $b ) { @@ -3512,7 +3834,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Some DBMSs return a special placeholder object representing blob fields * in result objects. Pass the object through this function to return the * original string. - * @param $b string + * + * @param string $b * @return string */ public function decodeBlob( $b ) { @@ -3526,7 +3849,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * full-wiki dumps, where a single query reads out over * hours or days. * - * @param $options Array + * @param array $options * @return void */ public function setSessionOptions( array $options ) { @@ -3542,9 +3865,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * @param bool|callable $lineCallback Optional function called before reading each line * @param bool|callable $resultCallback Optional function called for each MySQL result * @param bool|string $fname Calling function name or false if name should be - * generated dynamically using $filename - * @param bool|callable $inputCallback Callback: Optional function called for each complete line sent - * @throws MWException + * generated dynamically using $filename + * @param bool|callable $inputCallback Optional function called for each + * complete line sent * @throws Exception|MWException * @return bool|string */ @@ -3565,8 +3888,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { try { $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback ); - } - catch ( MWException $e ) { + } catch ( MWException $e ) { fclose( $fp ); throw $e; } @@ -3582,7 +3904,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * it fails back to MySQL if no DB-specific patch can be found * * @param string $patch The name of the patch, like patch-something.sql - * @return String Full path to patch file + * @return string Full path to patch file */ public function patchPath( $patch ) { global $IP; @@ -3600,7 +3922,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at * all. If it's set to false, $GLOBALS will be used. * - * @param bool|array $vars mapping variable name to value. + * @param bool|array $vars Mapping variable name to value. */ public function setSchemaVars( $vars ) { $this->mSchemaVars = $vars; @@ -3612,16 +3934,16 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Returns true on success, error string or exception on failure (depending * on object's error ignore settings). * - * @param $fp Resource: File handle - * @param $lineCallback Callback: Optional function called before reading each query - * @param $resultCallback Callback: Optional function called for each MySQL result + * @param resource $fp File handle + * @param bool|callable $lineCallback Optional function called before reading each query + * @param bool|callable $resultCallback Optional function called for each MySQL result * @param string $fname Calling function name - * @param $inputCallback Callback: Optional function called for each complete query sent + * @param bool|callable $inputCallback Optional function called for each complete query sent * @return bool|string */ public function sourceStream( $fp, $lineCallback = false, $resultCallback = false, - $fname = __METHOD__, $inputCallback = false ) - { + $fname = __METHOD__, $inputCallback = false + ) { $cmd = ''; while ( !feof( $fp ) ) { @@ -3659,6 +3981,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { if ( false === $res ) { $err = $this->lastError(); + return "Query \"{$cmd}\" failed with error code \"$err\".\n"; } } @@ -3674,7 +3997,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * @param string $sql SQL assembled so far * @param string $newLine New line about to be added to $sql - * @return Bool Whether $newLine contains end of the statement + * @return bool Whether $newLine contains end of the statement */ public function streamStatementEnd( &$sql, &$newLine ) { if ( $this->delimiter ) { @@ -3684,6 +4007,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { return true; } } + return false; } @@ -3702,7 +4026,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * table options its use should be avoided. * * @param string $ins SQL statement to replace variables in - * @return String The new SQL statement with variables replaced + * @return string The new SQL statement with variables replaced */ protected function replaceSchemaVars( $ins ) { $vars = $this->getSchemaVars(); @@ -3714,14 +4038,14 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { // replace /*$var*/ $ins = str_replace( '/*$' . $var . '*/', $this->strencode( $value ), $ins ); } + return $ins; } /** * Replace variables in sourced SQL * - * @param $ins string - * + * @param string $ins * @return string */ protected function replaceVars( $ins ) { @@ -3767,8 +4091,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Table name callback * - * @param $matches array - * + * @param array $matches * @return string */ protected function tableNameCallback( $matches ) { @@ -3778,8 +4101,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Index name callback * - * @param $matches array - * + * @param array $matches * @return string */ protected function indexNameCallback( $matches ) { @@ -3789,9 +4111,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * 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 + * @param string $lockName Name of lock to poll + * @param string $method Name of method calling us + * @return bool * @since 1.20 */ public function lockIsFree( $lockName, $method ) { @@ -3804,10 +4126,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Abstracted from Filestore::lock() so child classes can implement for * their own needs. * - * @param string $lockName name of lock to aquire - * @param string $method name of method calling us - * @param $timeout Integer: timeout - * @return Boolean + * @param string $lockName Name of lock to aquire + * @param string $method Name of method calling us + * @param int $timeout + * @return bool */ public function lock( $lockName, $method, $timeout = 5 ) { return true; @@ -3830,11 +4152,10 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Lock specific tables * - * @param array $read of tables to lock for read access - * @param array $write of tables to lock for write access - * @param string $method name of caller + * @param array $read Array of tables to lock for read access + * @param array $write Array of tables to lock for write access + * @param string $method Name of caller * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY - * * @return bool */ public function lockTables( $read, $write, $method, $lowPriority = true ) { @@ -3844,8 +4165,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Unlock specific tables * - * @param string $method the caller - * + * @param string $method The caller * @return bool */ public function unlockTables( $method ) { @@ -3854,8 +4174,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Delete a table - * @param $tableName string - * @param $fName string + * @param string $tableName + * @param string $fName * @return bool|ResultWrapper * @since 1.18 */ @@ -3867,6 +4187,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { if ( $this->cascadingDeletes() ) { $sql .= " CASCADE"; } + return $this->query( $sql, $fName ); } @@ -3874,7 +4195,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Get search engine class. All subclasses of this need to implement this * if they wish to use searching. * - * @return String + * @return string */ public function getSearchEngine() { return 'SearchEngineDummy'; @@ -3885,7 +4206,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * keyword for timestamps in PostgreSQL, and works with CHAR(14) as well * because "i" sorts after all numbers. * - * @return String + * @return string */ public function getInfinity() { return 'infinity'; @@ -3894,8 +4215,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * Encode an expiry time into the DBMS dependent format * - * @param string $expiry timestamp for expiry, or the 'infinity' string - * @return String + * @param string $expiry Timestamp for expiry, or the 'infinity' string + * @return string */ public function encodeExpiry( $expiry ) { return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) @@ -3907,8 +4228,8 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * Decode an expiry time into a DBMS independent format * * @param string $expiry DB timestamp field value for expiry - * @param $format integer: TS_* constant, defaults to TS_MW - * @return String + * @param int $format TS_* constant, defaults to TS_MW + * @return string */ public function decodeExpiry( $expiry, $format = TS_MW ) { return ( $expiry == '' || $expiry == $this->getInfinity() ) @@ -3922,7 +4243,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { * * This is a MySQL-specific feature. * - * @param $value Mixed: true for allow, false for deny, or "default" to + * @param bool|string $value True for allow, false for deny, or "default" to * restore the initial value */ public function setBigSelects( $value = true ) { @@ -3931,6 +4252,7 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { /** * @since 1.19 + * @return string */ public function __toString() { return (string)$this->mConn; @@ -3947,10 +4269,9 @@ abstract class DatabaseBase implements IDatabase, DatabaseType { $callers = array(); foreach ( $this->mTrxIdleCallbacks as $callbackInfo ) { $callers[] = $callbackInfo[1]; - } $callers = implode( ', ', $callers ); - trigger_error( "DB transaction callbacks still pending (from $callers)." ); + trigger_error( "DB transaction callbacks still pending (from $callers)." ); } } } diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php index 0875695f..2dfec41d 100644 --- a/includes/db/DatabaseError.php +++ b/includes/db/DatabaseError.php @@ -26,22 +26,28 @@ * @ingroup Database */ class DBError extends MWException { - - /** - * @var DatabaseBase - */ + /** @var DatabaseBase */ public $db; /** * Construct a database error - * @param $db DatabaseBase object which threw the error + * @param DatabaseBase $db Object which threw the error * @param string $error A simple error message to be used for debugging */ function __construct( DatabaseBase $db = null, $error ) { $this->db = $db; parent::__construct( $error ); } +} +/** + * Base class for the more common types of database errors. These are known to occur + * frequently, so we try to give friendly error messages for them. + * + * @ingroup Database + * @since 1.23 + */ +class DBExpectedError extends DBError { /** * @return string */ @@ -66,8 +72,7 @@ class DBError extends MWException { $s = $this->getHTMLContent(); if ( $wgShowDBErrorBacktrace ) { - $s .= '<p>Backtrace:</p><p>' . - nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . '</p>'; + $s .= '<p>Backtrace:</p><pre>' . htmlspecialchars( $this->getTraceAsString() ) . '</pre>'; } return $s; @@ -84,16 +89,21 @@ class DBError extends MWException { * @return string */ protected function getHTMLContent() { - return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) . '</p>'; + return '<p>' . nl2br( htmlspecialchars( $this->getTextContent() ) ) . '</p>'; } } /** * @ingroup Database */ -class DBConnectionError extends DBError { +class DBConnectionError extends DBExpectedError { + /** @var string Error text */ public $error; + /** + * @param DatabaseBase $db Object throwing the error + * @param string $error Error text + */ function __construct( DatabaseBase $db = null, $error = 'unknown error' ) { $msg = 'DB connection error'; @@ -116,25 +126,24 @@ class DBConnectionError extends DBError { } /** - * @param $key - * @param $fallback - * @return string + * @param string $key + * @param string $fallback Unescaped alternative error text in case the + * message cache cannot be used. Can contain parameters as in regular + * messages, that should be passed as additional parameters. + * @return string Unprocessed plain error text with parameters replaced */ function msg( $key, $fallback /*[, params...] */ ) { - global $wgLang; - $args = array_slice( func_get_args(), 2 ); if ( $this->useMessageCache() ) { - $message = $wgLang->getMessage( $key ); + return wfMessage( $key, $args )->useDatabase( false )->text(); } else { - $message = $fallback; + return wfMsgReplaceArgs( $fallback, $args ); } - return wfMsgReplaceArgs( $message, $args ); } /** - * @return boolean + * @return bool */ function isLoggable() { // Don't send to the exception log, already in dberror log @@ -142,20 +151,19 @@ class DBConnectionError extends DBError { } /** - * @return string - */ - function getPageTitle() { - return $this->msg( 'dberr-header', 'This wiki has a problem' ); - } - - /** - * @return string + * @return string Safe HTML */ function getHTML() { global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors; - $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.' ) ); + $sorry = htmlspecialchars( $this->msg( + 'dberr-problems', + 'Sorry! This site is experiencing technical difficulties.' + ) ); + $again = htmlspecialchars( $this->msg( + 'dberr-again', + 'Try waiting a few minutes and reloading.' + ) ); if ( $wgShowHostnames || $wgShowSQLErrors ) { $info = str_replace( @@ -163,23 +171,25 @@ class DBConnectionError extends DBError { htmlspecialchars( $this->msg( 'dberr-info', '(Cannot contact the database server: $1)' ) ) ); } else { - $info = htmlspecialchars( $this->msg( 'dberr-info-hidden', '(Cannot contact the database server)' ) ); + $info = htmlspecialchars( $this->msg( + 'dberr-info-hidden', + '(Cannot contact the database server)' + ) ); } # No database access MessageCache::singleton()->disable(); - $text = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>"; + $html = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>"; if ( $wgShowDBErrorBacktrace ) { - $text .= '<p>Backtrace:</p><p>' . - nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . '</p>'; + $html .= '<p>Backtrace:</p><pre>' . htmlspecialchars( $this->getTraceAsString() ) . '</pre>'; } - $text .= '<hr />'; - $text .= $this->searchForm(); + $html .= '<hr />'; + $html .= $this->searchForm(); - return $text; + return $html; } protected function getTextContent() { @@ -192,25 +202,31 @@ class DBConnectionError extends DBError { } } + /** + * Output the exception report using HTML. + * + * @return void + */ public function reportHTML() { global $wgUseFileCache; - # Check whether we can serve a file-cached copy of the page with the error underneath + // Check whether we can serve a file-cached copy of the page with the error underneath if ( $wgUseFileCache ) { try { $cache = $this->fileCachedPage(); - # Cached version on file system? + // Cached version on file system? if ( $cache !== null ) { - # Hack: extend the body for error messages + // 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;">' . + // Add cache notice... + $cache .= '<div style="border:1px solid #ffd0d0;padding:1em;">' . htmlspecialchars( $this->msg( 'dberr-cachederror', - 'This is a cached copy of the requested page, and may not be up to date. ' ) ) . + 'This is a cached copy of the requested page, and may not be up to date.' ) ) . '</div>'; - # Output cached page with notices on bottom and re-close body + // Output cached page with notices on bottom and re-close body echo "{$cache}<hr />{$this->getHTML()}</body></html>"; + return; } } catch ( MWException $e ) { @@ -218,7 +234,7 @@ class DBConnectionError extends DBError { } } - # We can't, cough and die in the usual fashion + // We can't, cough and die in the usual fashion parent::reportHTML(); } @@ -228,8 +244,14 @@ class DBConnectionError extends DBError { function searchForm() { global $wgSitename, $wgCanonicalServer, $wgRequest; - $usegoogle = htmlspecialchars( $this->msg( 'dberr-usegoogle', 'You can try searching via Google in the meantime.' ) ); - $outofdate = htmlspecialchars( $this->msg( 'dberr-outofdate', 'Note that their indexes of our content may be out of date.' ) ); + $usegoogle = htmlspecialchars( $this->msg( + 'dberr-usegoogle', + 'You can try searching via Google in the meantime.' + ) ); + $outofdate = htmlspecialchars( $this->msg( + 'dberr-outofdate', + 'Note that their indexes of our content may be out of date.' + ) ); $googlesearch = htmlspecialchars( $this->msg( 'searchbutton', 'Search' ) ); $search = htmlspecialchars( $wgRequest->getVal( 'search' ) ); @@ -239,8 +261,8 @@ class DBConnectionError extends DBError { $trygoogle = <<<EOT <div style="margin: 1.5em">$usegoogle<br /> -<small>$outofdate</small></div> -<!-- SiteSearch Google --> +<small>$outofdate</small> +</div> <form method="get" action="//www.google.com/search" id="googlesearch"> <input type="hidden" name="domains" value="$server" /> <input type="hidden" name="num" value="50" /> @@ -249,13 +271,13 @@ class DBConnectionError extends DBError { <input type="text" name="q" size="31" maxlength="255" value="$search" /> <input type="submit" name="btnG" value="$googlesearch" /> - <div> - <input type="radio" name="sitesearch" id="gwiki" value="$server" checked="checked" /><label for="gwiki">$sitename</label> - <input type="radio" name="sitesearch" id="gWWW" value="" /><label for="gWWW">WWW</label> - </div> + <p> + <label><input type="radio" name="sitesearch" value="$server" checked="checked" />$sitename</label> + <label><input type="radio" name="sitesearch" value="" />WWW</label> + </p> </form> -<!-- SiteSearch Google --> EOT; + return $trygoogle; } @@ -263,26 +285,28 @@ EOT; * @return string */ private function fileCachedPage() { - global $wgTitle, $wgOut, $wgRequest; + $context = RequestContext::getMain(); - if ( $wgOut->isDisabled() ) { - return ''; // Done already? + if ( $context->getOutput()->isDisabled() ) { + // Done already? + return ''; } - if ( $wgTitle ) { // use $wgTitle if we managed to set it - $t = $wgTitle->getPrefixedDBkey(); + if ( $context->getTitle() ) { + // Use the main context's title if we managed to set it + $t = $context->getTitle()->getPrefixedDBkey(); } else { - # Fallback to the raw title URL param. We can't use the Title - # class is it may hit the interwiki table and give a DB error. - # We may get a cache miss due to not sanitizing the title though. - $t = str_replace( ' ', '_', $wgRequest->getVal( 'title' ) ); + // Fallback to the raw title URL param. We can't use the Title + // class is it may hit the interwiki table and give a DB error. + // We may get a cache miss due to not sanitizing the title though. + $t = str_replace( ' ', '_', $context->getRequest()->getVal( 'title' ) ); if ( $t == '' ) { // fallback to main page $t = Title::newFromText( $this->msg( 'mainpage', 'Main Page' ) )->getPrefixedDBkey(); } } - $cache = HTMLFileCache::newFromTitle( $t, 'view' ); + $cache = new HTMLFileCache( $t, 'view' ); if ( $cache->isCached() ) { return $cache->fetchText(); } else { @@ -294,18 +318,20 @@ EOT; /** * @ingroup Database */ -class DBQueryError extends DBError { +class DBQueryError extends DBExpectedError { public $error, $errno, $sql, $fname; /** - * @param $db DatabaseBase - * @param $error string - * @param $errno int|string - * @param $sql string - * @param $fname string + * @param DatabaseBase $db + * @param string $error + * @param int|string $errno + * @param string $sql + * @param string $fname */ function __construct( DatabaseBase $db, $error, $errno, $sql, $fname ) { - $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . + $message = "A database error has occurred. Did you forget to run " . + "maintenance/update.php after upgrading? See: " . + "https://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . "Query: $sql\n" . "Function: $fname\n" . "Error: $errno $error\n"; @@ -318,14 +344,6 @@ class DBQueryError extends DBError { } /** - * @return boolean - */ - function isLoggable() { - // Don't send to the exception log, already in dberror log - return false; - } - - /** * @return string */ function getPageTitle() { @@ -380,7 +398,7 @@ class DBQueryError extends DBError { * 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(). + * @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. */ protected function getTechnicalDetails() { @@ -405,7 +423,7 @@ class DBQueryError extends DBError { /** * @param string $key Message key - * @return string: English message text + * @return string English message text */ private function getFallbackMessage( $key ) { $messages = array( @@ -416,12 +434,13 @@ This may indicate a bug in the software.', 'databaseerror-function' => 'Function: $1', 'databaseerror-error' => 'Error: $1', ); + return $messages[$key]; } - } /** * @ingroup Database */ -class DBUnexpectedError extends DBError {} +class DBUnexpectedError extends DBError { +} diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 37f5372e..af3cc72d 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -22,47 +22,54 @@ * @author Joel Penner <a-joelpe at microsoft dot com> * @author Chris Pucci <a-cpucci at microsoft dot com> * @author Ryan Biesemeyer <v-ryanbi at microsoft dot com> + * @author Ryan Schmidt <skizzerz at gmail dot com> */ /** * @ingroup Database */ class DatabaseMssql extends DatabaseBase { - var $mInsertId = null; - var $mLastResult = null; - var $mAffectedRows = null; - - var $mPort; - - function cascadingDeletes() { + protected $mInsertId = null; + protected $mLastResult = null; + protected $mAffectedRows = null; + protected $mSubqueryId = 0; + protected $mScrollableCursor = true; + protected $mPrepareStatements = true; + protected $mBinaryColumnCache = null; + protected $mBitColumnCache = null; + protected $mIgnoreDupKeyErrors = false; + + protected $mPort; + + public function cascadingDeletes() { return true; } - function cleanupTriggers() { - return true; + public function cleanupTriggers() { + return false; } - function strictIPs() { - return true; + public function strictIPs() { + return false; } - function realTimestamps() { - return true; + public function realTimestamps() { + return false; } - function implicitGroupby() { + public function implicitGroupby() { return false; } - function implicitOrderby() { + public function implicitOrderby() { return false; } - function functionalIndexes() { + public function functionalIndexes() { return true; } - function unionSupportsOrderAndLimit() { + public function unionSupportsOrderAndLimit() { return false; } @@ -75,16 +82,21 @@ class DatabaseMssql extends DatabaseBase { * @throws DBConnectionError * @return bool|DatabaseBase|null */ - function open( $server, $user, $password, $dbName ) { + public function open( $server, $user, $password, $dbName ) { # Test for driver support, to avoid suppressed fatal error if ( !function_exists( 'sqlsrv_connect' ) ) { - throw new DBConnectionError( $this, "MS Sql Server Native (sqlsrv) functions missing. You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" ); + throw new DBConnectionError( + $this, + "Microsoft SQL Server Native (sqlsrv) functions missing. + You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" + ); } - global $wgDBport; + global $wgDBport, $wgDBWindowsAuthentication; - if ( !strlen( $user ) ) { # e.g. the class is being loaded - return; + # e.g. the class is being loaded + if ( !strlen( $user ) ) { + return null; } $this->close(); @@ -100,35 +112,23 @@ class DatabaseMssql extends DatabaseBase { $connectionInfo['Database'] = $dbName; } - // Start NT Auth Hack - // Quick and dirty work around to provide NT Auth designation support. - // Current solution requires installer to know to input 'ntauth' for both username and password - // to trigger connection via NT Auth. - ugly, ugly, ugly - // TO-DO: Make this better and add NT Auth choice to MW installer when SQL Server option is chosen. - $ntAuthUserTest = strtolower( $user ); - $ntAuthPassTest = strtolower( $password ); - // Decide which auth scenerio to use - if ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) { - // Don't add credentials to $connectionInfo - } else { + // if we are using Windows auth, don't add credentials to $connectionInfo + if ( !$wgDBWindowsAuthentication ) { $connectionInfo['UID'] = $user; $connectionInfo['PWD'] = $password; } - // End NT Auth Hack wfSuppressWarnings(); $this->mConn = sqlsrv_connect( $server, $connectionInfo ); wfRestoreWarnings(); if ( $this->mConn === false ) { - wfDebug( "DB connection error\n" ); - wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); - wfDebug( $this->lastError() . "\n" ); - return false; + throw new DBConnectionError( $this, $this->lastError() ); } $this->mOpened = true; + return $this->mConn; } @@ -141,14 +141,39 @@ class DatabaseMssql extends DatabaseBase { return sqlsrv_close( $this->mConn ); } + /** + * @param bool|MssqlResultWrapper|resource $result + * @return bool|MssqlResultWrapper + */ + public function resultObject( $result ) { + if ( empty( $result ) ) { + return false; + } elseif ( $result instanceof MssqlResultWrapper ) { + return $result; + } elseif ( $result === true ) { + // Successful write query + return $result; + } else { + return new MssqlResultWrapper( $this, $result ); + } + } + + /** + * @param string $sql + * @return bool|MssqlResult + * @throws DBUnexpectedError + */ protected function doQuery( $sql ) { - wfDebug( "SQL: [$sql]\n" ); + if ( $this->debug() ) { + wfDebug( "SQL: [$sql]\n" ); + } $this->offset = 0; - // several extensions seem to think that all databases support limits via LIMIT N after the WHERE clause - // well, MSSQL uses SELECT TOP N, so to catch any of those extensions we'll do a quick check for a LIMIT - // clause and pass $sql through $this->LimitToTopN() which parses the limit clause and passes the result to - // $this->limitResult(); + // several extensions seem to think that all databases support limits + // via LIMIT N after the WHERE clause well, MSSQL uses SELECT TOP N, + // so to catch any of those extensions we'll do a quick check for a + // LIMIT clause and pass $sql through $this->LimitToTopN() which parses + // the limit clause and passes the result to $this->limitResult(); if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) { // massage LIMIT -> TopN $sql = $this->LimitToTopN( $sql ); @@ -161,149 +186,235 @@ class DatabaseMssql extends DatabaseBase { } // perform query - $stmt = sqlsrv_query( $this->mConn, $sql ); - if ( $stmt == false ) { - $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . - "Query: " . htmlentities( $sql ) . "\n" . - "Function: " . __METHOD__ . "\n"; - // process each error (our driver will give us an array of errors unlike other providers) - foreach ( sqlsrv_errors() as $error ) { - $message .= $message . "ERROR[" . $error['code'] . "] " . $error['message'] . "\n"; - } - throw new DBUnexpectedError( $this, $message ); + // SQLSRV_CURSOR_STATIC is slower than SQLSRV_CURSOR_CLIENT_BUFFERED (one of the two is + // needed if we want to be able to seek around the result set), however CLIENT_BUFFERED + // has a bug in the sqlsrv driver where wchar_t types (such as nvarchar) that are empty + // strings make php throw a fatal error "Severe error translating Unicode" + if ( $this->mScrollableCursor ) { + $scrollArr = array( 'Scrollable' => SQLSRV_CURSOR_STATIC ); + } else { + $scrollArr = array(); } - // remember number of rows affected - $this->mAffectedRows = sqlsrv_rows_affected( $stmt ); - // if it is a SELECT statement, or an insert with a request to output something we want to return a row. - if ( ( preg_match( '#\bSELECT\s#i', $sql ) ) || - ( preg_match( '#\bINSERT\s#i', $sql ) && preg_match( '#\bOUTPUT\s+INSERTED\b#i', $sql ) ) ) { - // this is essentially a rowset, but Mediawiki calls these 'result' - // the rowset owns freeing the statement - $res = new MssqlResult( $stmt ); + if ( $this->mPrepareStatements ) { + // we do prepare + execute so we can get its field metadata for later usage if desired + $stmt = sqlsrv_prepare( $this->mConn, $sql, array(), $scrollArr ); + $success = sqlsrv_execute( $stmt ); } else { - // otherwise we simply return it was successful, failure throws an exception - $res = true; + $stmt = sqlsrv_query( $this->mConn, $sql, array(), $scrollArr ); + $success = (bool)$stmt; } - return $res; - } - function freeResult( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; + if ( $this->mIgnoreDupKeyErrors ) { + // ignore duplicate key errors, but nothing else + // this emulates INSERT IGNORE in MySQL + if ( $success === false ) { + $errors = sqlsrv_errors( SQLSRV_ERR_ERRORS ); + $success = true; + + foreach ( $errors as $err ) { + if ( $err['SQLSTATE'] == '23000' && $err['code'] == '2601' ) { + continue; // duplicate key error caused by unique index + } elseif ( $err['SQLSTATE'] == '23000' && $err['code'] == '2627' ) { + continue; // duplicate key error caused by primary key + } elseif ( $err['SQLSTATE'] == '01000' && $err['code'] == '3621' ) { + continue; // generic "the statement has been terminated" error + } + + $success = false; // getting here means we got an error we weren't expecting + break; + } + + if ( $success ) { + $this->mAffectedRows = 0; + return $stmt; + } + } + } + + if ( $success === false ) { + return false; } - $res->free(); + // remember number of rows affected + $this->mAffectedRows = sqlsrv_rows_affected( $stmt ); + + return $stmt; } - function fetchObject( $res ) { + public function freeResult( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - $row = $res->fetch( 'OBJECT' ); - return $row; + + sqlsrv_free_stmt( $res ); } - function getErrors() { - $strRet = ''; - $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"; - } - } else { - $strRet = "No errors found"; - } - return $strRet; + /** + * @param MssqlResultWrapper $res + * @return stdClass + */ + public function fetchObject( $res ) { + // $res is expected to be an instance of MssqlResultWrapper here + return $res->fetchObject(); } - function fetchRow( $res ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - $row = $res->fetch( SQLSRV_FETCH_BOTH ); - return $row; + /** + * @param MssqlResultWrapper $res + * @return array + */ + public function fetchRow( $res ) { + return $res->fetchRow(); } - function numRows( $res ) { + /** + * @param mixed $res + * @return int + */ + public function numRows( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - return ( $res ) ? $res->numrows() : 0; + + return sqlsrv_num_rows( $res ); } - function numFields( $res ) { + /** + * @param mixed $res + * @return int + */ + public function numFields( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - return ( $res ) ? $res->numfields() : 0; + + return sqlsrv_num_fields( $res ); } - function fieldName( $res, $n ) { + /** + * @param mixed $res + * @param int $n + * @return int + */ + public function fieldName( $res, $n ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - return ( $res ) ? $res->fieldname( $n ) : 0; + + $metadata = sqlsrv_field_metadata( $res ); + return $metadata[$n]['Name']; } /** * This must be called after nextSequenceVal - * @return null + * @return int|null */ - function insertId() { + public function insertId() { return $this->mInsertId; } - function dataSeek( $res, $row ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - return ( $res ) ? $res->seek( $row ) : false; + /** + * @param MssqlResultWrapper $res + * @param int $row + * @return bool + */ + public function dataSeek( $res, $row ) { + return $res->seek( $row ); } - function lastError() { - if ( $this->mConn ) { - return $this->getErrors(); + /** + * @return string + */ + public function lastError() { + $strRet = ''; + $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL ); + if ( $retErrors != null ) { + foreach ( $retErrors as $arrError ) { + $strRet .= $this->formatError( $arrError ) . "\n"; + } } else { - return "No database connection"; + $strRet = "No errors found"; } + + return $strRet; + } + + /** + * @param array $err + * @return string + */ + private function formatError( $err ) { + return '[SQLSTATE ' . $err['SQLSTATE'] . '][Error Code ' . $err['code'] . ']' . $err['message']; } - function lastErrno() { + /** + * @return string + */ + public function lastErrno() { $err = sqlsrv_errors( SQLSRV_ERR_ALL ); - if ( $err[0] ) { + if ( $err !== null && isset( $err[0] ) ) { return $err[0]['code']; } else { return 0; } } - function affectedRows() { + /** + * @return int + */ + public function affectedRows() { return $this->mAffectedRows; } /** * SELECT wrapper * - * @param $table Mixed: array or string, table name(s) (prefix auto-added) - * @param $vars Mixed: array or string, field name(s) to be retrieved - * @param $conds Mixed: array or string, condition(s) for WHERE - * @param $fname String: calling function name (use __METHOD__) for logs/profiling - * @param array $options associative array of options (e.g. array('GROUP BY' => 'page_title')), - * see Database::makeSelectOptions code for list of supported stuff - * @param $join_conds Array: Associative array of table join conditions (optional) - * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) - * @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure + * @param mixed $table Array or string, table name(s) (prefix auto-added) + * @param mixed $vars Array or string, field name(s) to be retrieved + * @param mixed $conds Array or string, condition(s) for WHERE + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @param array $options Associative array of options (e.g. + * array('GROUP BY' => 'page_title')), see Database::makeSelectOptions + * code for list of supported stuff + * @param array $join_conds Associative array of table join conditions + * (optional) (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) + * @return mixed Database result resource (feed to Database::fetchObject + * or whatever), or false on failure */ - function select( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) - { + public 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'] ) ) { - sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL ON;" ); - $ret = $this->query( $sql, $fname ); - sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" ); + try { + $this->mScrollableCursor = false; + $this->mPrepareStatements = false; + $this->query( "SET SHOWPLAN_ALL ON" ); + $ret = $this->query( $sql, $fname ); + $this->query( "SET SHOWPLAN_ALL OFF" ); + } catch ( DBQueryError $dqe ) { + if ( isset( $options['FOR COUNT'] ) ) { + // likely don't have privs for SHOWPLAN, so run a select count instead + $this->query( "SET SHOWPLAN_ALL OFF" ); + unset( $options['EXPLAIN'] ); + $ret = $this->select( + $table, + 'COUNT(*) AS EstimateRows', + $conds, + $fname, + $options, + $join_conds + ); + } else { + // someone actually wanted the query plan instead of an est row count + // let them know of the error + $this->mScrollableCursor = true; + $this->mPrepareStatements = true; + throw $dqe; + } + } + $this->mScrollableCursor = true; + $this->mPrepareStatements = true; return $ret; } return $this->query( $sql, $fname ); @@ -312,21 +423,70 @@ class DatabaseMssql extends DatabaseBase { /** * SELECT wrapper * - * @param $table Mixed: Array or string, table name(s) (prefix auto-added) - * @param $vars Mixed: Array or string, field name(s) to be retrieved - * @param $conds Mixed: Array or string, condition(s) for WHERE - * @param $fname String: Calling function name (use __METHOD__) for logs/profiling - * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), - * see Database::makeSelectOptions code for list of supported stuff - * @param $join_conds Array: Associative array of table join conditions (optional) - * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) - * @return string, the SQL text + * @param mixed $table Array or string, table name(s) (prefix auto-added) + * @param mixed $vars Array or string, field name(s) to be retrieved + * @param mixed $conds Array or string, condition(s) for WHERE + * @param string $fname Calling function name (use __METHOD__) for logs/profiling + * @param array $options Associative array of options (e.g. array('GROUP BY' => 'page_title')), + * see Database::makeSelectOptions code for list of supported stuff + * @param array $join_conds Associative array of table join conditions (optional) + * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) + * @return string The SQL text */ - function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) { + public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, + $options = array(), $join_conds = array() + ) { if ( isset( $options['EXPLAIN'] ) ) { unset( $options['EXPLAIN'] ); } - return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); + + $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); + + // try to rewrite aggregations of bit columns (currently MAX and MIN) + if ( strpos( $sql, 'MAX(' ) !== false || strpos( $sql, 'MIN(' ) !== false ) { + $bitColumns = array(); + if ( is_array( $table ) ) { + foreach ( $table as $t ) { + $bitColumns += $this->getBitColumns( $this->tableName( $t ) ); + } + } else { + $bitColumns = $this->getBitColumns( $this->tableName( $table ) ); + } + + foreach ( $bitColumns as $col => $info ) { + $replace = array( + "MAX({$col})" => "MAX(CAST({$col} AS tinyint))", + "MIN({$col})" => "MIN(CAST({$col} AS tinyint))", + ); + $sql = str_replace( array_keys( $replace ), array_values( $replace ), $sql ); + } + } + + return $sql; + } + + public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, + $fname = __METHOD__ + ) { + $this->mScrollableCursor = false; + try { + parent::deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname ); + } catch ( Exception $e ) { + $this->mScrollableCursor = true; + throw $e; + } + $this->mScrollableCursor = true; + } + + public function delete( $table, $conds, $fname = __METHOD__ ) { + $this->mScrollableCursor = false; + try { + parent::delete( $table, $conds, $fname ); + } catch ( Exception $e ) { + $this->mScrollableCursor = true; + throw $e; + } + $this->mScrollableCursor = true; } /** @@ -335,30 +495,45 @@ class DatabaseMssql extends DatabaseBase { * This is not necessarily an accurate estimate, so use sparingly * Returns -1 if count cannot be found * Takes same arguments as Database::select() + * @param string $table + * @param string $vars + * @param string $conds + * @param string $fname + * @param array $options * @return int */ - function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) { - $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx + public function estimateRowCount( $table, $vars = '*', $conds = '', + $fname = __METHOD__, $options = array() + ) { + // http://msdn2.microsoft.com/en-us/library/aa259203.aspx + $options['EXPLAIN'] = true; + $options['FOR COUNT'] = true; $res = $this->select( $table, $vars, $conds, $fname, $options ); $rows = -1; if ( $res ) { $row = $this->fetchRow( $res ); + if ( isset( $row['EstimateRows'] ) ) { $rows = $row['EstimateRows']; } } + return $rows; } /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure + * @param string $table + * @param string $index + * @param string $fname * @return array|bool|null */ - 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. + public 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 . "'"; $res = $this->query( $sql, $fname ); if ( !$res ) { @@ -383,6 +558,7 @@ class DatabaseMssql extends DatabaseBase { } } } + return empty( $result ) ? false : $result; } @@ -401,7 +577,7 @@ class DatabaseMssql extends DatabaseBase { * @throws DBQueryError * @return bool */ - function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) { + public function insert( $table, $arrToInsert, $fname = __METHOD__, $options = array() ) { # No rows to insert, easy just return now if ( !count( $arrToInsert ) ) { return true; @@ -413,24 +589,39 @@ class DatabaseMssql extends DatabaseBase { $table = $this->tableName( $table ); - if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row - $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible + if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) { // Not multi row + $arrToInsert = array( 0 => $arrToInsert ); // make everything multi row compatible } - $allOk = true; - // We know the table we're inserting into, get its identity column $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() ) { + // strip matching square brackets and the db/schema from table name + $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); + $tableRaw = array_pop( $tableRawArr ); + $res = $this->doQuery( + "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS " . + "WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" + ); + if ( $res && sqlsrv_has_rows( $res ) ) { // There is an identity for this table. - $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) ); + $identityArr = sqlsrv_fetch_array( $res, SQLSRV_FETCH_ASSOC ); + $identity = array_pop( $identityArr ); + } + sqlsrv_free_stmt( $res ); + + // Determine binary/varbinary fields so we can encode data as a hex string like 0xABCDEF + $binaryColumns = $this->getBinaryColumns( $table ); + + // INSERT IGNORE is not supported by SQL Server + // remove IGNORE from options list and set ignore flag to true + if ( in_array( 'IGNORE', $options ) ) { + $options = array_diff( $options, array( 'IGNORE' ) ); + $this->mIgnoreDupKeyErrors = true; } - unset( $res ); foreach ( $arrToInsert as $a ) { - // start out with empty identity column, this is so we can return it as a result of the insert logic + // start out with empty identity column, this is so we can return + // it as a result of the insert logic $sqlPre = ''; $sqlPost = ''; $identityClause = ''; @@ -441,152 +632,246 @@ class DatabaseMssql extends DatabaseBase { foreach ( $a as $k => $v ) { if ( $k == $identity ) { if ( !is_null( $v ) ) { - // there is a value being passed to us, we need to turn on and off inserted identity + // 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;"; - } else { - // we can't insert NULL into an identity column, so remove the column from the insert. + // we can't insert NULL into an identity column, + // so remove the column from the insert. unset( $a[$k] ); } } } - $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result - } - - $keys = array_keys( $a ); - // INSERT IGNORE is not supported by SQL Server - // remove IGNORE from options list and set ignore flag to true - $ignoreClause = false; - foreach ( $options as $k => $v ) { - if ( strtoupper( $v ) == "IGNORE" ) { - unset( $options[$k] ); - $ignoreClause = true; - } + // we want to output an identity column as result + $identityClause = "OUTPUT INSERTED.$identity "; } - // translate MySQL INSERT IGNORE to something SQL Server can use - // example: - // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop') - // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop') - if ( $ignoreClause ) { - $prival = $a[$keys[0]]; - $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')"; - } + $keys = array_keys( $a ); // Build the actual query $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) . " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES ("; $first = true; - foreach ( $a as $value ) { + foreach ( $a as $key => $value ) { + if ( isset( $binaryColumns[$key] ) ) { + $value = new MssqlBlob( $value ); + } if ( $first ) { $first = false; } else { $sql .= ','; } - if ( is_string( $value ) ) { - $sql .= $this->addQuotes( $value ); - } elseif ( is_null( $value ) ) { + if ( is_null( $value ) ) { $sql .= 'null'; } elseif ( is_array( $value ) || is_object( $value ) ) { - if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) { + if ( is_object( $value ) && $value instanceof Blob ) { $sql .= $this->addQuotes( $value ); } else { $sql .= $this->addQuotes( serialize( $value ) ); } } else { - $sql .= $value; + $sql .= $this->addQuotes( $value ); } } $sql .= ')' . $sqlPost; // Run the query - $ret = sqlsrv_query( $this->mConn, $sql ); - - if ( $ret === false ) { - throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname ); - } elseif ( $ret != null ) { - // remember number of rows affected - $this->mAffectedRows = sqlsrv_rows_affected( $ret ); - 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->mScrollableCursor = false; + try { + $ret = $this->query( $sql ); + } catch ( Exception $e ) { + $this->mScrollableCursor = true; + $this->mIgnoreDupKeyErrors = false; + throw $e; + } + $this->mScrollableCursor = true; + + if ( !is_null( $identity ) ) { + // then we want to get the identity column value we were assigned and save it off + $row = $ret->fetchObject(); + if( is_object( $row ) ){ $this->mInsertId = $row->$identity; } - sqlsrv_free_stmt( $ret ); - continue; } - $allOk = false; } - return $allOk; + $this->mIgnoreDupKeyErrors = false; + return $ret; } /** * INSERT SELECT wrapper * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...) - * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() - * $conds may be "*" to copy the whole table - * srcTable may be an array of tables. + * Source items may be literals rather than field names, but strings should + * be quoted with Database::addQuotes(). * @param string $destTable - * @param array|string $srcTable + * @param array|string $srcTable May be an array of tables. * @param array $varMap - * @param array $conds + * @param array $conds May be "*" to copy the whole table. * @param string $fname * @param array $insertOptions * @param array $selectOptions * @throws DBQueryError * @return null|ResultWrapper */ - function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = array(), $selectOptions = array() ) { - $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions ); - - if ( $ret === false ) { - throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), /*$sql*/ '', $fname ); - } elseif ( $ret != null ) { - // remember number of rows affected - $this->mAffectedRows = sqlsrv_rows_affected( $ret ); - return $ret; + public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, + $insertOptions = array(), $selectOptions = array() + ) { + $this->mScrollableCursor = false; + try { + $ret = parent::insertSelect( + $destTable, + $srcTable, + $varMap, + $conds, + $fname, + $insertOptions, + $selectOptions + ); + } catch ( Exception $e ) { + $this->mScrollableCursor = true; + throw $e; } - return null; + $this->mScrollableCursor = true; + + return $ret; } /** - * Return the next in a sequence, save the value for retrieval via insertId() - * @return + * UPDATE wrapper. Takes a condition array and a SET array. + * + * @param string $table Name of the table to UPDATE. This will be passed through + * DatabaseBase::tableName(). + * + * @param array $values 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 array $conds An array of conditions (WHERE). See + * DatabaseBase::select() for the details of the format of + * condition arrays. Use '*' to update all rows. + * + * @param string $fname The function name of the caller (from __METHOD__), + * for logging and profiling. + * + * @param array $options An array of UPDATE options, can be: + * - IGNORE: Ignore unique key conflicts + * - LOW_PRIORITY: MySQL-specific, see MySQL manual. + * @return bool */ - function nextSequenceValue( $seqName ) { - if ( !$this->tableExists( 'sequence_' . $seqName ) ) { - sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" ); + function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { + $table = $this->tableName( $table ); + $binaryColumns = $this->getBinaryColumns( $table ); + + $opts = $this->makeUpdateOptions( $options ); + $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET, $binaryColumns ); + + if ( $conds !== array() && $conds !== '*' ) { + $sql .= " WHERE " . $this->makeList( $conds, LIST_AND, $binaryColumns ); } - sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" ); - $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" ); - $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't - sqlsrv_free_stmt( $ret ); - $this->mInsertId = $row['id']; - return $row['id']; + $this->mScrollableCursor = false; + try { + $ret = $this->query( $sql ); + } catch ( Exception $e ) { + $this->mScrollableCursor = true; + throw $e; + } + $this->mScrollableCursor = true; + return true; } /** - * Return the current value of a sequence. Assumes it has ben nextval'ed in this session. - * @return + * Makes an encoded list of strings from an array + * @param array $a Containing the data + * @param int $mode Constant + * - LIST_COMMA: comma separated, no field names + * - LIST_AND: ANDed WHERE clause (without the WHERE). See + * the documentation for $conds in DatabaseBase::select(). + * - LIST_OR: ORed WHERE clause (without the WHERE) + * - LIST_SET: comma separated with field names, like a SET clause + * - LIST_NAMES: comma separated field names + * @param array $binaryColumns Contains a list of column names that are binary types + * This is a custom parameter only present for MS SQL. + * + * @throws MWException|DBUnexpectedError + * @return string */ - function currentSequenceValue( $seqName ) { - $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" ); - if ( $ret !== false ) { - $row = sqlsrv_fetch_array( $ret ); - sqlsrv_free_stmt( $ret ); - return $row['id']; - } else { - return $this->nextSequenceValue( $seqName ); + public function makeList( $a, $mode = LIST_COMMA, $binaryColumns = array() ) { + if ( !is_array( $a ) ) { + throw new DBUnexpectedError( $this, + 'DatabaseBase::makeList called with incorrect parameters' ); } + + $first = true; + $list = ''; + + foreach ( $a as $field => $value ) { + if ( $mode != LIST_NAMES && isset( $binaryColumns[$field] ) ) { + if ( is_array( $value ) ) { + foreach ( $value as &$v ) { + $v = new MssqlBlob( $v ); + } + } else { + $value = new MssqlBlob( $value ); + } + } + + if ( !$first ) { + if ( $mode == LIST_AND ) { + $list .= ' AND '; + } elseif ( $mode == LIST_OR ) { + $list .= ' OR '; + } else { + $list .= ','; + } + } else { + $first = false; + } + + if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) { + $list .= "($value)"; + } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) { + $list .= "$value"; + } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) { + if ( count( $value ) == 0 ) { + throw new MWException( __METHOD__ . ": empty input for field $field" ); + } elseif ( count( $value ) == 1 ) { + // Special-case single values, as IN isn't terribly efficient + // Don't necessarily assume the single key is 0; we don't + // enforce linear numeric ordering on other arrays here. + $value = array_values( $value ); + $list .= $field . " = " . $this->addQuotes( $value[0] ); + } else { + $list .= $field . " IN (" . $this->makeList( $value ) . ") "; + } + } elseif ( $value === null ) { + if ( $mode == LIST_AND || $mode == LIST_OR ) { + $list .= "$field IS "; + } elseif ( $mode == LIST_SET ) { + $list .= "$field = "; + } + $list .= 'NULL'; + } else { + if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { + $list .= "$field = "; + } + $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value ); + } + } + + return $list; } - # Returns the size of a text field, or -1 for "unlimited" - function textFieldSize( $table, $field ) { + /** + * @param string $table + * @param string $field + * @return int Returns the size of a text field, or -1 for "unlimited" + */ + public function textFieldSize( $table, $field ) { $table = $this->tableName( $table ); $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'"; @@ -596,40 +881,75 @@ class DatabaseMssql extends DatabaseBase { if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) { $size = $row['CHARACTER_MAXIMUM_LENGTH']; } + return $size; } /** * Construct a LIMIT query with optional offset * This is used for query pages - * $sql string SQL query we will append the limit too - * $limit integer the SQL limit - * $offset integer the SQL offset (default false) - * @return mixed|string + * + * @param string $sql SQL query we will append the limit too + * @param int $limit The SQL limit + * @param bool|int $offset The SQL offset (default false) + * @return array|string */ - function limitResult( $sql, $limit, $offset = false ) { + public function limitResult( $sql, $limit, $offset = false ) { if ( $offset === false || $offset == 0 ) { if ( strpos( $sql, "SELECT" ) === false ) { return "TOP {$limit} " . $sql; } else { - return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 ); + return preg_replace( '/\bSELECT(\s+DISTINCT)?\b/Dsi', + 'SELECT$1 TOP ' . $limit, $sql, 1 ); } } else { - $sql = ' - SELECT * FROM ( - SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM ( - SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1 - ) as sub2 - ) AS sub3 - WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit ); + // This one is fun, we need to pull out the select list as well as any ORDER BY clause + $select = $orderby = array(); + $s1 = preg_match( '#SELECT\s+(.+?)\s+FROM#Dis', $sql, $select ); + $s2 = preg_match( '#(ORDER BY\s+.+?)(\s*FOR XML .*)?$#Dis', $sql, $orderby ); + $overOrder = $postOrder = ''; + $first = $offset + 1; + $last = $offset + $limit; + $sub1 = 'sub_' . $this->mSubqueryId; + $sub2 = 'sub_' . ( $this->mSubqueryId + 1 ); + $this->mSubqueryId += 2; + if ( !$s1 ) { + // wat + throw new DBUnexpectedError( $this, "Attempting to LIMIT a non-SELECT query\n" ); + } + if ( !$s2 ) { + // no ORDER BY + $overOrder = 'ORDER BY (SELECT 1)'; + } else { + if ( !isset( $orderby[2] ) || !$orderby[2] ) { + // don't need to strip it out if we're using a FOR XML clause + $sql = str_replace( $orderby[1], '', $sql ); + } + $overOrder = $orderby[1]; + $postOrder = ' ' . $overOrder; + } + $sql = "SELECT {$select[1]} + FROM ( + SELECT ROW_NUMBER() OVER({$overOrder}) AS rowNumber, * + FROM ({$sql}) {$sub1} + ) {$sub2} + WHERE rowNumber BETWEEN {$first} AND {$last}{$postOrder}"; + return $sql; } } - // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult() - // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser. - // This exists becase there are still too many extensions that don't use dynamic sql generation. - function LimitToTopN( $sql ) { + /** + * If there is a limit clause, parse it, strip it, and pass the remaining + * SQL through limitResult() with the appropriate parameters. Not the + * prettiest solution, but better than building a whole new parser. This + * exists becase there are still too many extensions that don't use dynamic + * sql generation. + * + * @param string $sql + * @return array|mixed|string + */ + public function LimitToTopN( $sql ) { // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset} $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i'; if ( preg_match( $pattern, $sql, $matches ) ) { @@ -637,22 +957,20 @@ class DatabaseMssql extends DatabaseBase { $row_count = $matches[4]; // offset = $matches[3] OR $matches[6] $offset = $matches[3] or - $offset = $matches[6] or - $offset = false; + $offset = $matches[6] or + $offset = false; // strip the matching LIMIT clause out $sql = str_replace( $matches[0], '', $sql ); + return $this->limitResult( $sql, $row_count, $offset ); } - return $sql; - } - function timestamp( $ts = 0 ) { - return wfTimestamp( TS_ISO_8601, $ts ); + return $sql; } /** - * @return string wikitext of a link to the server software's web site + * @return string Wikitext of a link to the server software's web site */ public function getSoftwareLink() { return "[{{int:version-db-mssql-url}} MS SQL Server]"; @@ -661,23 +979,40 @@ class DatabaseMssql extends DatabaseBase { /** * @return string Version information from the database */ - function getServerVersion() { + public function getServerVersion() { $server_info = sqlsrv_server_info( $this->mConn ); $version = 'Error'; if ( isset( $server_info['SQLServerVersion'] ) ) { $version = $server_info['SQLServerVersion']; } + return $version; } - 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(); + /** + * @param string $table + * @param string $fname + * @return bool + */ + public function tableExists( $table, $fname = __METHOD__ ) { + list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); + + if ( $db !== false ) { + // remote database + wfDebug( "Attempting to call tableExists on a remote table" ); return false; } - if ( sqlsrv_fetch( $res ) ) { + + if ( $schema === false ) { + global $wgDBmwschema; + $schema = $wgDBmwschema; + } + + $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE = 'BASE TABLE' + AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'" ); + + if ( $res->numRows() ) { return true; } else { return false; @@ -686,40 +1021,53 @@ class DatabaseMssql extends DatabaseBase { /** * Query whether a given column exists in the mediawiki schema + * @param string $table + * @param string $field + * @param string $fname * @return bool */ - 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(); + public function fieldExists( $table, $field, $fname = __METHOD__ ) { + list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); + + if ( $db !== false ) { + // remote database + wfDebug( "Attempting to call fieldExists on a remote table" ); return false; } - if ( sqlsrv_fetch( $res ) ) { + + $res = $this->query( "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); + + if ( $res->numRows() ) { return true; } else { return false; } } - function fieldInfo( $table, $field ) { - $table = $this->tableName( $table ); - $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(); + public function fieldInfo( $table, $field ) { + list( $db, $schema, $table ) = $this->tableName( $table, 'split' ); + + if ( $db !== false ) { + // remote database + wfDebug( "Attempting to call fieldInfo on a remote table" ); return false; } - $meta = $this->fetchRow( $res ); + + $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); + + $meta = $res->fetchRow(); if ( $meta ) { return new MssqlField( $meta ); } + return false; } /** * Begin a transaction, committing any previously open transaction + * @param string $fname */ protected function doBegin( $fname = __METHOD__ ) { sqlsrv_begin_transaction( $this->mConn ); @@ -728,6 +1076,7 @@ class DatabaseMssql extends DatabaseBase { /** * End a transaction + * @param string $fname */ protected function doCommit( $fname = __METHOD__ ) { sqlsrv_commit( $this->mConn ); @@ -737,6 +1086,7 @@ class DatabaseMssql extends DatabaseBase { /** * Rollback a transaction. * No-op on non-transactional databases. + * @param string $fname */ protected function doRollback( $fname = __METHOD__ ) { sqlsrv_rollback( $this->mConn ); @@ -747,7 +1097,7 @@ class DatabaseMssql extends DatabaseBase { * Escapes a identifier for use inm SQL. * Throws an exception if it is invalid. * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx - * @param $identifier + * @param string $identifier * @throws MWException * @return string */ @@ -758,153 +1108,82 @@ class DatabaseMssql extends DatabaseBase { if ( strlen( $identifier ) > 128 ) { throw new MWException( "The identifier '$identifier' is too long (max. 128)" ); } - if ( ( strpos( $identifier, '[' ) !== false ) || ( strpos( $identifier, ']' ) !== false ) ) { - // It may be allowed if you quoted with double quotation marks, but that would break if QUOTED_IDENTIFIER is OFF - throw new MWException( "You can't use square brackers in the identifier '$identifier'" ); + if ( ( strpos( $identifier, '[' ) !== false ) + || ( strpos( $identifier, ']' ) !== false ) + ) { + // It may be allowed if you quoted with double quotation marks, but + // that would break if QUOTED_IDENTIFIER is OFF + throw new MWException( "Square brackets are not allowed in '$identifier'" ); } + return "[$identifier]"; } /** - * Initial setup. - * Precondition: This object is connected as the superuser. - * Creates the database, schema, user and login. + * @param string $s + * @return string */ - function initial_setup( $dbName, $newUser, $loginPassword ) { - $dbName = $this->escapeIdentifier( $dbName ); - - // It is not clear what can be used as a login, - // From http://msdn.microsoft.com/en-us/library/ms173463.aspx - // a sysname may be the same as an identifier. - $newUser = $this->escapeIdentifier( $newUser ); - $loginPassword = $this->addQuotes( $loginPassword ); - - $this->doQuery( "CREATE DATABASE $dbName;" ); - $this->doQuery( "USE $dbName;" ); - $this->doQuery( "CREATE SCHEMA $dbName;" ); - $this->doQuery( " - CREATE - LOGIN $newUser - WITH - PASSWORD=$loginPassword - ; - " ); - $this->doQuery( " - CREATE - USER $newUser - FOR - LOGIN $newUser - WITH - DEFAULT_SCHEMA=$dbName - ; - " ); - $this->doQuery( " - GRANT - BACKUP DATABASE, - BACKUP LOG, - CREATE DEFAULT, - CREATE FUNCTION, - CREATE PROCEDURE, - CREATE RULE, - CREATE TABLE, - CREATE VIEW, - CREATE FULLTEXT CATALOG - ON - DATABASE::$dbName - TO $newUser - ; - " ); - $this->doQuery( " - GRANT - CONTROL - ON - SCHEMA::$dbName - TO $newUser - ; - " ); - } - - function encodeBlob( $b ) { - // we can't have zero's and such, this is a simple encoding to make sure we don't barf - return base64_encode( $b ); - } - - function decodeBlob( $b ) { - // we can't have zero's and such, this is a simple encoding to make sure we don't barf - return base64_decode( $b ); + public function strencode( $s ) { # Should not be called by us + return str_replace( "'", "''", $s ); } /** - * @private + * @param string $s * @return string */ - function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) { - $ret = array(); - $retJOIN = array(); - $use_index_safe = is_array( $use_index ) ? $use_index : array(); - $join_conds_safe = is_array( $join_conds ) ? $join_conds : array(); - foreach ( $tables as $table ) { - // Is there a JOIN and INDEX clause for this table? - if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) { - $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table ); - $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) ); - $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')'; - $retJOIN[] = $tableClause; - // Is there an INDEX clause? - } elseif ( isset( $use_index_safe[$table] ) ) { - $tableClause = $this->tableName( $table ); - $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) ); - $ret[] = $tableClause; - // Is there a JOIN clause? - } elseif ( isset( $join_conds_safe[$table] ) ) { - $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table ); - $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')'; - $retJOIN[] = $tableClause; - } else { - $tableClause = $this->tableName( $table ); - $ret[] = $tableClause; - } - } - // We can't separate explicit JOIN clauses with ',', use ' ' for those - $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : ""; - $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : ""; - // Compile our final table clause - return implode( ' ', array( $straightJoins, $otherJoins ) ); - } - - function strencode( $s ) { # Should not be called by us - return str_replace( "'", "''", $s ); - } - - function addQuotes( $s ) { - if ( $s instanceof Blob ) { - return "'" . $s->fetch( $s ) . "'"; + public function addQuotes( $s ) { + if ( $s instanceof MssqlBlob ) { + return $s->fetch(); + } elseif ( $s instanceof Blob ) { + // this shouldn't really ever be called, but it's here if needed + // (and will quite possibly make the SQL error out) + $blob = new MssqlBlob( $s->fetch() ); + return $blob->fetch(); } else { + if ( is_bool( $s ) ) { + $s = $s ? 1 : 0; + } return parent::addQuotes( $s ); } } + /** + * @param string $s + * @return string + */ public function addIdentifierQuotes( $s ) { // http://msdn.microsoft.com/en-us/library/aa223962.aspx return '[' . $s . ']'; } + /** + * @param string $name + * @return bool + */ public function isQuotedIdentifier( $name ) { - return $name[0] == '[' && substr( $name, -1, 1 ) == ']'; + return strlen( $name ) && $name[0] == '[' && substr( $name, -1, 1 ) == ']'; } - function selectDB( $db ) { - return ( $this->query( "SET DATABASE $db" ) !== false ); + /** + * @param string $db + * @return bool + */ + public function selectDB( $db ) { + try { + $this->mDBname = $db; + $this->query( "USE $db" ); + return true; + } catch ( Exception $e ) { + return false; + } } /** - * @private - * - * @param array $options an associative array of options to be turned into - * an SQL query, valid keys are listed in the function. - * @return Array + * @param array $options An associative array of options to be turned into + * an SQL query, valid keys are listed in the function. + * @return array */ - function makeSelectOptions( $options ) { + public function makeSelectOptions( $options ) { $tailOpts = ''; $startOpts = ''; @@ -919,10 +1198,15 @@ class DatabaseMssql extends DatabaseBase { $tailOpts .= $this->makeOrderBy( $options ); - if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) { + if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { $startOpts .= 'DISTINCT'; } + if ( isset( $noKeyOptions['FOR XML'] ) ) { + // used in group concat field emulation + $tailOpts .= " FOR XML PATH('')"; + } + // we want this to be compatible with the output of parent::makeSelectOptions() return array( $startOpts, '', $tailOpts, '' ); } @@ -931,27 +1215,165 @@ class DatabaseMssql extends DatabaseBase { * Get the type of the DBMS, as it appears in $wgDBtype. * @return string */ - function getType() { + public function getType() { return 'mssql'; } - function buildConcat( $stringList ) { + /** + * @param array $stringList + * @return string + */ + public function buildConcat( $stringList ) { return implode( ' + ', $stringList ); } + /** + * Build a GROUP_CONCAT or equivalent statement for a query. + * MS SQL doesn't have GROUP_CONCAT so we emulate it with other stuff (and boy is it nasty) + * + * This is useful for combining a field for several rows into a single string. + * NULL values will not appear in the output, duplicated values will appear, + * and the resulting delimiter-separated values have no defined sort order. + * Code using the results may need to use the PHP unique() or sort() methods. + * + * @param string $delim Glue to bind the results together + * @param string|array $table Table name + * @param string $field Field name + * @param string|array $conds Conditions + * @param string|array $join_conds Join conditions + * @return string SQL text + * @since 1.23 + */ + public function buildGroupConcatField( $delim, $table, $field, $conds = '', + $join_conds = array() + ) { + $gcsq = 'gcsq_' . $this->mSubqueryId; + $this->mSubqueryId++; + + $delimLen = strlen( $delim ); + $fld = "{$field} + {$this->addQuotes( $delim )}"; + $sql = "(SELECT LEFT({$field}, LEN({$field}) - {$delimLen}) FROM (" + . $this->selectSQLText( $table, $fld, $conds, null, array( 'FOR XML' ), $join_conds ) + . ") {$gcsq} ({$field}))"; + + return $sql; + } + + /** + * @return string + */ public function getSearchEngine() { return "SearchMssql"; } /** - * Since MSSQL doesn't recognize the infinity keyword, set date manually. - * @todo Remove magic date + * Returns an associative array for fields that are of type varbinary, binary, or image + * $table can be either a raw table name or passed through tableName() first + * @param string $table + * @return array + */ + private function getBinaryColumns( $table ) { + $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); + $tableRaw = array_pop( $tableRawArr ); + + if ( $this->mBinaryColumnCache === null ) { + $this->populateColumnCaches(); + } + + return isset( $this->mBinaryColumnCache[$tableRaw] ) + ? $this->mBinaryColumnCache[$tableRaw] + : array(); + } + + /** + * @param string $table + * @return array + */ + private function getBitColumns( $table ) { + $tableRawArr = explode( '.', preg_replace( '#\[([^\]]*)\]#', '$1', $table ) ); + $tableRaw = array_pop( $tableRawArr ); + + if ( $this->mBitColumnCache === null ) { + $this->populateColumnCaches(); + } + + return isset( $this->mBitColumnCache[$tableRaw] ) + ? $this->mBitColumnCache[$tableRaw] + : array(); + } + + private function populateColumnCaches() { + $res = $this->select( 'INFORMATION_SCHEMA.COLUMNS', '*', + array( + 'TABLE_CATALOG' => $this->mDBname, + 'TABLE_SCHEMA' => $this->mSchema, + 'DATA_TYPE' => array( 'varbinary', 'binary', 'image', 'bit' ) + ) ); + + $this->mBinaryColumnCache = array(); + $this->mBitColumnCache = array(); + foreach ( $res as $row ) { + if ( $row->DATA_TYPE == 'bit' ) { + $this->mBitColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; + } else { + $this->mBinaryColumnCache[$row->TABLE_NAME][$row->COLUMN_NAME] = $row; + } + } + } + + /** + * @param string $name + * @param string $format + * @return string + */ + function tableName( $name, $format = 'quoted' ) { + # Replace reserved words with better ones + switch ( $name ) { + case 'user': + return $this->realTableName( 'mwuser', $format ); + default: + return $this->realTableName( $name, $format ); + } + } + + /** + * call this instead of tableName() in the updater when renaming tables + * @param string $name + * @param string $format One of quoted, raw, or split * @return string */ - public function getInfinity() { - return '3000-01-31 00:00:00.000'; + function realTableName( $name, $format = 'quoted' ) { + $table = parent::tableName( $name, $format ); + if ( $format == 'split' ) { + // Used internally, we want the schema split off from the table name and returned + // as a list with 3 elements (database, schema, table) + $table = explode( '.', $table ); + while ( count( $table ) < 3 ) { + array_unshift( $table, false ); + } + } + return $table; } + /** + * Called in the installer and updater. + * Probably doesn't need to be called anywhere else in the codebase. + * @param bool|null $value + * @return bool|null + */ + public function prepareStatements( $value = null ) { + return wfSetVar( $this->mPrepareStatements, $value ); + } + + /** + * Called in the installer and updater. + * Probably doesn't need to be called anywhere else in the codebase. + * @param bool|null $value + * @return bool|null + */ + public function scrollableCursor( $value = null ) { + return wfSetVar( $this->mScrollableCursor, $value ); + } } // end DatabaseMssql class /** @@ -960,10 +1382,11 @@ class DatabaseMssql extends DatabaseBase { * @ingroup Database */ class MssqlField implements Field { - private $name, $tablename, $default, $max_length, $nullable, $type; + private $name, $tableName, $default, $max_length, $nullable, $type; + function __construct( $info ) { $this->name = $info['COLUMN_NAME']; - $this->tablename = $info['TABLE_NAME']; + $this->tableName = $info['TABLE_NAME']; $this->default = $info['COLUMN_DEFAULT']; $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' ); @@ -995,208 +1418,105 @@ class MssqlField implements Field { } } -/** - * The MSSQL PHP driver doesn't support sqlsrv_num_rows, so we recall all rows into an array and maintain our - * own cursor index into that array...This is similar to the way the Oracle driver handles this same issue - * - * @ingroup Database - */ -class MssqlResult { - - public function __construct( $queryresult = false ) { - $this->mCursor = 0; - $this->mRows = array(); - $this->mNumFields = sqlsrv_num_fields( $queryresult ); - $this->mFieldMeta = sqlsrv_field_metadata( $queryresult ); +class MssqlBlob extends Blob { + public function __construct( $data ) { + if ( $data instanceof MssqlBlob ) { + return $data; + } elseif ( $data instanceof Blob ) { + $this->mData = $data->fetch(); + } elseif ( is_array( $data ) && is_object( $data ) ) { + $this->mData = serialize( $data ); + } else { + $this->mData = $data; + } + } - $rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC ); + /** + * Returns an unquoted hex representation of a binary string + * for insertion into varbinary-type fields + * @return string + */ + public function fetch() { + if ( $this->mData === null ) { + return 'null'; + } - foreach ( $rows as $row ) { - if ( $row !== null ) { - foreach ( $row as $k => $v ) { - if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object - $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" ); - } - } - $this->mRows[] = $row;// read results into memory, cursors are not supported - } + $ret = '0x'; + $dataLength = strlen( $this->mData ); + for ( $i = 0; $i < $dataLength; $i++ ) { + $ret .= bin2hex( pack( 'C', ord( $this->mData[$i] ) ) ); } - $this->mRowCount = count( $this->mRows ); - sqlsrv_free_stmt( $queryresult ); + + return $ret; } +} - private function array_to_obj( $array, &$obj ) { - foreach ( $array as $key => $value ) { - if ( is_array( $value ) ) { - $obj->$key = new stdClass(); - $this->array_to_obj( $value, $obj->$key ); - } else { - if ( !empty( $key ) ) { - $obj->$key = $value; - } - } +class MssqlResultWrapper extends ResultWrapper { + private $mSeekTo = null; + + /** + * @return stdClass|bool + */ + public function fetchObject() { + $res = $this->result; + + if ( $this->mSeekTo !== null ) { + $result = sqlsrv_fetch_object( $res, 'stdClass', array(), + SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); + $this->mSeekTo = null; + } else { + $result = sqlsrv_fetch_object( $res ); } - return $obj; - } - public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) { - if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) { + // MediaWiki expects us to return boolean false when there are no more rows instead of null + if ( $result === null ) { return false; } - $arrNum = array(); - if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) { - foreach ( $this->mRows[$this->mCursor] as $value ) { - $arrNum[] = $value; - } - } - switch ( $mode ) { - case SQLSRV_FETCH_ASSOC: - $ret = $this->mRows[$this->mCursor]; - break; - case SQLSRV_FETCH_NUMERIC: - $ret = $arrNum; - break; - case 'OBJECT': - $o = new $object_class; - $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o ); - break; - case SQLSRV_FETCH_BOTH: - default: - $ret = $this->mRows[$this->mCursor] + $arrNum; - break; - } - $this->mCursor++; - return $ret; + return $result; } - public function get( $pos, $fld ) { - return $this->mRows[$pos][$fld]; - } + /** + * @return array|bool + */ + public function fetchRow() { + $res = $this->result; - public function numrows() { - return $this->mRowCount; - } + if ( $this->mSeekTo !== null ) { + $result = sqlsrv_fetch_array( $res, SQLSRV_FETCH_BOTH, + SQLSRV_SCROLL_ABSOLUTE, $this->mSeekTo ); + $this->mSeekTo = null; + } else { + $result = sqlsrv_fetch_array( $res ); + } - public function seek( $iRow ) { - $this->mCursor = min( $iRow, $this->mRowCount ); - } + // MediaWiki expects us to return boolean false when there are no more rows instead of null + if ( $result === null ) { + return false; + } - public function numfields() { - return $this->mNumFields; + return $result; } - public function fieldname( $nr ) { - $arrKeys = array_keys( $this->mRows[0] ); - return $arrKeys[$nr]; - } + /** + * @param int $row + * @return bool + */ + public function seek( $row ) { + $res = $this->result; - public function fieldtype( $nr ) { - $i = 0; - $intType = -1; - foreach ( $this->mFieldMeta as $meta ) { - if ( $nr == $i ) { - $intType = $meta['Type']; - break; - } - $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; + // check bounds + $numRows = $this->db->numRows( $res ); + $row = intval( $row ); + + if ( $numRows === 0 ) { + return false; + } elseif ( $row < 0 || $row > $numRows - 1 ) { + return false; } - return $strType; - } - public function free() { - unset( $this->mRows ); + // Unlike MySQL, the seek actually happens on the next access + $this->mSeekTo = $row; + return true; } } diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php index 956bb694..dc4a67df 100644 --- a/includes/db/DatabaseMysql.php +++ b/includes/db/DatabaseMysql.php @@ -28,10 +28,9 @@ * @see Database */ class DatabaseMysql extends DatabaseMysqlBase { - /** - * @param $sql string - * @return resource + * @param string $sql + * @return resource False on error */ protected function doQuery( $sql ) { if ( $this->bufferResults() ) { @@ -39,14 +38,23 @@ class DatabaseMysql extends DatabaseMysqlBase { } else { $ret = mysql_unbuffered_query( $sql, $this->mConn ); } + return $ret; } + /** + * @param string $realServer + * @return bool|resource MySQL Database connection or false on failure to connect + * @throws DBConnectionError + */ protected function mysqlConnect( $realServer ) { # Fail now # Otherwise we get a suppressed fatal error, which is very hard to track down if ( !extension_loaded( 'mysql' ) ) { - throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" ); + throw new DBConnectionError( + $this, + "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" + ); } $connFlags = 0; @@ -81,6 +89,18 @@ class DatabaseMysql extends DatabaseMysqlBase { } /** + * @param string $charset + * @return bool + */ + protected function mysqlSetCharset( $charset ) { + if ( function_exists( 'mysql_set_charset' ) ) { + return mysql_set_charset( $charset, $this->mConn ); + } else { + return $this->query( 'SET NAMES ' . $charset, __METHOD__ ); + } + } + + /** * @return bool */ protected function closeConnection() { @@ -113,11 +133,12 @@ class DatabaseMysql extends DatabaseMysqlBase { } /** - * @param $db + * @param string $db * @return bool */ function selectDB( $db ) { $this->mDBname = $db; + return mysql_select_db( $db, $this->mConn ); } @@ -156,6 +177,10 @@ class DatabaseMysql extends DatabaseMysqlBase { return mysql_field_name( $res, $n ); } + protected function mysqlFieldType( $res, $n ) { + return mysql_field_type( $res, $n ); + } + protected function mysqlDataSeek( $res, $row ) { return mysql_data_seek( $res, $row ); } diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php index 8f12b92d..ba0f39ff 100644 --- a/includes/db/DatabaseMysqlBase.php +++ b/includes/db/DatabaseMysqlBase.php @@ -33,6 +33,11 @@ abstract class DatabaseMysqlBase extends DatabaseBase { /** @var MysqlMasterPos */ protected $lastKnownSlavePos; + /** @var null|int */ + protected $mFakeSlaveLag = null; + + protected $mFakeMaster = false; + /** * @return string */ @@ -41,15 +46,15 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } /** - * @param $server string - * @param $user string - * @param $password string - * @param $dbName string + * @param string $server + * @param string $user + * @param string $password + * @param string $dbName + * @throws Exception|DBConnectionError * @return bool - * @throws DBConnectionError */ function open( $server, $user, $password, $dbName ) { - global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode; + global $wgAllDBsAreLocalhost, $wgSQLMode; wfProfileIn( __METHOD__ ); # Debugging hack -- fake cluster @@ -76,6 +81,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } catch ( Exception $ex ) { wfProfileOut( "dbconnect-$server" ); wfProfileOut( __METHOD__ ); + $this->restoreErrorHandler(); throw $ex; } $error = $this->restoreErrorHandler(); @@ -87,13 +93,14 @@ abstract class DatabaseMysqlBase extends DatabaseBase { if ( !$error ) { $error = $this->lastError(); } - wfLogDBError( "Error connecting to {$this->mServer}: $error\n" ); + wfLogDBError( "Error connecting to {$this->mServer}: $error" ); wfDebug( "DB connection error\n" . "Server: $server, User: $user, Password: " . substr( $password, 0, 3 ) . "..., error: " . $error . "\n" ); wfProfileOut( __METHOD__ ); - return $this->reportConnectionError( $error ); + + $this->reportConnectionError( $error ); } if ( $dbName != '' ) { @@ -101,44 +108,74 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $success = $this->selectDB( $dbName ); wfRestoreWarnings(); if ( !$success ) { - wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" ); + wfLogDBError( "Error selecting database $dbName on server {$this->mServer}" ); wfDebug( "Error selecting database $dbName on server {$this->mServer} " . "from client host " . wfHostname() . "\n" ); wfProfileOut( __METHOD__ ); - return $this->reportConnectionError( "Error selecting database $dbName" ); + + $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__ ); + // Tell the server what we're communicating with + if ( !$this->connectInitCharset() ) { + $this->reportConnectionError( "Error setting character set" ); } + // 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__ ); + // Use doQuery() to avoid opening implicit transactions (DBO_TRX) + $success = $this->doQuery( "SET sql_mode = $mode", __METHOD__ ); + if ( !$success ) { + wfLogDBError( "Error setting sql_mode to $mode on server {$this->mServer}" ); + wfProfileOut( __METHOD__ ); + $this->reportConnectionError( "Error setting sql_mode to $mode" ); + } } $this->mOpened = true; wfProfileOut( __METHOD__ ); + return true; } /** + * Set the character set information right after connection + * @return bool + */ + protected function connectInitCharset() { + global $wgDBmysql5; + + if ( $wgDBmysql5 ) { + // Tell the server we're communicating with it in UTF-8. + // This may engage various charset conversions. + return $this->mysqlSetCharset( 'utf8' ); + } else { + return $this->mysqlSetCharset( 'binary' ); + } + } + + /** * Open a connection to a MySQL server * - * @param $realServer string + * @param string $realServer * @return mixed Raw connection * @throws DBConnectionError */ abstract protected function mysqlConnect( $realServer ); /** - * @param $res ResultWrapper + * Set the character set of the MySQL link + * + * @param string $charset + * @return bool + */ + abstract protected function mysqlSetCharset( $charset ); + + /** + * @param ResultWrapper|resource $res * @throws DBUnexpectedError */ function freeResult( $res ) { @@ -156,14 +193,14 @@ abstract class DatabaseMysqlBase extends DatabaseBase { /** * Free result memory * - * @param $res Raw result + * @param resource $res Raw result * @return bool */ abstract protected function mysqlFreeResult( $res ); /** - * @param $res ResultWrapper - * @return object|bool + * @param ResultWrapper|resource $res + * @return stdClass|bool * @throws DBUnexpectedError */ function fetchObject( $res ) { @@ -180,21 +217,25 @@ abstract class DatabaseMysqlBase extends DatabaseBase { // 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() ) ); + throw new DBUnexpectedError( + $this, + 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) + ); } + return $row; } /** * Fetch a result row as an object * - * @param $res Raw result + * @param resource $res Raw result * @return stdClass */ abstract protected function mysqlFetchObject( $res ); /** - * @param $res ResultWrapper + * @param ResultWrapper|resource $res * @return array|bool * @throws DBUnexpectedError */ @@ -212,22 +253,26 @@ abstract class DatabaseMysqlBase extends DatabaseBase { // 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() ) ); + 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 + * @param resource $res Raw result * @return array */ abstract protected function mysqlFetchArray( $res ); /** * @throws DBUnexpectedError - * @param $res ResultWrapper + * @param ResultWrapper|resource $res * @return int */ function numRows( $res ) { @@ -237,6 +282,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { 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. @@ -248,68 +294,94 @@ abstract class DatabaseMysqlBase extends DatabaseBase { /** * Get number of rows in result * - * @param $res Raw result + * @param resource $res Raw result * @return int */ abstract protected function mysqlNumRows( $res ); /** - * @param $res ResultWrapper + * @param ResultWrapper|resource $res * @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 + * @param resource $res Raw result * @return int */ abstract protected function mysqlNumFields( $res ); /** - * @param $res ResultWrapper - * @param $n string + * @param ResultWrapper|resource $res + * @param int $n * @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 + * @param ResultWrapper|resource $res + * @param int $n * @return string */ abstract protected function mysqlFieldName( $res, $n ); /** - * @param $res ResultWrapper - * @param $row + * mysql_field_type() wrapper + * @param ResultWrapper|resource $res + * @param int $n + * @return string + */ + public function fieldType( $res, $n ) { + if ( $res instanceof ResultWrapper ) { + $res = $res->result; + } + + return $this->mysqlFieldType( $res, $n ); + } + + /** + * Get the type of the specified field in a result + * + * @param ResultWrapper|resource $res + * @param int $n + * @return string + */ + abstract protected function mysqlFieldType( $res, $n ); + + /** + * @param ResultWrapper|resource $res + * @param int $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 + * @param ResultWrapper|resource $res + * @param int $row * @return bool */ abstract protected function mysqlDataSeek( $res, $row ); @@ -332,22 +404,23 @@ abstract class DatabaseMysqlBase extends DatabaseBase { if ( $error ) { $error .= ' (' . $this->mServer . ')'; } + return $error; } /** * Returns the text of the error message from previous MySQL operation * - * @param $conn Raw connection + * @param resource $conn Raw connection * @return string */ abstract protected function mysqlError( $conn = null ); /** - * @param $table string - * @param $uniqueIndexes - * @param $rows array - * @param $fname string + * @param string $table + * @param array $uniqueIndexes + * @param array $rows + * @param string $fname * @return ResultWrapper */ function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { @@ -359,14 +432,16 @@ abstract class DatabaseMysqlBase extends DatabaseBase { * 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 + * @param string|array $table + * @param string|array $vars + * @param string|array $conds + * @param string $fname + * @param string|array $options + * @return bool|int */ - public function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) { + public function estimateRowCount( $table, $vars = '*', $conds = '', + $fname = __METHOD__, $options = array() + ) { $options['EXPLAIN'] = true; $res = $this->select( $table, $vars, $conds, $fname, $options ); if ( $res === false ) { @@ -380,12 +455,13 @@ abstract class DatabaseMysqlBase extends DatabaseBase { foreach ( $res as $plan ) { $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero } + return $rows; } /** - * @param $table string - * @param $field string + * @param string $table + * @param string $field * @return bool|MySQLField */ function fieldInfo( $table, $field ) { @@ -401,14 +477,15 @@ abstract class DatabaseMysqlBase extends DatabaseBase { return new MySQLField( $meta ); } } + return false; } /** * Get column information from a result * - * @param $res Raw result - * @param $n int + * @param resource $res Raw result + * @param int $n * @return stdClass */ abstract protected function mysqlFetchField( $res, $n ); @@ -417,9 +494,9 @@ abstract class DatabaseMysqlBase extends DatabaseBase { * 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 + * @param string $table + * @param string $index + * @param string $fname * @return bool|array|null False or null on failure */ function indexInfo( $table, $index, $fname = __METHOD__ ) { @@ -443,12 +520,12 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $result[] = $row; } } + return empty( $result ) ? false : $result; } /** - * @param $s string - * + * @param string $s * @return string */ function strencode( $s ) { @@ -458,24 +535,24 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $this->ping(); $sQuoted = $this->mysqlRealEscapeString( $s ); } + return $sQuoted; } /** * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes". * - * @param $s string - * + * @param string $s * @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 ) . '`'; + return '`' . str_replace( array( "\0", '`' ), array( '', '``' ), $s ) . '`'; } /** - * @param $name string + * @param string $name * @return bool */ public function isQuotedIdentifier( $name ) { @@ -495,6 +572,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $this->mOpened = false; $this->mConn = false; $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname ); + return true; } @@ -506,6 +584,24 @@ abstract class DatabaseMysqlBase extends DatabaseBase { abstract protected function mysqlPing(); /** + * Set lag time in seconds for a fake slave + * + * @param int $lag + */ + public function setFakeSlaveLag( $lag ) { + $this->mFakeSlaveLag = $lag; + } + + /** + * Make this connection a fake master + * + * @param bool $enabled + */ + public function setFakeMaster( $enabled = true ) { + $this->mFakeMaster = $enabled; + } + + /** * Returns slave lag. * * This will do a SHOW SLAVE STATUS @@ -515,6 +611,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { function getLag() { if ( !is_null( $this->mFakeSlaveLag ) ) { wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" ); + return $this->mFakeSlaveLag; } @@ -541,52 +638,14 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } /** - * @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 + * @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 + * @param DBMasterPos|MySQLMasterPos $pos + * @param int $timeout The maximum number of seconds to wait for synchronisation + * @return int Zero if the slave was past that position already, + * greater than zero if we waited for some period of time, less than + * zero if we timed out. */ function masterPosWait( DBMasterPos $pos, $timeout ) { if ( $this->lastKnownSlavePos && $this->lastKnownSlavePos->hasReached( $pos ) ) { @@ -598,9 +657,25 @@ abstract class DatabaseMysqlBase extends DatabaseBase { $this->commit( __METHOD__, 'flush' ); if ( !is_null( $this->mFakeSlaveLag ) ) { - $status = parent::masterPosWait( $pos, $timeout ); - wfProfileOut( __METHOD__ ); - return $status; + $wait = intval( ( $pos->pos - microtime( true ) + $this->mFakeSlaveLag ) * 1e6 ); + + if ( $wait > $timeout * 1e6 ) { + wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" ); + wfProfileOut( __METHOD__ ); + + return -1; + } elseif ( $wait > 0 ) { + wfDebug( "Fake slave waiting $wait us\n" ); + usleep( $wait ); + wfProfileOut( __METHOD__ ); + + return 1; + } else { + wfDebug( "Fake slave up to date ($wait us)\n" ); + wfProfileOut( __METHOD__ ); + + return 0; + } } # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set @@ -618,6 +693,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } wfProfileOut( __METHOD__ ); + return $status; } @@ -628,14 +704,20 @@ abstract class DatabaseMysqlBase extends DatabaseBase { */ function getSlavePos() { if ( !is_null( $this->mFakeSlaveLag ) ) { - return parent::getSlavePos(); + $pos = new MySQLMasterPos( 'fake', microtime( true ) - $this->mFakeSlaveLag ); + wfDebug( __METHOD__ . ": fake slave pos = $pos\n" ); + + return $pos; } $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; + $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; @@ -649,7 +731,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { */ function getMasterPos() { if ( $this->mFakeMaster ) { - return parent::getMasterPos(); + return new MySQLMasterPos( 'fake', microtime( true ) ); } $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' ); @@ -663,7 +745,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } /** - * @param $index + * @param string $index * @return string */ function useIndexClause( $index ) { @@ -681,18 +763,22 @@ abstract class DatabaseMysqlBase extends DatabaseBase { * @return string */ public function getSoftwareLink() { + // MariaDB includes its name in its version string (sent when the connection is opened), + // and this is how MariaDB's version of the mysql command-line client identifies MariaDB + // servers (see the mariadb_connection() function in libmysql/libmysql.c). $version = $this->getServerVersion(); - if ( strpos( $version, 'MariaDB' ) !== false ) { + if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) { return '[{{int:version-db-mariadb-url}} MariaDB]'; - } elseif ( strpos( $version, 'percona' ) !== false ) { - return '[{{int:version-db-percona-url}} Percona Server]'; - } else { - return '[{{int:version-db-mysql-url}} MySQL]'; } + + // Percona Server's version suffix is not very distinctive, and @@version_comment + // doesn't give the necessary info for source builds, so assume the server is MySQL. + // (Even Percona's version of mysql doesn't try to make the distinction.) + return '[{{int:version-db-mysql-url}} MySQL]'; } /** - * @param $options array + * @param array $options */ public function setSessionOptions( array $options ) { if ( isset( $options['connTimeout'] ) ) { @@ -702,34 +788,41 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } } + /** + * @param string $sql + * @param string $newLine + * @return bool + */ 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 + * @param string $lockName Name of lock to poll + * @param string $method Name of method calling us + * @return bool * @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 + * @param string $lockName + * @param string $method + * @param int $timeout * @return bool */ public function lock( $lockName, $method, $timeout = 5 ) { @@ -741,28 +834,31 @@ abstract class DatabaseMysqlBase extends DatabaseBase { 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 + * FROM MYSQL DOCS: + * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock + * @param string $lockName + * @param string $method * @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 + * @param array $read + * @param array $write + * @param string $method + * @param bool $lowPriority * @return bool */ public function lockTables( $read, $write, $method, $lowPriority = true ) { @@ -770,8 +866,8 @@ abstract class DatabaseMysqlBase extends DatabaseBase { foreach ( $write as $table ) { $tbl = $this->tableName( $table ) . - ( $lowPriority ? ' LOW_PRIORITY' : '' ) . - ' WRITE'; + ( $lowPriority ? ' LOW_PRIORITY' : '' ) . + ' WRITE'; $items[] = $tbl; } foreach ( $read as $table ) { @@ -779,15 +875,17 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } $sql = "LOCK TABLES " . implode( ',', $items ); $this->query( $sql, $method ); + return true; } /** - * @param $method string + * @param string $method * @return bool */ public function unlockTables( $method ) { $this->query( "UNLOCK TABLES", $method ); + return true; } @@ -795,7 +893,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { * Get search engine class. All subclasses of this * need to implement this if they wish to use searching. * - * @return String + * @return string */ public function getSearchEngine() { return 'SearchMySQL'; @@ -803,7 +901,6 @@ abstract class DatabaseMysqlBase extends DatabaseBase { /** * @param bool $value - * @return mixed */ public function setBigSelects( $value = true ) { if ( $value === 'default' ) { @@ -822,12 +919,12 @@ abstract class DatabaseMysqlBase extends DatabaseBase { /** * 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 + * @param string $delTable + * @param string $joinTable + * @param string $delVar + * @param string $joinVar + * @param array|string $conds + * @param bool|string $fname * @throws DBUnexpectedError * @return bool|ResultWrapper */ @@ -853,16 +950,18 @@ abstract class DatabaseMysqlBase extends DatabaseBase { * @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__ + 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 ( !is_array( reset( $rows ) ) ) { + $rows = array( $rows ); + } $table = $this->tableName( $table ); $columns = array_keys( $rows[0] ); @@ -885,6 +984,7 @@ abstract class DatabaseMysqlBase extends DatabaseBase { */ function getServerUptime() { $vars = $this->getMysqlStatus( 'Uptime' ); + return (int)$vars['Uptime']; } @@ -927,24 +1027,26 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } /** - * @param $oldName - * @param $newName - * @param $temporary bool - * @param $fname string + * @param string $oldName + * @param string $newName + * @param bool $temporary + * @param string $fname + * @return bool */ 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 ); + + return $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 + * @param string $fname Calling function name * @return array */ function listTables( $prefix = null, $fname = __METHOD__ ) { @@ -965,14 +1067,15 @@ abstract class DatabaseMysqlBase extends DatabaseBase { } /** - * @param $tableName - * @param $fName string + * @param string $tableName + * @param string $fName * @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 ); } @@ -982,14 +1085,19 @@ abstract class DatabaseMysqlBase extends DatabaseBase { protected function getDefaultSchemaVars() { $vars = parent::getDefaultSchemaVars(); $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] ); - $vars['wgDBTableOptions'] = str_replace( 'CHARSET=mysql4', 'CHARSET=binary', $vars['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 + * @param string $which * @return array */ function getMysqlStatus( $which = "%" ) { @@ -1006,9 +1114,9 @@ abstract class DatabaseMysqlBase extends DatabaseBase { /** * Lists VIEWs in the database * - * @param string $prefix Only show VIEWs with this prefix, eg. + * @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 + * @param string $fname Name of calling function * @return array * @since 1.22 */ @@ -1022,12 +1130,12 @@ abstract class DatabaseMysqlBase extends DatabaseBase { // Query for the VIEWS $result = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' ); $this->allViews = array(); - while ( ($row = $this->fetchRow($result)) !== false ) { + while ( ( $row = $this->fetchRow( $result ) ) !== false ) { array_push( $this->allViews, $row[$propertyName] ); } } - if ( is_null($prefix) || $prefix === '' ) { + if ( is_null( $prefix ) || $prefix === '' ) { return $this->allViews; } @@ -1038,24 +1146,23 @@ abstract class DatabaseMysqlBase extends DatabaseBase { array_push( $filteredViews, $viewName ); } } + return $filteredViews; } /** * Differentiates between a TABLE and a VIEW. * - * @param $name string: Name of the TABLE/VIEW to test + * @param string $name Name of the TABLE/VIEW to test + * @param string $prefix * @return bool * @since 1.22 */ public function isView( $name, $prefix = null ) { return in_array( $name, $this->listViews( $prefix ) ); } - } - - /** * Utility class. * @ingroup Database @@ -1130,7 +1237,11 @@ class MySQLField implements Field { } class MySQLMasterPos implements DBMasterPos { - var $file, $pos; + /** @var string */ + public $file; + + /** @var int Timestamp */ + public $pos; function __construct( $file, $pos ) { $this->file = $file; @@ -1143,19 +1254,21 @@ class MySQLMasterPos implements DBMasterPos { } /** - * @return array|false (int, int) + * @return array|bool (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 index 0ec54314..a03c9aaf 100644 --- a/includes/db/DatabaseMysqli.php +++ b/includes/db/DatabaseMysqli.php @@ -29,9 +29,8 @@ * @see Database */ class DatabaseMysqli extends DatabaseMysqlBase { - /** - * @param $sql string + * @param string $sql * @return resource */ protected function doQuery( $sql ) { @@ -40,10 +39,17 @@ class DatabaseMysqli extends DatabaseMysqlBase { } else { $ret = $this->mConn->query( $sql, MYSQLI_USE_RESULT ); } + return $ret; } + /** + * @param string $realServer + * @return bool|mysqli + * @throws DBConnectionError + */ protected function mysqlConnect( $realServer ) { + global $wgDBmysql5; # Fail now # Otherwise we get a suppressed fatal error, which is very hard to track down if ( !function_exists( 'mysqli_init' ) ) { @@ -52,14 +58,22 @@ class DatabaseMysqli extends DatabaseMysqlBase { } // Other than mysql_connect, mysqli_real_connect expects an explicit port - // parameter. So we need to parse the port out of $realServer + // and socket parameters. So we need to parse the port and socket out of + // $realServer $port = null; + $socket = null; $hostAndPort = IP::splitHostAndPort( $realServer ); if ( $hostAndPort ) { $realServer = $hostAndPort[0]; if ( $hostAndPort[1] ) { $port = $hostAndPort[1]; } + } elseif ( substr_count( $realServer, ':' ) == 1 ) { + // If we have a colon and something that's not a port number + // inside the hostname, assume it's the socket location + $hostAndSocket = explode( ':', $realServer ); + $realServer = $hostAndSocket[0]; + $socket = $hostAndSocket[1]; } $connFlags = 0; @@ -74,22 +88,41 @@ class DatabaseMysqli extends DatabaseMysqlBase { } $mysqli = mysqli_init(); - $numAttempts = 2; + if ( $wgDBmysql5 ) { + // Tell the server we're communicating with it in UTF-8. + // This may engage various charset conversions. + $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'utf8' ); + } else { + $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'binary' ); + } + $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, 3 ); - for ( $i = 0; $i < $numAttempts; $i++ ) { - if ( $i > 1 ) { - usleep( 1000 ); - } - if ( $mysqli->real_connect( $realServer, $this->mUser, - $this->mPassword, $this->mDBname, $port, null, $connFlags ) ) - { - return $mysqli; - } + if ( $mysqli->real_connect( $realServer, $this->mUser, + $this->mPassword, $this->mDBname, $port, $socket, $connFlags ) + ) { + return $mysqli; } return false; } + protected function connectInitCharset() { + // already done in mysqlConnect() + return true; + } + + /** + * @param string $charset + * @return bool + */ + protected function mysqlSetCharset( $charset ) { + if ( method_exists( $this->mConn, 'set_charset' ) ) { + return $this->mConn->set_charset( $charset ); + } else { + return $this->query( 'SET NAMES ' . $charset, __METHOD__ ); + } + } + /** * @return bool */ @@ -123,11 +156,12 @@ class DatabaseMysqli extends DatabaseMysqlBase { } /** - * @param $db + * @param string $db * @return bool */ function selectDB( $db ) { $this->mDBname = $db; + return $this->mConn->select_db( $db ); } @@ -138,35 +172,63 @@ class DatabaseMysqli extends DatabaseMysqlBase { return $this->mConn->server_info; } + /** + * @param mysqli $res + * @return bool + */ protected function mysqlFreeResult( $res ) { $res->free_result(); + return true; } + /** + * @param mysqli $res + * @return bool + */ protected function mysqlFetchObject( $res ) { $object = $res->fetch_object(); if ( $object === null ) { return false; } + return $object; } + /** + * @param mysqli $res + * @return bool + */ protected function mysqlFetchArray( $res ) { $array = $res->fetch_array(); if ( $array === null ) { return false; } + return $array; } + /** + * @param mysqli $res + * @return mixed + */ protected function mysqlNumRows( $res ) { return $res->num_rows; } + /** + * @param mysqli $res + * @return mixed + */ protected function mysqlNumFields( $res ) { return $res->field_count; } + /** + * @param mysqli $res + * @param int $n + * @return mixed + */ protected function mysqlFetchField( $res, $n ) { $field = $res->fetch_field_direct( $n ); $field->not_null = $field->flags & MYSQLI_NOT_NULL_FLAG; @@ -174,26 +236,58 @@ class DatabaseMysqli extends DatabaseMysqlBase { $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; } + /** + * @param resource|ResultWrapper $res + * @param int $n + * @return mixed + */ protected function mysqlFieldName( $res, $n ) { $field = $res->fetch_field_direct( $n ); + return $field->name; } + /** + * @param resource|ResultWrapper $res + * @param int $n + * @return mixed + */ + protected function mysqlFieldType( $res, $n ) { + $field = $res->fetch_field_direct( $n ); + + return $field->type; + } + + /** + * @param resource|ResultWrapper $res + * @param int $row + * @return mixed + */ protected function mysqlDataSeek( $res, $row ) { return $res->data_seek( $row ); } + /** + * @param mysqli $conn Optional connection object + * @return string + */ protected function mysqlError( $conn = null ) { - if ($conn === null) { + if ( $conn === null ) { return mysqli_connect_error(); } else { return $conn->error; } } + /** + * Escapes special characters in a string for use in an SQL statement + * @param string $s + * @return string + */ protected function mysqlRealEscapeString( $s ) { return $this->mConn->real_escape_string( $s ); } @@ -202,4 +296,18 @@ class DatabaseMysqli extends DatabaseMysqlBase { return $this->mConn->ping(); } + /** + * Give an id for the connection + * + * mysql driver used resource id, but mysqli objects cannot be cast to string. + * @return string + */ + public function __toString() { + if ( $this->mConn instanceof Mysqli ) { + return (string)$this->mConn->thread_id; + } else { + // mConn might be false or something. + return (string)$this->mConn; + } + } } diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 9009b328..f031f780 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -50,17 +50,19 @@ class ORAResult { } /** - * @param $db DatabaseBase - * @param $stmt + * @param DatabaseBase $db + * @param resource $stmt A valid OCI statement identifier * @param bool $unique */ function __construct( &$db, $stmt, $unique = false ) { $this->db =& $db; - if ( ( $this->nrows = oci_fetch_all( $stmt, $this->rows, 0, - 1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM ) ) === false ) { + $this->nrows = oci_fetch_all( $stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM ); + if ( $this->nrows === false ) { $e = oci_error( $stmt ); $db->reportQueryError( $e['message'], $e['code'], '', __METHOD__ ); $this->free(); + return; } @@ -121,6 +123,7 @@ class ORAResult { $ret[$lc] = $v; $ret[$k] = $v; } + return $ret; } } @@ -183,25 +186,49 @@ class ORAField implements Field { * @ingroup Database */ class DatabaseOracle extends DatabaseBase { - var $mInsertId = null; - var $mLastResult = null; - var $lastResult = null; - var $cursor = 0; - var $mAffectedRows; + /** @var resource */ + protected $mLastResult = null; + + /** @var int The number of rows affected as an integer */ + protected $mAffectedRows; - var $ignore_DUP_VAL_ON_INDEX = false; - var $sequenceData = null; + /** @var int */ + private $mInsertId = null; - var $defaultCharset = 'AL32UTF8'; + /** @var bool */ + private $ignoreDupValOnIndex = false; - var $mFieldInfoCache = array(); + /** @var bool|array */ + private $sequenceData = null; - function __construct( $server = false, $user = false, $password = false, $dbName = false, - $flags = 0, $tablePrefix = 'get from global' ) - { + /** @var string Character set for Oracle database */ + private $defaultCharset = 'AL32UTF8'; + + /** @var array */ + private $mFieldInfoCache = array(); + + function __construct( $p = null ) { global $wgDBprefix; - $tablePrefix = $tablePrefix == 'get from global' ? strtoupper( $wgDBprefix ) : strtoupper( $tablePrefix ); - parent::__construct( $server, $user, $password, $dbName, $flags, $tablePrefix ); + + if ( !is_array( $p ) ) { // legacy calling pattern + wfDeprecated( __METHOD__ . " method called without parameter array.", "1.22" ); + $args = func_get_args(); + $p = array( + 'host' => isset( $args[0] ) ? $args[0] : false, + 'user' => isset( $args[1] ) ? $args[1] : false, + 'password' => isset( $args[2] ) ? $args[2] : false, + 'dbname' => isset( $args[3] ) ? $args[3] : false, + 'flags' => isset( $args[4] ) ? $args[4] : 0, + 'tablePrefix' => isset( $args[5] ) ? $args[5] : 'get from global', + 'schema' => 'get from global', + 'foreign' => isset( $args[6] ) ? $args[6] : false + ); + } + if ( $p['tablePrefix'] == 'get from global' ) { + $p['tablePrefix'] = $wgDBprefix; + } + $p['tablePrefix'] = strtoupper( $p['tablePrefix'] ); + parent::__construct( $p ); wfRunHooks( 'DatabaseOraclePostInit', array( $this ) ); } @@ -220,21 +247,27 @@ class DatabaseOracle 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 searchableIPs() { return true; } @@ -251,7 +284,11 @@ class DatabaseOracle extends DatabaseBase { 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" ); + 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\n " . + "and database)\n" ); } $this->close(); @@ -274,7 +311,7 @@ class DatabaseOracle extends DatabaseBase { } if ( !strlen( $user ) ) { # e.g. the class is being loaded - return; + return null; } if ( $wgDBOracleDRCP ) { @@ -285,11 +322,29 @@ class DatabaseOracle extends DatabaseBase { wfSuppressWarnings(); if ( $this->mFlags & DBO_PERSISTENT ) { - $this->mConn = oci_pconnect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode ); + $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 ); + $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 ); + $this->mConn = oci_connect( + $this->mUser, + $this->mPassword, + $this->mServer, + $this->defaultCharset, + $session_mode + ); } wfRestoreWarnings(); @@ -308,6 +363,7 @@ class DatabaseOracle extends DatabaseBase { $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' ); $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' ); $this->doQuery( 'ALTER SESSION SET NLS_NUMERIC_CHARACTERS=\'.,\'' ); + return $this->mConn; } @@ -343,20 +399,28 @@ class DatabaseOracle extends DatabaseBase { // you have to select data from plan table after explain $explain_id = MWTimestamp::getLocalInstance()->format( 'dmYHis' ); - $sql = preg_replace( '/^EXPLAIN /', 'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR', $sql, 1, $explain_count ); + $sql = preg_replace( + '/^EXPLAIN /', + 'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR', + $sql, + 1, + $explain_count + ); wfSuppressWarnings(); if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) { $e = oci_error( $this->mConn ); $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; } if ( !oci_execute( $stmt, $this->execFlags() ) ) { $e = oci_error( $stmt ); - if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) { + if ( !$this->ignoreDupValOnIndex || $e['code'] != '1' ) { $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; } } @@ -364,11 +428,13 @@ class DatabaseOracle extends DatabaseBase { wfRestoreWarnings(); if ( $explain_count > 0 ) { - return $this->doQuery( 'SELECT id, cardinality "ROWS" FROM plan_table WHERE statement_id = \'' . $explain_id . '\'' ); + return $this->doQuery( 'SELECT id, cardinality "ROWS" FROM plan_table ' . + 'WHERE statement_id = \'' . $explain_id . '\'' ); } elseif ( oci_statement_type( $stmt ) == 'SELECT' ) { return new ORAResult( $this, $stmt, $union_unique ); } else { $this->mAffectedRows = oci_num_rows( $stmt ); + return true; } } @@ -377,6 +443,10 @@ class DatabaseOracle extends DatabaseBase { return $this->query( $sql, $fname, true ); } + /** + * Frees resources associated with the LOB descriptor + * @param ResultWrapper|resource $res + */ function freeResult( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; @@ -385,6 +455,10 @@ class DatabaseOracle extends DatabaseBase { $res->free(); } + /** + * @param ResultWrapper|stdClass $res + * @return mixed + */ function fetchObject( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; @@ -423,12 +497,16 @@ class DatabaseOracle extends DatabaseBase { /** * This must be called after nextSequenceVal - * @return null + * @return null|int */ function insertId() { return $this->mInsertId; } + /** + * @param mixed $res + * @param int $row + */ function dataSeek( $res, $row ) { if ( $res instanceof ORAResult ) { $res->seek( $row ); @@ -443,6 +521,7 @@ class DatabaseOracle extends DatabaseBase { } else { $e = oci_error( $this->mConn ); } + return $e['message']; } @@ -452,6 +531,7 @@ class DatabaseOracle extends DatabaseBase { } else { $e = oci_error( $this->mConn ); } + return $e['code']; } @@ -462,6 +542,9 @@ class DatabaseOracle extends DatabaseBase { /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure + * @param string $table + * @param string $index + * @param string $fname * @return bool */ function indexInfo( $table, $index, $fname = __METHOD__ ) { @@ -482,7 +565,7 @@ class DatabaseOracle extends DatabaseBase { } if ( in_array( 'IGNORE', $options ) ) { - $this->ignore_DUP_VAL_ON_INDEX = true; + $this->ignoreDupValOnIndex = true; } if ( !is_array( reset( $a ) ) ) { @@ -495,7 +578,7 @@ class DatabaseOracle extends DatabaseBase { $retVal = true; if ( in_array( 'IGNORE', $options ) ) { - $this->ignore_DUP_VAL_ON_INDEX = false; + $this->ignoreDupValOnIndex = false; } return $retVal; @@ -509,6 +592,7 @@ class DatabaseOracle extends DatabaseBase { if ( is_numeric( $col ) ) { $bind = $val; $val = null; + return $bind; } elseif ( $includeCol ) { $bind = "$col = "; @@ -535,6 +619,13 @@ class DatabaseOracle extends DatabaseBase { return $bind; } + /** + * @param string $table + * @param array $row + * @param string $fname + * @return bool + * @throws DBUnexpectedError + */ private function insertOneRow( $table, $row, $fname ) { global $wgContLang; @@ -563,6 +654,7 @@ class DatabaseOracle extends DatabaseBase { if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) { $e = oci_error( $this->mConn ); $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; } foreach ( $row as $col => &$val ) { @@ -585,9 +677,11 @@ class DatabaseOracle extends DatabaseBase { if ( oci_bind_by_name( $stmt, ":$col", $val, -1, SQLT_CHR ) === false ) { $e = oci_error( $stmt ); $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; } } else { + /** @var OCI_Lob[] $lob */ if ( ( $lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB ) ) === false ) { $e = oci_error( $stmt ); throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] ); @@ -599,10 +693,10 @@ class DatabaseOracle extends DatabaseBase { if ( $col_type == 'BLOB' ) { $lob[$col]->writeTemporary( $val, OCI_TEMP_BLOB ); - oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, OCI_B_BLOB ); + oci_bind_by_name( $stmt, ":$col", $lob[$col], -1, OCI_B_BLOB ); } else { $lob[$col]->writeTemporary( $val, OCI_TEMP_CLOB ); - oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, OCI_B_CLOB ); + oci_bind_by_name( $stmt, ":$col", $lob[$col], -1, OCI_B_CLOB ); } } } @@ -611,8 +705,9 @@ class DatabaseOracle extends DatabaseBase { if ( oci_execute( $stmt, $this->execFlags() ) === false ) { $e = oci_error( $stmt ); - if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) { + if ( !$this->ignoreDupValOnIndex || $e['code'] != '1' ) { $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; } else { $this->mAffectedRows = oci_num_rows( $stmt ); @@ -633,12 +728,12 @@ class DatabaseOracle extends DatabaseBase { oci_commit( $this->mConn ); } - oci_free_statement( $stmt ); + return oci_free_statement( $stmt ); } function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = array(), $selectOptions = array() ) - { + $insertOptions = array(), $selectOptions = array() + ) { $destTable = $this->tableName( $destTable ); if ( !is_array( $selectOptions ) ) { $selectOptions = array( $selectOptions ); @@ -651,8 +746,8 @@ class DatabaseOracle extends DatabaseBase { } if ( ( $sequenceData = $this->getSequenceData( $destTable ) ) !== false && - !isset( $varMap[$sequenceData['column']] ) ) - { + !isset( $varMap[$sequenceData['column']] ) + ) { $varMap[$sequenceData['column']] = 'GET_SEQUENCE_VALUE(\'' . $sequenceData['sequence'] . '\')'; } @@ -671,13 +766,13 @@ class DatabaseOracle extends DatabaseBase { $sql .= " $tailOpts"; if ( in_array( 'IGNORE', $insertOptions ) ) { - $this->ignore_DUP_VAL_ON_INDEX = true; + $this->ignoreDupValOnIndex = true; } $retval = $this->query( $sql, $fname ); if ( in_array( 'IGNORE', $insertOptions ) ) { - $this->ignore_DUP_VAL_ON_INDEX = false; + $this->ignoreDupValOnIndex = false; } return $retval; @@ -699,7 +794,9 @@ class DatabaseOracle extends DatabaseBase { // add sequence column to each list of columns, when not set foreach ( $rows as &$row ) { if ( !isset( $row[$sequenceData['column']] ) ) { - $row[$sequenceData['column']] = $this->addIdentifierQuotes('GET_SEQUENCE_VALUE(\'' . $sequenceData['sequence'] . '\')'); + $row[$sequenceData['column']] = + $this->addIdentifierQuotes( 'GET_SEQUENCE_VALUE(\'' . + $sequenceData['sequence'] . '\')' ); } } } @@ -727,33 +824,45 @@ class DatabaseOracle extends DatabaseBase { function tableNameInternal( $name ) { $name = $this->tableName( $name ); + return preg_replace( '/.*\.(.*)/', '$1', $name ); } + /** * Return the next in a sequence, save the value for retrieval via insertId() - * @return null + * + * @param string $seqName + * @return null|int */ function nextSequenceValue( $seqName ) { $res = $this->query( "SELECT $seqName.nextval FROM dual" ); $row = $this->fetchRow( $res ); $this->mInsertId = $row[0]; + return $this->mInsertId; } /** * Return sequence_name if table has a sequence + * + * @param string $table * @return bool */ private function getSequenceData( $table ) { if ( $this->sequenceData == null ) { $result = $this->doQuery( "SELECT lower(asq.sequence_name), - lower(atc.table_name), - lower(atc.column_name) - FROM all_sequences asq, all_tab_columns atc - WHERE decode(atc.table_name, '{$this->mTablePrefix}MWUSER', '{$this->mTablePrefix}USER', atc.table_name) || '_' || - atc.column_name || '_SEQ' = '{$this->mTablePrefix}' || asq.sequence_name - AND asq.sequence_owner = upper('{$this->mDBname}') - AND atc.owner = upper('{$this->mDBname}')" ); + lower(atc.table_name), + lower(atc.column_name) + FROM all_sequences asq, all_tab_columns atc + WHERE decode( + atc.table_name, + '{$this->mTablePrefix}MWUSER', + '{$this->mTablePrefix}USER', + atc.table_name + ) || '_' || + atc.column_name || '_SEQ' = '{$this->mTablePrefix}' || asq.sequence_name + AND asq.sequence_owner = upper('{$this->mDBname}') + AND atc.owner = upper('{$this->mDBname}')" ); while ( ( $row = $result->fetchRow() ) !== false ) { $this->sequenceData[$row[1]] = array( @@ -763,12 +872,20 @@ class DatabaseOracle extends DatabaseBase { } } $table = strtolower( $this->removeIdentifierQuotes( $this->tableName( $table ) ) ); + return ( isset( $this->sequenceData[$table] ) ) ? $this->sequenceData[$table] : false; } - # Returns the size of a text field, or -1 for "unlimited" + /** + * Returns the size of a text field, or -1 for "unlimited" + * + * @param string $table + * @param string $field + * @return mixed + */ function textFieldSize( $table, $field ) { $fieldInfoData = $this->fieldInfo( $table, $field ); + return $fieldInfoData->maxLength(); } @@ -776,6 +893,7 @@ class DatabaseOracle extends DatabaseBase { if ( $offset === false ) { $offset = 0; } + return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < (1 + $limit + $offset)"; } @@ -787,19 +905,24 @@ class DatabaseOracle extends DatabaseBase { if ( $b instanceof Blob ) { $b = $b->fetch(); } + return $b; } 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 = __METHOD__ ) { + function duplicateTableStructure( $oldName, $newName, $temporary = false, + $fname = __METHOD__ + ) { $temporary = $temporary ? 'TRUE' : 'FALSE'; $newName = strtoupper( $newName ); @@ -809,7 +932,8 @@ class DatabaseOracle extends DatabaseBase { $oldPrefix = substr( $oldName, 0, strlen( $oldName ) - strlen( $tabName ) ); $newPrefix = strtoupper( $this->mTablePrefix ); - return $this->doQuery( "BEGIN DUPLICATE_TABLE( '$tabName', '$oldPrefix', '$newPrefix', $temporary ); END;" ); + return $this->doQuery( "BEGIN DUPLICATE_TABLE( '$tabName', " . + "'$oldPrefix', '$newPrefix', $temporary ); END;" ); } function listTables( $prefix = null, $fname = __METHOD__ ) { @@ -819,7 +943,8 @@ class DatabaseOracle extends DatabaseBase { } $owner = strtoupper( $this->mDBname ); - $result = $this->doQuery( "SELECT table_name FROM all_tables WHERE owner='$owner' AND table_name NOT LIKE '%!_IDX\$_' ESCAPE '!' $listWhere" ); + $result = $this->doQuery( "SELECT table_name FROM all_tables " . + "WHERE owner='$owner' AND table_name NOT LIKE '%!_IDX\$_' ESCAPE '!' $listWhere" ); // dirty code ... i know $endArray = array(); @@ -851,6 +976,10 @@ class DatabaseOracle extends DatabaseBase { /** * Return aggregated value function call + * + * @param array $valuedata + * @param string $valuename + * @return mixed */ public function aggregateValue( $valuedata, $valuename = 'value' ) { return $valuedata; @@ -871,7 +1000,7 @@ class DatabaseOracle extends DatabaseBase { } /** - * @return string wikitext of a link to the server software's web site + * @return string Wikitext of a link to the server software's web site */ public function getSoftwareLink() { return '[{{int:version-db-oracle-url}} Oracle]'; @@ -882,15 +1011,22 @@ class DatabaseOracle extends DatabaseBase { */ function getServerVersion() { //better version number, fallback on driver - $rset = $this->doQuery( 'SELECT version FROM product_component_version WHERE UPPER(product) LIKE \'ORACLE DATABASE%\'' ); + $rset = $this->doQuery( + 'SELECT version FROM product_component_version ' . + 'WHERE UPPER(product) LIKE \'ORACLE DATABASE%\'' + ); if ( !( $row = $rset->fetchRow() ) ) { return oci_server_version( $this->mConn ); } + return $row['version']; } /** * Query whether a given index exists + * @param string $table + * @param string $index + * @param string $fname * @return bool */ function indexExists( $table, $index, $fname = __METHOD__ ) { @@ -898,27 +1034,30 @@ class DatabaseOracle extends DatabaseBase { $table = strtoupper( $this->removeIdentifierQuotes( $table ) ); $index = strtoupper( $index ); $owner = strtoupper( $this->mDBname ); - $SQL = "SELECT 1 FROM all_indexes WHERE owner='$owner' AND index_name='{$table}_{$index}'"; - $res = $this->doQuery( $SQL ); + $sql = "SELECT 1 FROM all_indexes WHERE owner='$owner' AND index_name='{$table}_{$index}'"; + $res = $this->doQuery( $sql ); if ( $res ) { $count = $res->numRows(); $res->free(); } else { $count = 0; } + return $count != 0; } /** * Query whether a given table exists (in the given schema, or the default mw one if not given) + * @param string $table + * @param string $fname * @return bool */ function tableExists( $table, $fname = __METHOD__ ) { $table = $this->tableName( $table ); $table = $this->addQuotes( strtoupper( $this->removeIdentifierQuotes( $table ) ) ); $owner = $this->addQuotes( strtoupper( $this->mDBname ) ); - $SQL = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table"; - $res = $this->doQuery( $SQL ); + $sql = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table"; + $res = $this->doQuery( $sql ); if ( $res && $res->numRows() > 0 ) { $exists = true; } else { @@ -926,6 +1065,7 @@ class DatabaseOracle extends DatabaseBase { } $res->free(); + return $exists; } @@ -935,8 +1075,8 @@ class DatabaseOracle extends DatabaseBase { * For internal calls. Use fieldInfo for normal usage. * Returns false if the field doesn't exist * - * @param $table Array - * @param $field String + * @param array|string $table + * @param string $field * @return ORAField|ORAResult */ private function fieldInfoMulti( $table, $field ) { @@ -960,10 +1100,15 @@ class DatabaseOracle extends DatabaseBase { $tableWhere = '= \'' . $table . '\''; } - $fieldInfoStmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name ' . $tableWhere . ' and column_name = \'' . $field . '\'' ); + $fieldInfoStmt = oci_parse( + $this->mConn, + 'SELECT * FROM wiki_field_info_full WHERE table_name ' . + $tableWhere . ' and column_name = \'' . $field . '\'' + ); if ( oci_execute( $fieldInfoStmt, $this->execFlags() ) === false ) { $e = oci_error( $fieldInfoStmt ); $this->reportQueryError( $e['message'], $e['code'], 'fieldInfo QUERY', __METHOD__ ); + return false; } $res = new ORAResult( $this, $fieldInfoStmt ); @@ -982,19 +1127,21 @@ class DatabaseOracle extends DatabaseBase { $this->mFieldInfoCache["$table.$field"] = $fieldInfoTemp; } $res->free(); + return $fieldInfoTemp; } /** * @throws DBUnexpectedError - * @param $table - * @param $field + * @param string $table + * @param string $field * @return ORAField */ function fieldInfo( $table, $field ) { if ( is_array( $table ) ) { throw new DBUnexpectedError( $this, 'DatabaseOracle::fieldInfo called with table array!' ); } + return $this->fieldInfoMulti( $table, $field ); } @@ -1022,7 +1169,16 @@ class DatabaseOracle extends DatabaseBase { } } - /* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */ + /** + * defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; + * + * @param resource $fp + * @param bool|string $lineCallback + * @param bool|callable $resultCallback + * @param string $fname + * @param bool|callable $inputCallback + * @return bool|string + */ function sourceStream( $fp, $lineCallback = false, $resultCallback = false, $fname = __METHOD__, $inputCallback = false ) { $cmd = ''; @@ -1031,7 +1187,7 @@ class DatabaseOracle extends DatabaseBase { $replacements = array(); - while ( ! feof( $fp ) ) { + while ( !feof( $fp ) ) { if ( $lineCallback ) { call_user_func( $lineCallback ); } @@ -1041,7 +1197,7 @@ class DatabaseOracle extends DatabaseBase { if ( $sl < 0 ) { continue; } - if ( '-' == $line { 0 } && '-' == $line { 1 } ) { + if ( '-' == $line[0] && '-' == $line[1] ) { continue; } @@ -1055,7 +1211,7 @@ class DatabaseOracle extends DatabaseBase { $dollarquote = true; } } elseif ( !$dollarquote ) { - if ( ';' == $line { $sl } && ( $sl < 2 || ';' != $line { $sl - 1 } ) ) { + if ( ';' == $line[$sl] && ( $sl < 2 || ';' != $line[$sl - 1] ) ) { $done = true; $line = substr( $line, 0, $sl ); } @@ -1088,6 +1244,7 @@ class DatabaseOracle extends DatabaseBase { if ( false === $res ) { $err = $this->lastError(); + return "Query \"{$cmd}\" failed with error code \"$err\".\n"; } } @@ -1096,6 +1253,7 @@ class DatabaseOracle extends DatabaseBase { $done = false; } } + return true; } @@ -1114,8 +1272,10 @@ class DatabaseOracle extends DatabaseBase { if ( $e['code'] != '1435' ) { $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); } + return false; } + return true; } @@ -1128,6 +1288,7 @@ class DatabaseOracle extends DatabaseBase { if ( isset( $wgContLang->mLoaded ) && $wgContLang->mLoaded ) { $s = $wgContLang->checkTitleEncoding( $s ); } + return "'" . $this->strencode( $s ) . "'"; } @@ -1135,6 +1296,7 @@ class DatabaseOracle extends DatabaseBase { if ( !$this->getFlag( DBO_DDLMODE ) ) { $s = '/*Q*/' . $s; } + return $s; } @@ -1173,13 +1335,17 @@ class DatabaseOracle extends DatabaseBase { $conds2[$col] = $val; } } + return $conds2; } - function selectRow( $table, $vars, $conds, $fname = __METHOD__, $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 ); } + return parent::selectRow( $table, $vars, $conds, $fname, $options, $join_conds ); } @@ -1187,10 +1353,8 @@ class DatabaseOracle extends DatabaseBase { * Returns an optional USE INDEX clause to go after the table, and a * string to go at the end of the query * - * @private - * - * @param array $options an associative array of options to be turned into - * an SQL query, valid keys are listed in the function. + * @param array $options An associative array of options to be turned into + * an SQL query, valid keys are listed in the function. * @return array */ function makeSelectOptions( $options ) { @@ -1216,7 +1380,7 @@ class DatabaseOracle extends DatabaseBase { $startOpts .= 'DISTINCT'; } - if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { + if ( isset( $options['USE INDEX'] ) && !is_array( $options['USE INDEX'] ) ) { $useIndex = $this->useIndexClause( $options['USE INDEX'] ); } else { $useIndex = ''; @@ -1233,21 +1397,41 @@ class DatabaseOracle extends DatabaseBase { // all deletions on these tables have transactions so final failure rollbacks these updates $table = $this->tableName( $table ); if ( $table == $this->tableName( 'user' ) ) { - $this->update( 'archive', array( 'ar_user' => 0 ), array( 'ar_user' => $conds['user_id'] ), $fname ); - $this->update( 'ipblocks', array( 'ipb_user' => 0 ), array( 'ipb_user' => $conds['user_id'] ), $fname ); - $this->update( 'image', array( 'img_user' => 0 ), array( 'img_user' => $conds['user_id'] ), $fname ); - $this->update( 'oldimage', array( 'oi_user' => 0 ), array( 'oi_user' => $conds['user_id'] ), $fname ); - $this->update( 'filearchive', array( 'fa_deleted_user' => 0 ), array( 'fa_deleted_user' => $conds['user_id'] ), $fname ); - $this->update( 'filearchive', array( 'fa_user' => 0 ), array( 'fa_user' => $conds['user_id'] ), $fname ); - $this->update( 'uploadstash', array( 'us_user' => 0 ), array( 'us_user' => $conds['user_id'] ), $fname ); - $this->update( 'recentchanges', array( 'rc_user' => 0 ), array( 'rc_user' => $conds['user_id'] ), $fname ); - $this->update( 'logging', array( 'log_user' => 0 ), array( 'log_user' => $conds['user_id'] ), $fname ); + $this->update( 'archive', array( 'ar_user' => 0 ), + array( 'ar_user' => $conds['user_id'] ), $fname ); + $this->update( 'ipblocks', array( 'ipb_user' => 0 ), + array( 'ipb_user' => $conds['user_id'] ), $fname ); + $this->update( 'image', array( 'img_user' => 0 ), + array( 'img_user' => $conds['user_id'] ), $fname ); + $this->update( 'oldimage', array( 'oi_user' => 0 ), + array( 'oi_user' => $conds['user_id'] ), $fname ); + $this->update( 'filearchive', array( 'fa_deleted_user' => 0 ), + array( 'fa_deleted_user' => $conds['user_id'] ), $fname ); + $this->update( 'filearchive', array( 'fa_user' => 0 ), + array( 'fa_user' => $conds['user_id'] ), $fname ); + $this->update( 'uploadstash', array( 'us_user' => 0 ), + array( 'us_user' => $conds['user_id'] ), $fname ); + $this->update( 'recentchanges', array( 'rc_user' => 0 ), + array( 'rc_user' => $conds['user_id'] ), $fname ); + $this->update( 'logging', array( 'log_user' => 0 ), + array( 'log_user' => $conds['user_id'] ), $fname ); } elseif ( $table == $this->tableName( 'image' ) ) { - $this->update( 'oldimage', array( 'oi_name' => 0 ), array( 'oi_name' => $conds['img_name'] ), $fname ); + $this->update( 'oldimage', array( 'oi_name' => 0 ), + array( 'oi_name' => $conds['img_name'] ), $fname ); } + return parent::delete( $table, $conds, $fname ); } + /** + * @param string $table + * @param array $values + * @param array $conds + * @param string $fname + * @param array $options + * @return bool + * @throws DBUnexpectedError + */ function update( $table, $values, $conds, $fname = __METHOD__, $options = array() ) { global $wgContLang; @@ -1275,6 +1459,7 @@ class DatabaseOracle extends DatabaseBase { if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) { $e = oci_error( $this->mConn ); $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; } foreach ( $values as $col => &$val ) { @@ -1296,9 +1481,11 @@ class DatabaseOracle extends DatabaseBase { if ( oci_bind_by_name( $stmt, ":$col", $val ) === false ) { $e = oci_error( $stmt ); $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; } } else { + /** @var OCI_Lob[] $lob */ if ( ( $lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB ) ) === false ) { $e = oci_error( $stmt ); throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] ); @@ -1310,10 +1497,10 @@ class DatabaseOracle extends DatabaseBase { if ( $col_type == 'BLOB' ) { $lob[$col]->writeTemporary( $val ); - oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, SQLT_BLOB ); + oci_bind_by_name( $stmt, ":$col", $lob[$col], -1, SQLT_BLOB ); } else { $lob[$col]->writeTemporary( $val ); - oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, OCI_B_CLOB ); + oci_bind_by_name( $stmt, ":$col", $lob[$col], -1, OCI_B_CLOB ); } } } @@ -1322,8 +1509,9 @@ class DatabaseOracle extends DatabaseBase { if ( oci_execute( $stmt, $this->execFlags() ) === false ) { $e = oci_error( $stmt ); - if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) { + if ( !$this->ignoreDupValOnIndex || $e['code'] != '1' ) { $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; } else { $this->mAffectedRows = oci_num_rows( $stmt ); @@ -1344,7 +1532,7 @@ class DatabaseOracle extends DatabaseBase { oci_commit( $this->mConn ); } - oci_free_statement( $stmt ); + return oci_free_statement( $stmt ); } function bitNot( $field ) { @@ -1360,9 +1548,6 @@ class DatabaseOracle extends DatabaseBase { return 'BITOR(' . $fieldLeft . ', ' . $fieldRight . ')'; } - function setFakeMaster( $enabled = true ) { - } - function getDBname() { return $this->mDBname; } @@ -1371,6 +1556,14 @@ class DatabaseOracle extends DatabaseBase { return $this->mServer; } + public function buildGroupConcatField( + $delim, $table, $field, $conds = '', $join_conds = array() + ) { + $fld = "LISTAGG($field," . $this->addQuotes( $delim ) . ") WITHIN GROUP (ORDER BY $field)"; + + return '(' . $this->selectSQLText( $table, $fld, $conds, null, array(), $join_conds ) . ')'; + } + public function getSearchEngine() { return 'SearchOracle'; } @@ -1378,5 +1571,4 @@ class DatabaseOracle extends DatabaseBase { public function getInfinity() { return '31-12-2030 12:00:00.000000'; } - -} // end DatabaseOracle class +} diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 0bd966ba..ce14d7a9 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -26,9 +26,9 @@ class PostgresField implements Field { $has_default, $default; /** - * @param $db DatabaseBase - * @param $table - * @param $field + * @param DatabaseBase $db + * @param string $table + * @param string $field * @return null|PostgresField */ static function fromText( $db, $table, $field ) { @@ -79,6 +79,7 @@ SQL; $n->conname = $row->conname; $n->has_default = ( $row->atthasdef === 't' ); $n->default = $row->adsrc; + return $n; } @@ -113,8 +114,10 @@ SQL; function conname() { return $this->conname; } + /** * @since 1.19 + * @return bool|mixed */ function defaultValue() { if ( $this->has_default ) { @@ -123,7 +126,6 @@ SQL; return false; } } - } /** @@ -134,8 +136,7 @@ SQL; * @ingroup Database */ class PostgresTransactionState { - - static $WATCHED = array( + private static $WATCHED = array( array( "desc" => "%s: Connection state changed from %s -> %s\n", "states" => array( @@ -155,6 +156,12 @@ class PostgresTransactionState { ) ); + /** @var array */ + private $mNewState; + + /** @var array */ + private $mCurrentState; + public function __construct( $conn ) { $this->mConn = $conn; $this->update(); @@ -181,7 +188,6 @@ class PostgresTransactionState { } $old = next( $this->mCurrentState ); $new = next( $this->mNewState ); - } } } @@ -211,21 +217,23 @@ class PostgresTransactionState { * @since 1.19 */ class SavepointPostgres { - /** - * Establish a savepoint within a transaction - */ + /** @var DatabaseBase Establish a savepoint within a transaction */ protected $dbw; protected $id; protected $didbegin; + /** + * @param DatabaseBase $dbw + * @param int $id + */ public function __construct( $dbw, $id ) { $this->dbw = $dbw; $this->id = $id; $this->didbegin = false; /* If we are not in a transaction, we need to be for savepoint trickery */ if ( !$dbw->trxLevel() ) { - $dbw->begin( "FOR SAVEPOINT" ); - $this->didbegin = true; + $dbw->begin( "FOR SAVEPOINT" ); + $this->didbegin = true; } } @@ -247,10 +255,10 @@ class SavepointPostgres { global $wgDebugDBTransactions; if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) { if ( $wgDebugDBTransactions ) { - wfDebug( sprintf ( $msg_ok, $this->id ) ); + wfDebug( sprintf( $msg_ok, $this->id ) ); } } else { - wfDebug( sprintf ( $msg_failed, $this->id ) ); + wfDebug( sprintf( $msg_failed, $this->id ) ); } } @@ -284,10 +292,26 @@ class SavepointPostgres { * @ingroup Database */ class DatabasePostgres extends DatabaseBase { - var $mInsertId = null; - var $mLastResult = null; - var $numeric_version = null; - var $mAffectedRows = null; + /** @var resource */ + protected $mLastResult = null; + + /** @var int The number of rows affected as an integer */ + protected $mAffectedRows = null; + + /** @var int */ + private $mInsertId = null; + + /** @var float|string */ + private $numericVersion = null; + + /** @var string Connect string to open a PostgreSQL connection */ + private $connectString; + + /** @var PostgresTransactionState */ + private $mTransactionState; + + /** @var string */ + private $mCoreSchema; function getType() { return 'postgres'; @@ -296,32 +320,42 @@ class DatabasePostgres 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 searchableIPs() { return true; } + function functionalIndexes() { return true; } 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() ) . "'"; - $res = $this->doQuery( $SQL ); + $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() ) . "'"; + $res = $this->doQuery( $sql ); + return $this->numRows( $res ); } @@ -331,19 +365,24 @@ class DatabasePostgres extends DatabaseBase { * @param string $user * @param string $password * @param string $dbName - * @throws DBConnectionError + * @throws DBConnectionError|Exception * @return DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { # Test for Postgres support, to avoid suppressed fatal error if ( !function_exists( 'pg_connect' ) ) { - throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" ); + throw new DBConnectionError( + $this, + "Postgres functions missing, have you compiled PHP with the --with-pgsql\n" . + "option? (Note: if you recently installed PHP, you may need to restart your\n" . + "webserver and database)\n" + ); } global $wgDBport; if ( !strlen( $user ) ) { # e.g. the class is being loaded - return; + return null; } $this->mServer = $server; @@ -370,12 +409,20 @@ class DatabasePostgres extends DatabaseBase { $this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW ); $this->close(); $this->installErrorHandler(); - $this->mConn = pg_connect( $this->connectString ); + + try { + $this->mConn = pg_connect( $this->connectString ); + } catch ( Exception $ex ) { + $this->restoreErrorHandler(); + throw $ex; + } + $phpError = $this->restoreErrorHandler(); if ( !$this->mConn ) { wfDebug( "DB connection error\n" ); - wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); + wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . + substr( $password, 0, 3 ) . "...\n" ); wfDebug( $this->lastError() . "\n" ); throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) ); } @@ -406,7 +453,8 @@ class DatabasePostgres extends DatabaseBase { /** * Postgres doesn't support selectDB in the same way MySQL does. So if the * DB name doesn't match the open connection, open a new one - * @return + * @param string $db + * @return bool */ function selectDB( $db ) { if ( $this->mDBname !== $db ) { @@ -421,6 +469,7 @@ class DatabasePostgres extends DatabaseBase { foreach ( $vars as $name => $value ) { $s .= "$name='" . str_replace( "'", "\\'", $value ) . "' "; } + return $s; } @@ -447,38 +496,44 @@ class DatabasePostgres extends DatabaseBase { if ( pg_result_error( $this->mLastResult ) ) { return false; } + return $this->mLastResult; } protected function dumpError() { - $diags = array( PGSQL_DIAG_SEVERITY, - PGSQL_DIAG_SQLSTATE, - PGSQL_DIAG_MESSAGE_PRIMARY, - PGSQL_DIAG_MESSAGE_DETAIL, - PGSQL_DIAG_MESSAGE_HINT, - PGSQL_DIAG_STATEMENT_POSITION, - PGSQL_DIAG_INTERNAL_POSITION, - PGSQL_DIAG_INTERNAL_QUERY, - PGSQL_DIAG_CONTEXT, - PGSQL_DIAG_SOURCE_FILE, - PGSQL_DIAG_SOURCE_LINE, - PGSQL_DIAG_SOURCE_FUNCTION ); + $diags = array( + PGSQL_DIAG_SEVERITY, + PGSQL_DIAG_SQLSTATE, + PGSQL_DIAG_MESSAGE_PRIMARY, + PGSQL_DIAG_MESSAGE_DETAIL, + PGSQL_DIAG_MESSAGE_HINT, + PGSQL_DIAG_STATEMENT_POSITION, + PGSQL_DIAG_INTERNAL_POSITION, + PGSQL_DIAG_INTERNAL_QUERY, + PGSQL_DIAG_CONTEXT, + PGSQL_DIAG_SOURCE_FILE, + 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 ) ) ); } } function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - /* Transaction stays in the ERROR state until rolledback */ if ( $tempIgnore ) { /* Check for constraint violation */ if ( $errno === '23505' ) { parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore ); + return; } } - /* Don't ignore serious errors */ - $this->rollback( __METHOD__ ); + /* Transaction stays in the ERROR state until rolledback */ + if ( $this->mTrxLevel ) { + $this->rollback( __METHOD__ ); + }; parent::reportQueryError( $error, $errno, $sql, $fname, false ); } @@ -486,6 +541,10 @@ class DatabasePostgres extends DatabaseBase { return $this->query( $sql, $fname, true ); } + /** + * @param stdClass|ResultWrapper $res + * @throws DBUnexpectedError + */ function freeResult( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; @@ -498,6 +557,11 @@ class DatabasePostgres extends DatabaseBase { } } + /** + * @param ResultWrapper|stdClass $res + * @return stdClass + * @throws DBUnexpectedError + */ function fetchObject( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; @@ -510,8 +574,12 @@ 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 ) ) { - throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); + throw new DBUnexpectedError( + $this, + 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) + ); } + return $row; } @@ -523,8 +591,12 @@ class DatabasePostgres extends DatabaseBase { $row = pg_fetch_array( $res ); wfRestoreWarnings(); if ( pg_last_error( $this->mConn ) ) { - throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); + throw new DBUnexpectedError( + $this, + 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) + ); } + return $row; } @@ -536,8 +608,12 @@ class DatabasePostgres extends DatabaseBase { $n = pg_num_rows( $res ); wfRestoreWarnings(); if ( pg_last_error( $this->mConn ) ) { - throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); + throw new DBUnexpectedError( + $this, + 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) + ); } + return $n; } @@ -545,6 +621,7 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } + return pg_num_fields( $res ); } @@ -552,6 +629,7 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } + return pg_field_name( $res, $n ); } @@ -559,16 +637,22 @@ class DatabasePostgres extends DatabaseBase { * Return the result of the last call to nextSequenceValue(); * This must be called after nextSequenceValue(). * - * @return integer|null + * @return int|null */ function insertId() { return $this->mInsertId; } + /** + * @param mixed $res + * @param int $row + * @return bool + */ function dataSeek( $res, $row ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } + return pg_result_seek( $res, $row ); } @@ -583,6 +667,7 @@ class DatabasePostgres extends DatabaseBase { return 'No database connection'; } } + function lastErrno() { if ( $this->mLastResult ) { return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE ); @@ -599,6 +684,7 @@ class DatabasePostgres extends DatabaseBase { if ( empty( $this->mLastResult ) ) { return 0; } + return pg_affected_rows( $this->mLastResult ); } @@ -608,9 +694,17 @@ class DatabasePostgres extends DatabaseBase { * This is not necessarily an accurate estimate, so use sparingly * Returns -1 if count cannot be found * Takes same arguments as Database::select() + * + * @param string $table + * @param string $vars + * @param string $conds + * @param string $fname + * @param array $options * @return int */ - function estimateRowCount( $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = array() ) { + function estimateRowCount( $table, $vars = '*', $conds = '', + $fname = __METHOD__, $options = array() + ) { $options['EXPLAIN'] = true; $res = $this->select( $table, $vars, $conds, $fname, $options ); $rows = -1; @@ -621,12 +715,17 @@ class DatabasePostgres extends DatabaseBase { $rows = $count[1]; } } + return $rows; } /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure + * + * @param string $table + * @param string $index + * @param string $fname * @return bool|null */ function indexInfo( $table, $index, $fname = __METHOD__ ) { @@ -640,6 +739,7 @@ class DatabasePostgres extends DatabaseBase { return $row; } } + return false; } @@ -647,7 +747,9 @@ class DatabasePostgres extends DatabaseBase { * Returns is of attributes used in index * * @since 1.19 - * @return Array + * @param string $index + * @param bool|string $schema + * @return array */ function indexAttributes( $index, $schema = false ) { if ( $schema === false ) { @@ -702,6 +804,7 @@ __INDEXATTR__; } else { return null; } + return $a; } @@ -714,10 +817,8 @@ __INDEXATTR__; if ( !$res ) { return null; } - foreach ( $res as $row ) { - return true; - } - return false; + + return $res->numRows() > 0; } /** @@ -727,10 +828,15 @@ __INDEXATTR__; * In Postgres when using FOR UPDATE, only the main table and tables that are inner joined * can be locked. That means tables in an outer join cannot be FOR UPDATE locked. Trying to do * so causes a DB error. This wrapper checks which tables can be locked and adjusts it accordingly. + * + * MySQL uses "ORDER BY NULL" as an optimization hint, but that syntax is illegal in PostgreSQL. + * @see DatabaseBase::selectSQLText */ - function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, $options = array(), $join_conds = array() ) { + function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__, + $options = array(), $join_conds = array() + ) { if ( is_array( $options ) ) { - $forUpdateKey = array_search( 'FOR UPDATE', $options ); + $forUpdateKey = array_search( 'FOR UPDATE', $options, true ); if ( $forUpdateKey !== false && $join_conds ) { unset( $options[$forUpdateKey] ); @@ -740,6 +846,10 @@ __INDEXATTR__; } } } + + if ( isset( $options['ORDER BY'] ) && $options['ORDER BY'] == 'NULL' ) { + unset( $options['ORDER BY'] ); + } } return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); @@ -751,11 +861,10 @@ __INDEXATTR__; * $args may be a single associative array, or an array of these with numeric keys, * for multi-row insert (Postgres version 8.2 and above only). * - * @param $table String: Name of the table to insert to. - * @param $args Array: Items to insert into the table. - * @param $fname String: Name of the function, for profiling - * @param string $options or Array. Valid options: IGNORE - * + * @param string $table Name of the table to insert to. + * @param array $args Items to insert into the table. + * @param string $fname Name of the function, for profiling + * @param array|string $options String or array. Valid options: IGNORE * @return bool Success of insert operation. IGNORE always returns true. */ function insert( $table, $args, $fname = __METHOD__, $options = array() ) { @@ -764,7 +873,7 @@ __INDEXATTR__; } $table = $this->tableName( $table ); - if ( !isset( $this->numeric_version ) ) { + if ( !isset( $this->numericVersion ) ) { $this->getServerVersion(); } @@ -793,7 +902,7 @@ __INDEXATTR__; $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES '; if ( $multi ) { - if ( $this->numeric_version >= 8.2 && !$savepoint ) { + if ( $this->numericVersion >= 8.2 && !$savepoint ) { $first = true; foreach ( $args as $row ) { if ( $first ) { @@ -853,7 +962,7 @@ __INDEXATTR__; } } if ( $savepoint ) { - $olde = error_reporting( $olde ); + error_reporting( $olde ); $savepoint->commit(); // Set the affected row count for the whole operation @@ -869,15 +978,23 @@ __INDEXATTR__; /** * INSERT SELECT wrapper * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...) - * Source items may be literals rather then field names, but strings should be quoted with Database::addQuotes() + * Source items may be literals rather then field names, but strings should + * be quoted with Database::addQuotes() * $conds may be "*" to copy the whole table * srcTable may be an array of tables. * @todo FIXME: Implement this a little better (seperate select/insert)? + * + * @param string $destTable + * @param array|string $srcTable + * @param array $varMap + * @param array $conds + * @param string $fname + * @param array $insertOptions + * @param array $selectOptions * @return bool */ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = __METHOD__, - $insertOptions = array(), $selectOptions = array() ) - { + $insertOptions = array(), $selectOptions = array() ) { $destTable = $this->tableName( $destTable ); if ( !is_array( $insertOptions ) ) { @@ -907,8 +1024,8 @@ __INDEXATTR__; } $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . - " SELECT $startOpts " . implode( ',', $varMap ) . - " FROM $srcTable $useIndex"; + " SELECT $startOpts " . implode( ',', $varMap ) . + " FROM $srcTable $useIndex"; if ( $conds != '*' ) { $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); @@ -925,7 +1042,7 @@ __INDEXATTR__; $savepoint->release(); $numrowsinserted++; } - $olde = error_reporting( $olde ); + error_reporting( $olde ); $savepoint->commit(); // Set the affected row count for the whole operation @@ -957,25 +1074,31 @@ __INDEXATTR__; /** * Return the next in a sequence, save the value for retrieval via insertId() - * @return null + * + * @param string $seqName + * @return int|null */ function nextSequenceValue( $seqName ) { $safeseq = str_replace( "'", "''", $seqName ); $res = $this->query( "SELECT nextval('$safeseq')" ); $row = $this->fetchRow( $res ); $this->mInsertId = $row[0]; + return $this->mInsertId; } /** * Return the current value of a sequence. Assumes it has been nextval'ed in this session. - * @return + * + * @param string $seqName + * @return int */ function currentSequenceValue( $seqName ) { $safeseq = str_replace( "'", "''", $seqName ); $res = $this->query( "SELECT currval('$safeseq')" ); $row = $this->fetchRow( $res ); $currval = $row[0]; + return $currval; } @@ -993,6 +1116,7 @@ __INDEXATTR__; } else { $size = $row->size; } + return $size; } @@ -1007,7 +1131,9 @@ __INDEXATTR__; 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 ); + + return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName " . + "(LIKE $oldName INCLUDING DEFAULTS)", $fname ); } function listTables( $prefix = null, $fname = __METHOD__ ) { @@ -1030,7 +1156,7 @@ __INDEXATTR__; return wfTimestamp( TS_POSTGRES, $ts ); } - /* + /** * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12 * to http://www.php.net/manual/en/ref.pgsql.php * @@ -1042,10 +1168,10 @@ __INDEXATTR__; * This should really be handled by PHP PostgreSQL module * * @since 1.19 - * @param $text string: postgreql array returned in a text form like {a,b} - * @param $output string - * @param $limit int - * @param $offset int + * @param string $text Postgreql array returned in a text form like {a,b} + * @param string $output + * @param int $limit + * @param int $offset * @return string */ function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) { @@ -1062,8 +1188,8 @@ __INDEXATTR__; $text, $match, 0, $offset ); $offset += strlen( $match[0] ); $output[] = ( '"' != $match[1][0] - ? $match[1] - : stripcslashes( substr( $match[1], 1, -1 ) ) ); + ? $match[1] + : stripcslashes( substr( $match[1], 1, -1 ) ) ); if ( '},' == $match[3] ) { return $output; } @@ -1071,18 +1197,22 @@ __INDEXATTR__; $offset = $this->pg_array_parse( $text, $output, $limit, $offset + 1 ); } } while ( $limit > $offset ); + return $output; } /** * Return aggregated value function call + * @param array $valuedata + * @param string $valuename + * @return array */ public function aggregateValue( $valuedata, $valuename = 'value' ) { return $valuedata; } /** - * @return string wikitext of a link to the server software's web site + * @return string Wikitext of a link to the server software's web site */ public function getSoftwareLink() { return '[{{int:version-db-postgres-url}} PostgreSQL]'; @@ -1093,11 +1223,12 @@ __INDEXATTR__; * Needs transaction * * @since 1.19 - * @return string return default schema for the current session + * @return string Default schema for the current session */ function getCurrentSchema() { $res = $this->query( "SELECT current_schema()", __METHOD__ ); $row = $this->fetchRow( $res ); + return $row[0]; } @@ -1109,13 +1240,15 @@ __INDEXATTR__; * @see getSearchPath() * @see setSearchPath() * @since 1.19 - * @return array list of actual schemas for the current sesson + * @return array List of actual schemas for the current sesson */ function getSchemas() { $res = $this->query( "SELECT current_schemas(false)", __METHOD__ ); $row = $this->fetchRow( $res ); $schemas = array(); + /* PHP pgsql support does not support array type, "{a,b}" string is returned */ + return $this->pg_array_parse( $row[0], $schemas ); } @@ -1126,12 +1259,14 @@ __INDEXATTR__; * Needs transaction * * @since 1.19 - * @return array how to search for table names schemas for the current user + * @return array How to search for table names schemas for the current user */ function getSearchPath() { $res = $this->query( "SHOW search_path", __METHOD__ ); $row = $this->fetchRow( $res ); + /* PostgreSQL returns SHOW values as strings */ + return explode( ",", $row[0] ); } @@ -1140,7 +1275,7 @@ __INDEXATTR__; * Values may contain magic keywords like "$user" * @since 1.19 * - * @param $search_path array list of schemas to be searched by default + * @param array $search_path List of schemas to be searched by default */ function setSearchPath( $search_path ) { $this->query( "SET search_path = " . implode( ", ", $search_path ) ); @@ -1157,14 +1292,15 @@ __INDEXATTR__; * This will be also called by the installer after the schema is created * * @since 1.19 - * @param $desired_schema string + * + * @param string $desiredSchema */ - function determineCoreSchema( $desired_schema ) { + function determineCoreSchema( $desiredSchema ) { $this->begin( __METHOD__ ); - if ( $this->schemaExists( $desired_schema ) ) { - if ( in_array( $desired_schema, $this->getSchemas() ) ) { - $this->mCoreSchema = $desired_schema; - wfDebug( "Schema \"" . $desired_schema . "\" already in the search path\n" ); + if ( $this->schemaExists( $desiredSchema ) ) { + if ( in_array( $desiredSchema, $this->getSchemas() ) ) { + $this->mCoreSchema = $desiredSchema; + wfDebug( "Schema \"" . $desiredSchema . "\" already in the search path\n" ); } else { /** * Prepend our schema (e.g. 'mediawiki') in front @@ -1173,14 +1309,15 @@ __INDEXATTR__; */ $search_path = $this->getSearchPath(); array_unshift( $search_path, - $this->addIdentifierQuotes( $desired_schema )); + $this->addIdentifierQuotes( $desiredSchema ) ); $this->setSearchPath( $search_path ); - $this->mCoreSchema = $desired_schema; - wfDebug( "Schema \"" . $desired_schema . "\" added to the search path\n" ); + $this->mCoreSchema = $desiredSchema; + wfDebug( "Schema \"" . $desiredSchema . "\" added to the search path\n" ); } } else { $this->mCoreSchema = $this->getCurrentSchema(); - wfDebug( "Schema \"" . $desired_schema . "\" not found, using current \"" . $this->mCoreSchema . "\"\n" ); + wfDebug( "Schema \"" . $desiredSchema . "\" not found, using current \"" . + $this->mCoreSchema . "\"\n" ); } /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */ $this->commit( __METHOD__ ); @@ -1190,7 +1327,7 @@ __INDEXATTR__; * Return schema name fore core MediaWiki tables * * @since 1.19 - * @return string core schema name + * @return string Core schema name */ function getCoreSchema() { return $this->mCoreSchema; @@ -1200,25 +1337,29 @@ __INDEXATTR__; * @return string Version information from the database */ function getServerVersion() { - if ( !isset( $this->numeric_version ) ) { + if ( !isset( $this->numericVersion ) ) { $versionInfo = pg_version( $this->mConn ); if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) { // Old client, abort install - $this->numeric_version = '7.3 or earlier'; + $this->numericVersion = '7.3 or earlier'; } elseif ( isset( $versionInfo['server'] ) ) { // Normal client - $this->numeric_version = $versionInfo['server']; + $this->numericVersion = $versionInfo['server']; } else { // Bug 16937: broken pgsql extension from PHP<5.3 - $this->numeric_version = pg_parameter_status( $this->mConn, 'server_version' ); + $this->numericVersion = pg_parameter_status( $this->mConn, 'server_version' ); } } - return $this->numeric_version; + + return $this->numericVersion; } /** * Query whether a given relation exists (in the given schema, or the * default mw one if not given) + * @param string $table + * @param array|string $types + * @param bool|string $schema * @return bool */ function relationExists( $table, $types, $schema = false ) { @@ -1231,17 +1372,21 @@ __INDEXATTR__; $table = $this->realTableName( $table, 'raw' ); $etable = $this->addQuotes( $table ); $eschema = $this->addQuotes( $schema ); - $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " + $sql = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema " . "AND c.relkind IN ('" . implode( "','", $types ) . "')"; - $res = $this->query( $SQL ); + $res = $this->query( $sql ); $count = $res ? $res->numRows() : 0; + return (bool)$count; } /** * For backward compatibility, this function checks both tables and * views. + * @param string $table + * @param string $fname + * @param bool|string $schema * @return bool */ function tableExists( $table, $fname = __METHOD__, $schema = false ) { @@ -1271,6 +1416,7 @@ SQL; return null; } $rows = $res->numRows(); + return $rows; } @@ -1282,41 +1428,47 @@ SQL; 'schemaname' => $this->getCoreSchema() ) ); + return $exists === $rule; } function constraintExists( $table, $constraint ) { - $SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints " . - "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", + $sql = sprintf( "SELECT 1 FROM information_schema.table_constraints " . + "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", $this->addQuotes( $this->getCoreSchema() ), $this->addQuotes( $table ), $this->addQuotes( $constraint ) ); - $res = $this->query( $SQL ); + $res = $this->query( $sql ); if ( !$res ) { return null; } $rows = $res->numRows(); + return $rows; } /** * Query whether a given schema exists. Returns true if it does, false if it doesn't. + * @param string $schema * @return bool */ function schemaExists( $schema ) { $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1, array( 'nspname' => $schema ), __METHOD__ ); + return (bool)$exists; } /** * Returns true if a given role (i.e. user) exists, false otherwise. + * @param string $roleName * @return bool */ function roleExists( $roleName ) { $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1, array( 'rolname' => $roleName ), __METHOD__ ); + return (bool)$exists; } @@ -1326,17 +1478,20 @@ SQL; /** * pg_field_type() wrapper + * @param ResultWrapper|resource $res ResultWrapper or PostgreSQL query result resource + * @param int $index Field number, starting from 0 * @return string */ function fieldType( $res, $index ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } + return pg_field_type( $res, $index ); } /** - * @param $b + * @param string $b * @return Blob */ function encodeBlob( $b ) { @@ -1347,6 +1502,7 @@ SQL; if ( $b instanceof Blob ) { $b = $b->fetch(); } + return pg_unescape_bytea( $b ); } @@ -1355,7 +1511,7 @@ SQL; } /** - * @param $s null|bool|Blob + * @param null|bool|Blob $s * @return int|string */ function addQuotes( $s ) { @@ -1366,6 +1522,7 @@ SQL; } elseif ( $s instanceof Blob ) { return "'" . $s->fetch( $s ) . "'"; } + return "'" . pg_escape_string( $this->mConn, $s ) . "'"; } @@ -1373,21 +1530,18 @@ SQL; * Postgres specific version of replaceVars. * Calls the parent version in Database.php * - * @private - * * @param string $ins SQL string, read from a stream (usually tables.sql) - * * @return string SQL string */ protected function replaceVars( $ins ) { $ins = parent::replaceVars( $ins ); - if ( $this->numeric_version >= 8.3 ) { + if ( $this->numericVersion >= 8.3 ) { // Thanks for not providing backwards-compatibility, 8.3 $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins ); } - if ( $this->numeric_version <= 8.1 ) { // Our minimum version + if ( $this->numericVersion <= 8.1 ) { // Our minimum version $ins = str_replace( 'USING gin', 'USING gist', $ins ); } @@ -1397,10 +1551,8 @@ SQL; /** * Various select options * - * @private - * - * @param array $options an associative array of options to be turned into - * an SQL query, valid keys are listed in the function. + * @param array $options An associative array of options to be turned into + * an SQL query, valid keys are listed in the function. * @return array */ function makeSelectOptions( $options ) { @@ -1425,8 +1577,9 @@ SQL; //} if ( isset( $options['FOR UPDATE'] ) ) { - $postLimitTail .= ' FOR UPDATE OF ' . implode( ', ', $options['FOR UPDATE'] ); - } else if ( isset( $noKeyOptions['FOR UPDATE'] ) ) { + $postLimitTail .= ' FOR UPDATE OF ' . + implode( ', ', array_map( array( &$this, 'tableName' ), $options['FOR UPDATE'] ) ); + } elseif ( isset( $noKeyOptions['FOR UPDATE'] ) ) { $postLimitTail .= ' FOR UPDATE'; } @@ -1437,9 +1590,6 @@ SQL; return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); } - function setFakeMaster( $enabled = true ) { - } - function getDBname() { return $this->mDBname; } @@ -1452,6 +1602,14 @@ SQL; return implode( ' || ', $stringList ); } + public function buildGroupConcatField( + $delimiter, $table, $field, $conds = '', $options = array(), $join_conds = array() + ) { + $fld = "array_to_string(array_agg($field)," . $this->addQuotes( $delimiter ) . ')'; + + return '(' . $this->selectSQLText( $table, $fld, $conds, null, array(), $join_conds ) . ')'; + } + public function getSearchEngine() { return 'SearchPostgres'; } @@ -1461,11 +1619,11 @@ SQL; if ( substr( $newLine, 0, 4 ) == '$mw$' ) { if ( $this->delimiter ) { $this->delimiter = false; - } - else { + } else { $this->delimiter = ';'; } } + return parent::streamStatementEnd( $sql, $newLine ); } @@ -1473,9 +1631,9 @@ SQL; * Check to see if a named lock is available. This is non-blocking. * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS * - * @param string $lockName name of lock to poll - * @param string $method name of method calling us - * @return Boolean + * @param string $lockName Name of lock to poll + * @param string $method Name of method calling us + * @return bool * @since 1.20 */ public function lockIsFree( $lockName, $method ) { @@ -1483,14 +1641,15 @@ SQL; $result = $this->query( "SELECT (CASE(pg_try_advisory_lock($key)) WHEN 'f' THEN 'f' ELSE pg_advisory_unlock($key) END) AS lockstatus", $method ); $row = $this->fetchObject( $result ); + return ( $row->lockstatus === 't' ); } /** * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS - * @param $lockName string - * @param $method string - * @param $timeout int + * @param string $lockName + * @param string $method + * @param int $timeout * @return bool */ public function lock( $lockName, $method, $timeout = 5 ) { @@ -1506,19 +1665,22 @@ SQL; } } wfDebug( __METHOD__ . " failed to acquire lock\n" ); + return false; } /** - * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS - * @param $lockName string - * @param $method string + * See http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKSFROM + * PG DOCS: http://www.postgresql.org/docs/8.2/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS + * @param string $lockName + * @param string $method * @return bool */ public function unlock( $lockName, $method ) { $key = $this->addQuotes( $this->bigintFromLockName( $lockName ) ); $result = $this->query( "SELECT pg_advisory_unlock($key) as lockstatus", $method ); $row = $this->fetchObject( $result ); + return ( $row->lockstatus === 't' ); } diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index 3e034649..dd2e813e 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -26,38 +26,53 @@ * @ingroup Database */ class DatabaseSqlite extends DatabaseBase { - + /** @var bool Whether full text is enabled */ private static $fulltextEnabled = null; - var $mAffectedRows; - var $mLastResult; - var $mDatabaseFile; - var $mName; + /** @var string File name for SQLite database file */ + public $mDatabaseFile; - /** - * @var PDO - */ + /** @var int The number of rows affected as an integer */ + protected $mAffectedRows; + + /** @var resource */ + protected $mLastResult; + + /** @var PDO */ protected $mConn; - /** - * Constructor. - * Parameters $server, $user and $password are not used. - * @param $server string - * @param $user string - * @param $password string - * @param $dbName string - * @param $flags int - */ - function __construct( $server = false, $user = false, $password = false, $dbName = false, $flags = 0 ) { - $this->mName = $dbName; - parent::__construct( $server, $user, $password, $dbName, $flags ); + /** @var FSLockManager (hopefully on the same server as the DB) */ + protected $lockMgr; + + function __construct( $p = null ) { + global $wgSharedDB, $wgSQLiteDataDir; + + if ( !is_array( $p ) ) { // legacy calling pattern + wfDeprecated( __METHOD__ . " method called without parameter array.", "1.22" ); + $args = func_get_args(); + $p = array( + 'host' => isset( $args[0] ) ? $args[0] : false, + 'user' => isset( $args[1] ) ? $args[1] : false, + 'password' => isset( $args[2] ) ? $args[2] : false, + 'dbname' => isset( $args[3] ) ? $args[3] : false, + 'flags' => isset( $args[4] ) ? $args[4] : 0, + 'tablePrefix' => isset( $args[5] ) ? $args[5] : 'get from global', + 'schema' => 'get from global', + 'foreign' => isset( $args[6] ) ? $args[6] : false + ); + } + $this->mDBname = $p['dbname']; + parent::__construct( $p ); // parent doesn't open when $user is false, but we can work with $dbName - if ( $dbName && !$this->isOpen() ) { - global $wgSharedDB; - if ( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) { - $this->attachDatabase( $wgSharedDB ); + if ( $p['dbname'] && !$this->isOpen() ) { + if ( $this->open( $p['host'], $p['user'], $p['password'], $p['dbname'] ) ) { + if ( $wgSharedDB ) { + $this->attachDatabase( $wgSharedDB ); + } } } + + $this->lockMgr = new FSLockManager( array( 'lockDirectory' => "$wgSQLiteDataDir/locks" ) ); } /** @@ -97,18 +112,20 @@ class DatabaseSqlite extends DatabaseBase { throw new DBConnectionError( $this, "SQLite database not accessible" ); } $this->openFile( $fileName ); + return $this->mConn; } /** * Opens a database file * - * @param $fileName string - * + * @param string $fileName * @throws DBConnectionError * @return PDO|bool SQL connection or false if failed */ function openFile( $fileName ) { + $err = false; + $this->mDatabaseFile = $fileName; try { if ( $this->mFlags & DBO_PERSISTENT ) { @@ -120,18 +137,23 @@ class DatabaseSqlite extends DatabaseBase { } catch ( PDOException $e ) { $err = $e->getMessage(); } + if ( !$this->mConn ) { wfDebug( "DB connection error: $err\n" ); throw new DBConnectionError( $this, $err ); } + $this->mOpened = !!$this->mConn; # set error codes only, don't raise exceptions if ( $this->mOpened ) { $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT ); # Enforce LIKE to be case sensitive, just like MySQL $this->query( 'PRAGMA case_sensitive_like = 1' ); - return true; + + return $this->mConn; } + + return false; } /** @@ -140,6 +162,7 @@ class DatabaseSqlite extends DatabaseBase { */ protected function closeConnection() { $this->mConn = null; + return true; } @@ -147,7 +170,7 @@ class DatabaseSqlite extends DatabaseBase { * Generates a database file name. Explicitly public for installer. * @param string $dir Directory where database resides * @param string $dbName Database name - * @return String + * @return string */ public static function generateFileName( $dir, $dbName ) { return "$dir/$dbName.sqlite"; @@ -167,12 +190,13 @@ class DatabaseSqlite extends DatabaseBase { self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false; } } + return self::$fulltextEnabled; } /** * Returns version of currently supported SQLite fulltext search module or false if none present. - * @return String + * @return string */ static function getFulltextSearchModule() { static $cachedResult = null; @@ -188,6 +212,7 @@ class DatabaseSqlite extends DatabaseBase { $cachedResult = 'FTS3'; } $db->close(); + return $cachedResult; } @@ -195,10 +220,11 @@ class DatabaseSqlite extends DatabaseBase { * Attaches external database to our connection, see http://sqlite.org/lang_attach.html * for details. * - * @param string $name database name to be used in queries like SELECT foo FROM dbname.table - * @param string $file database file name. If omitted, will be generated using $name and $wgSQLiteDataDir - * @param string $fname calling function name - * + * @param string $name Database name to be used in queries like + * SELECT foo FROM dbname.table + * @param bool|string $file Database file name. If omitted, will be generated + * using $name and $wgSQLiteDataDir + * @param string $fname Calling function name * @return ResultWrapper */ function attachDatabase( $name, $file = false, $fname = __METHOD__ ) { @@ -207,14 +233,14 @@ class DatabaseSqlite extends DatabaseBase { $file = self::generateFileName( $wgSQLiteDataDir, $name ); } $file = $this->addQuotes( $file ); + return $this->query( "ATTACH DATABASE $file AS $name", $fname ); } /** * @see DatabaseBase::isWriteQuery() * - * @param $sql string - * + * @param string $sql * @return bool */ function isWriteQuery( $sql ) { @@ -224,9 +250,8 @@ class DatabaseSqlite extends DatabaseBase { /** * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result * - * @param $sql string - * - * @return ResultWrapper + * @param string $sql + * @return bool|ResultWrapper */ protected function doQuery( $sql ) { $res = $this->mConn->query( $sql ); @@ -237,11 +262,12 @@ class DatabaseSqlite extends DatabaseBase { $this->mAffectedRows = $r->rowCount(); $res = new ResultWrapper( $this, $r->fetchAll() ); } + return $res; } /** - * @param $res ResultWrapper + * @param ResultWrapper|mixed $res */ function freeResult( $res ) { if ( $res instanceof ResultWrapper ) { @@ -252,8 +278,8 @@ class DatabaseSqlite extends DatabaseBase { } /** - * @param $res ResultWrapper - * @return object|bool + * @param ResultWrapper|array $res + * @return stdClass|bool */ function fetchObject( $res ) { if ( $res instanceof ResultWrapper ) { @@ -274,11 +300,12 @@ class DatabaseSqlite extends DatabaseBase { return $obj; } + return false; } /** - * @param $res ResultWrapper + * @param ResultWrapper|mixed $res * @return array|bool */ function fetchRow( $res ) { @@ -290,51 +317,61 @@ class DatabaseSqlite extends DatabaseBase { $cur = current( $r ); if ( is_array( $cur ) ) { next( $r ); + return $cur; } + return false; } /** * The PDO::Statement class implements the array interface so count() will work * - * @param $res ResultWrapper - * + * @param ResultWrapper|array $res * @return int */ function numRows( $res ) { $r = $res instanceof ResultWrapper ? $res->result : $res; + return count( $r ); } /** - * @param $res ResultWrapper + * @param ResultWrapper $res * @return int */ function numFields( $res ) { $r = $res instanceof ResultWrapper ? $res->result : $res; - return is_array( $r ) ? count( $r[0] ) : 0; + if ( is_array( $r ) && count( $r ) > 0 ) { + // The size of the result array is twice the number of fields. (Bug: 65578) + return count( $r[0] ) / 2; + } else { + // If the result is empty return 0 + return 0; + } } /** - * @param $res ResultWrapper - * @param $n + * @param ResultWrapper $res + * @param int $n * @return bool */ function fieldName( $res, $n ) { $r = $res instanceof ResultWrapper ? $res->result : $res; if ( is_array( $r ) ) { $keys = array_keys( $r[0] ); + return $keys[$n]; } + return false; } /** * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks * - * @param $name - * @param $format String + * @param string $name + * @param string $format * @return string */ function tableName( $name, $format = 'quoted' ) { @@ -342,14 +379,14 @@ class DatabaseSqlite extends DatabaseBase { if ( strpos( $name, 'sqlite_' ) === 0 ) { return $name; } + return str_replace( '"', '', parent::tableName( $name, $format ) ); } /** * Index names have DB scope * - * @param $index string - * + * @param string $index * @return string */ function indexName( $index ) { @@ -367,8 +404,8 @@ class DatabaseSqlite extends DatabaseBase { } /** - * @param $res ResultWrapper - * @param $row + * @param ResultWrapper|array $res + * @param int $row */ function dataSeek( $res, $row ) { if ( $res instanceof ResultWrapper ) { @@ -392,6 +429,7 @@ class DatabaseSqlite extends DatabaseBase { return "Cannot return last error, no db connection"; } $e = $this->mConn->errorInfo(); + return isset( $e[2] ) ? $e[2] : ''; } @@ -403,6 +441,7 @@ class DatabaseSqlite extends DatabaseBase { return "Cannot return last error, no db connection"; } else { $info = $this->mConn->errorInfo(); + return $info[1]; } } @@ -419,6 +458,9 @@ class DatabaseSqlite extends DatabaseBase { * Returns false if the index does not exist * - if errors are explicitly ignored, returns NULL on failure * + * @param string $table + * @param string $index + * @param string $fname * @return array */ function indexInfo( $table, $index, $fname = __METHOD__ ) { @@ -434,13 +476,14 @@ class DatabaseSqlite extends DatabaseBase { foreach ( $res as $row ) { $info[] = $row->name; } + return $info; } /** - * @param $table - * @param $index - * @param $fname string + * @param string $table + * @param string $index + * @param string $fname * @return bool|null */ function indexUnique( $table, $index, $fname = __METHOD__ ) { @@ -460,14 +503,14 @@ class DatabaseSqlite extends DatabaseBase { } $firstPart = substr( $row->sql, 0, $indexPos ); $options = explode( ' ', $firstPart ); + return in_array( 'UNIQUE', $options ); } /** * Filter the options used in SELECT statements * - * @param $options array - * + * @param array $options * @return array */ function makeSelectOptions( $options ) { @@ -476,20 +519,23 @@ class DatabaseSqlite extends DatabaseBase { $options[$k] = ''; } } + return parent::makeSelectOptions( $options ); } /** - * @param $options array + * @param array $options * @return string */ - function makeUpdateOptions( $options ) { + protected function makeUpdateOptionsArray( $options ) { + $options = parent::makeUpdateOptionsArray( $options ); $options = self::fixIgnore( $options ); - return parent::makeUpdateOptions( $options ); + + return $options; } /** - * @param $options array + * @param array $options * @return array */ static function fixIgnore( $options ) { @@ -499,20 +545,26 @@ class DatabaseSqlite extends DatabaseBase { $options[$k] = 'OR IGNORE'; } } + return $options; } /** - * @param $options array + * @param array $options * @return string */ function makeInsertOptions( $options ) { $options = self::fixIgnore( $options ); + return parent::makeInsertOptions( $options ); } /** * Based on generic method (parent) with some prior SQLite-sepcific adjustments + * @param string $table + * @param array $a + * @param string $fname + * @param array $options * @return bool */ function insert( $table, $a, $fname = __METHOD__, $options = array() ) { @@ -536,10 +588,10 @@ class DatabaseSqlite extends DatabaseBase { } /** - * @param $table - * @param $uniqueIndexes - * @param $rows - * @param $fname string + * @param string $table + * @param array $uniqueIndexes Unused + * @param string|array $rows + * @param string $fname * @return bool|ResultWrapper */ function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { @@ -566,6 +618,8 @@ class DatabaseSqlite extends DatabaseBase { * Returns the size of a text field, or -1 for "unlimited" * In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though. * + * @param string $table + * @param string $field * @return int */ function textFieldSize( $table, $field ) { @@ -580,12 +634,13 @@ class DatabaseSqlite extends DatabaseBase { } /** - * @param $sqls - * @param $all + * @param string $sqls + * @param bool $all Whether to "UNION ALL" or not * @return string */ function unionQueries( $sqls, $all ) { $glue = $all ? ' UNION ALL ' : ' UNION '; + return implode( $glue, $sqls ); } @@ -611,7 +666,7 @@ class DatabaseSqlite extends DatabaseBase { } /** - * @return string wikitext of a link to the server software's web site + * @return string Wikitext of a link to the server software's web site */ public function getSoftwareLink() { return "[{{int:version-db-sqlite-url}} SQLite]"; @@ -622,6 +677,7 @@ class DatabaseSqlite extends DatabaseBase { */ function getServerVersion() { $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION ); + return $ver; } @@ -629,15 +685,17 @@ class DatabaseSqlite extends DatabaseBase { * @return string User-friendly database information */ public function getServerInfo() { - return wfMessage( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() )->text(); + return wfMessage( self::getFulltextSearchModule() + ? 'sqlite-has-fts' + : 'sqlite-no-fts', $this->getServerVersion() )->text(); } /** * Get information about a given field * Returns false if the field does not exist. * - * @param $table string - * @param $field string + * @param string $table + * @param string $field * @return SQLiteField|bool False on failure */ function fieldInfo( $table, $field ) { @@ -649,6 +707,7 @@ class DatabaseSqlite extends DatabaseBase { return new SQLiteField( $row, $tableName ); } } + return false; } @@ -685,15 +744,15 @@ class DatabaseSqlite extends DatabaseBase { } /** - * @param $s string + * @param string $s * @return string */ function strencode( $s ) { - return substr( $this->addQuotes( $s ), 1, - 1 ); + return substr( $this->addQuotes( $s ), 1, -1 ); } /** - * @param $b + * @param string $b * @return Blob */ function encodeBlob( $b ) { @@ -701,18 +760,19 @@ class DatabaseSqlite extends DatabaseBase { } /** - * @param $b Blob|string + * @param Blob|string $b * @return string */ function decodeBlob( $b ) { if ( $b instanceof Blob ) { $b = $b->fetch(); } + return $b; } /** - * @param $s Blob|string + * @param Blob|string $s * @return string */ function addQuotes( $s ) { @@ -741,6 +801,7 @@ class DatabaseSqlite extends DatabaseBase { if ( count( $params ) > 0 && is_array( $params[0] ) ) { $params = $params[0]; } + return parent::buildLike( $params ) . "ESCAPE '\' "; } @@ -753,16 +814,18 @@ class DatabaseSqlite extends DatabaseBase { /** * No-op version of deadlockLoop + * * @return mixed */ public function deadlockLoop( /*...*/ ) { $args = func_get_args(); $function = array_shift( $args ); + return call_user_func_array( $function, $args ); } /** - * @param $s string + * @param string $s * @return string */ protected function replaceVars( $s ) { @@ -777,7 +840,11 @@ class DatabaseSqlite extends DatabaseBase { // INT -> INTEGER $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s ); // floating point types -> REAL - $s = preg_replace( '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i', 'REAL', $s ); + $s = preg_replace( + '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i', + 'REAL', + $s + ); // varchar -> TEXT $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s ); // TEXT normalization @@ -803,37 +870,73 @@ class DatabaseSqlite extends DatabaseBase { $s = preg_replace( '/\(\d+\)/', '', $s ); // No FULLTEXT $s = preg_replace( '/\bfulltext\b/i', '', $s ); + } elseif ( preg_match( '/^\s*DROP INDEX/i', $s ) ) { + // DROP INDEX is database-wide, not table-specific, so no ON <table> clause. + $s = preg_replace( '/\sON\s+[^\s]*/i', '', $s ); + } elseif ( preg_match( '/^\s*INSERT IGNORE\b/i', $s ) ) { + // INSERT IGNORE --> INSERT OR IGNORE + $s = preg_replace( '/^\s*INSERT IGNORE\b/i', 'INSERT OR IGNORE', $s ); } + return $s; } + public function lock( $lockName, $method, $timeout = 5 ) { + global $wgSQLiteDataDir; + + if ( !is_dir( "$wgSQLiteDataDir/locks" ) ) { // create dir as needed + if ( !is_writable( $wgSQLiteDataDir ) || !mkdir( "$wgSQLiteDataDir/locks" ) ) { + throw new DBError( "Cannot create directory \"$wgSQLiteDataDir/locks\"." ); + } + } + + return $this->lockMgr->lock( array( $lockName ), LockManager::LOCK_EX, $timeout )->isOK(); + } + + public function unlock( $lockName, $method ) { + return $this->lockMgr->unlock( array( $lockName ), LockManager::LOCK_EX )->isOK(); + } + /** * Build a concatenation list to feed into a SQL query * - * @param $stringList array - * + * @param string[] $stringList * @return string */ function buildConcat( $stringList ) { return '(' . implode( ') || (', $stringList ) . ')'; } + public function buildGroupConcatField( + $delim, $table, $field, $conds = '', $join_conds = array() + ) { + $fld = "group_concat($field," . $this->addQuotes( $delim ) . ')'; + + return '(' . $this->selectSQLText( $table, $fld, $conds, null, array(), $join_conds ) . ')'; + } + /** * @throws MWException - * @param $oldName - * @param $newName - * @param $temporary bool - * @param $fname string + * @param string $oldName + * @param string $newName + * @param bool $temporary + * @param string $fname * @return bool|ResultWrapper */ 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 ); + $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name=" . + $this->addQuotes( $oldName ) . " AND type='table'", $fname ); $obj = $this->fetchObject( $res ); if ( !$obj ) { throw new MWException( "Couldn't retrieve structure for table $oldName" ); } $sql = $obj->sql; - $sql = preg_replace( '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/', $this->addIdentifierQuotes( $newName ), $sql, 1 ); + $sql = preg_replace( + '/(?<=\W)"?' . preg_quote( trim( $this->addIdentifierQuotes( $oldName ), '"' ) ) . '"?(?=\W)/', + $this->addIdentifierQuotes( $newName ), + $sql, + 1 + ); if ( $temporary ) { if ( preg_match( '/^\\s*CREATE\\s+VIRTUAL\\s+TABLE\b/i', $sql ) ) { wfDebug( "Table $oldName is virtual, can't create a temporary duplicate.\n" ); @@ -841,6 +944,7 @@ class DatabaseSqlite extends DatabaseBase { $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql ); } } + return $this->query( $sql, $fname ); } @@ -848,7 +952,7 @@ class DatabaseSqlite extends DatabaseBase { * List all tables on the database * * @param string $prefix Only show tables with this prefix, e.g. mw_ - * @param string $fname calling function name + * @param string $fname Calling function name * * @return array */ @@ -869,13 +973,11 @@ class DatabaseSqlite extends DatabaseBase { if ( strpos( $table, 'sqlite_' ) !== 0 ) { $endArray[] = $table; } - } } return $endArray; } - } // end DatabaseSqlite class /** @@ -895,6 +997,7 @@ class DatabaseSqliteStandalone extends DatabaseSqlite { */ class SQLiteField implements Field { private $info, $tableName; + function __construct( $info, $tableName ) { $this->info = $info; $this->tableName = $tableName; @@ -915,6 +1018,7 @@ class SQLiteField implements Field { return str_replace( "''", "'", $this->info->dflt_value ); } } + return $this->info->dflt_value; } @@ -928,5 +1032,4 @@ class SQLiteField implements Field { function type() { return $this->info->type; } - } // end SQLiteField diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php index de58bab6..c1e80d33 100644 --- a/includes/db/DatabaseUtility.php +++ b/includes/db/DatabaseUtility.php @@ -51,7 +51,8 @@ class DBObject { * This allows us to distinguish a blob from a normal string and an array of strings */ class Blob { - private $mData; + /** @var string */ + protected $mData; function __construct( $data ) { $this->mData = $data; @@ -97,13 +98,23 @@ interface Field { * @ingroup Database */ class ResultWrapper implements Iterator { - var $db, $result, $pos = 0, $currentRow = null; + /** @var resource */ + public $result; + + /** @var DatabaseBase */ + protected $db; + + /** @var int */ + protected $pos = 0; + + /** @var object|null */ + protected $currentRow = null; /** * Create a new result object from a result resource and a Database object * * @param DatabaseBase $database - * @param resource $result + * @param resource|ResultWrapper $result */ function __construct( $database, $result ) { $this->db = $database; @@ -118,7 +129,7 @@ class ResultWrapper implements Iterator { /** * Get the number of rows in a result object * - * @return integer + * @return int */ function numRows() { return $this->db->numRows( $this ); @@ -129,7 +140,7 @@ class ResultWrapper implements Iterator { * Fields can be retrieved with $row->fieldname, with fields acting like * member variables. * - * @return object + * @return stdClass * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchObject() { @@ -140,7 +151,7 @@ class ResultWrapper implements Iterator { * Fetch the next row from the given result object, in associative array * form. Fields are retrieved with $row['fieldname']. * - * @return Array + * @return array * @throws DBUnexpectedError Thrown if the database returns an error */ function fetchRow() { @@ -160,14 +171,14 @@ class ResultWrapper implements Iterator { * Change the position of the cursor in a result object. * See mysql_data_seek() * - * @param $row integer + * @param int $row */ function seek( $row ) { $this->db->dataSeek( $this, $row ); } - /********************* - * Iterator functions + /* + * ======= Iterator functions ======= * Note that using these in combination with the non-iterator functions * above may cause rows to be skipped or repeated. */ @@ -181,12 +192,13 @@ class ResultWrapper implements Iterator { } /** - * @return int + * @return stdClass|array|bool */ function current() { if ( is_null( $this->currentRow ) ) { $this->next(); } + return $this->currentRow; } @@ -198,11 +210,12 @@ class ResultWrapper implements Iterator { } /** - * @return int + * @return stdClass */ function next() { $this->pos++; $this->currentRow = $this->fetchObject(); + return $this->currentRow; } @@ -219,10 +232,17 @@ class ResultWrapper implements Iterator { * doesn't go anywhere near an actual database. */ class FakeResultWrapper extends ResultWrapper { - var $result = array(); - var $db = null; // And it's going to stay that way :D - var $pos = 0; - var $currentRow = null; + /** @var array */ + public $result = array(); + + /** @var null And it's going to stay that way :D */ + protected $db = null; + + /** @var int */ + protected $pos = 0; + + /** @var array|stdClass|bool */ + protected $currentRow = null; function __construct( $array ) { $this->result = $array; @@ -235,6 +255,9 @@ class FakeResultWrapper extends ResultWrapper { return count( $this->result ); } + /** + * @return array|bool + */ function fetchRow() { if ( $this->pos < count( $this->result ) ) { $this->currentRow = $this->result[$this->pos]; @@ -256,7 +279,10 @@ class FakeResultWrapper extends ResultWrapper { function free() { } - // Callers want to be able to access fields with $this->fieldName + /** + * Callers want to be able to access fields with $this->fieldName + * @return bool|stdClass + */ function fetchObject() { $this->fetchRow(); if ( $this->currentRow ) { @@ -271,16 +297,21 @@ class FakeResultWrapper extends ResultWrapper { $this->currentRow = null; } + /** + * @return bool|stdClass + */ function next() { return $this->fetchObject(); } } /** - * Used by DatabaseBase::buildLike() to represent characters that have special meaning in SQL LIKE clauses - * and thus need no escaping. Don't instantiate it manually, use DatabaseBase::anyChar() and anyString() instead. + * Used by DatabaseBase::buildLike() to represent characters that have special + * meaning in SQL LIKE clauses and thus need no escaping. Don't instantiate it + * manually, use DatabaseBase::anyChar() and anyString() instead. */ class LikeMatch { + /** @var string */ private $str; /** @@ -295,7 +326,7 @@ class LikeMatch { /** * Return the original stored string. * - * @return String + * @return string */ public function toString() { return $this->str; @@ -304,6 +335,8 @@ class LikeMatch { /** * An object representing a master or slave position in a replicated setup. + * + * The implementation details of this opaque type are up to the database subclass. */ interface DBMasterPos { } diff --git a/includes/db/IORMRow.php b/includes/db/IORMRow.php index 39411791..c66cddfd 100644 --- a/includes/db/IORMRow.php +++ b/includes/db/IORMRow.php @@ -32,7 +32,6 @@ */ interface IORMRow { - /** * Load the specified fields from the database. * @@ -40,8 +39,8 @@ interface IORMRow { * @deprecated since 1.22 * * @param array|null $fields - * @param boolean $override - * @param boolean $skipLoaded + * @param bool $override + * @param bool $skipLoaded * * @return bool Success indicator */ @@ -86,7 +85,7 @@ interface IORMRow { * * @since 1.20 * - * @return integer|null + * @return int|null */ public function getId(); @@ -95,7 +94,7 @@ interface IORMRow { * * @since 1.20 * - * @param integer|null $id + * @param int|null $id */ public function setId( $id ); @@ -106,7 +105,7 @@ interface IORMRow { * * @param string $name * - * @return boolean + * @return bool */ public function hasField( $name ); @@ -115,7 +114,7 @@ interface IORMRow { * * @since 1.20 * - * @return boolean + * @return bool */ public function hasIdField(); @@ -125,7 +124,7 @@ interface IORMRow { * @since 1.20 * * @param array $fields The fields to set - * @param boolean $override Override already set fields with the provided values? + * @param bool $override Override already set fields with the provided values? */ public function setFields( array $fields, $override = true ); @@ -136,7 +135,7 @@ interface IORMRow { * @since 1.20 * * @param null|array $fields - * @param boolean $incNullId + * @param bool $incNullId * * @return array */ @@ -148,7 +147,7 @@ interface IORMRow { * @since 1.20 * @deprecated since 1.22 * - * @param boolean $override + * @param bool $override */ public function loadDefaults( $override = true ); @@ -161,7 +160,7 @@ interface IORMRow { * @param string|null $functionName * @deprecated since 1.22 * - * @return boolean Success indicator + * @return bool Success indicator */ public function save( $functionName = null ); @@ -171,7 +170,7 @@ interface IORMRow { * @since 1.20 * @deprecated since 1.22 * - * @return boolean Success indicator + * @return bool Success indicator */ public function remove(); @@ -214,9 +213,9 @@ interface IORMRow { * @deprecated since 1.22 * * @param string $field - * @param integer $amount + * @param int $amount * - * @return boolean Success indicator + * @return bool Success indicator */ public function addToField( $field, $amount ); @@ -245,7 +244,7 @@ interface IORMRow { * @since 1.20 * @deprecated since 1.22 * - * @param boolean $update + * @param bool $update */ public function setUpdateSummaries( $update ); @@ -255,7 +254,7 @@ interface IORMRow { * @since 1.20 * @deprecated since 1.22 * - * @param boolean $summaryMode + * @param bool $summaryMode */ public function setSummaryMode( $summaryMode ); @@ -268,5 +267,4 @@ interface IORMRow { * @return IORMTable */ public function getTable(); - } diff --git a/includes/db/IORMTable.php b/includes/db/IORMTable.php index 36865655..4dc693ac 100644 --- a/includes/db/IORMTable.php +++ b/includes/db/IORMTable.php @@ -28,7 +28,6 @@ */ interface IORMTable { - /** * Returns the name of the database table objects of this type are stored in. * @@ -63,8 +62,9 @@ interface IORMTable { * * array * * blob * - * TODO: get rid of the id field. Every row instance needs to have - * one so this is just causing hassle at various locations by requiring an extra check for field name. + * @todo Get rid of the id field. Every row instance needs to have one so + * this is just causing hassle at various locations by requiring an extra + * check for field name. * * @since 1.20 * @@ -107,10 +107,10 @@ interface IORMTable { * @param string|null $functionName * * @return ORMResult The result set - * @throws DBQueryError if the query failed (even if the database was in ignoreErrors mode) + * @throws DBQueryError If the query failed (even if the database was in ignoreErrors mode) */ public function select( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ); + array $options = array(), $functionName = null ); /** * Selects the the specified fields of the records matching the provided @@ -123,10 +123,10 @@ interface IORMTable { * @param array $options * @param string|null $functionName * - * @return array of self + * @return array Array of self */ public function selectObjects( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ); + array $options = array(), $functionName = null ); /** * Do the actual select. @@ -139,10 +139,10 @@ interface IORMTable { * @param null|string $functionName * * @return ResultWrapper - * @throws DBQueryError if the query failed (even if the database was in ignoreErrors mode) + * @throws DBQueryError If the query failed (even if the database was in ignoreErrors mode) */ public function rawSelect( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ); + array $options = array(), $functionName = null ); /** * Selects the the specified fields of the records matching the provided @@ -161,13 +161,13 @@ interface IORMTable { * @param array|string|null $fields * @param array $conditions * @param array $options - * @param boolean $collapse Set to false to always return each result row as associative array. + * @param bool $collapse Set to false to always return each result row as associative array. * @param string|null $functionName * - * @return array of array + * @return array Array of array */ public function selectFields( $fields = null, array $conditions = array(), - array $options = array(), $collapse = true, $functionName = null ); + array $options = array(), $collapse = true, $functionName = null ); /** * Selects the the specified fields of the first matching record. @@ -183,7 +183,7 @@ interface IORMTable { * @return IORMRow|bool False on failure */ public function selectRow( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ); + array $options = array(), $functionName = null ); /** * Selects the the specified fields of the records matching the provided @@ -199,7 +199,7 @@ interface IORMTable { * @return ResultWrapper */ public function rawSelectRow( array $fields, array $conditions = array(), - array $options = array(), $functionName = null ); + array $options = array(), $functionName = null ); /** * Selects the the specified fields of the first record matching the provided @@ -213,13 +213,13 @@ interface IORMTable { * @param array|string|null $fields * @param array $conditions * @param array $options - * @param boolean $collapse Set to false to always return each result row as associative array. + * @param bool $collapse Set to false to always return each result row as associative array. * @param string|null $functionName * * @return mixed|array|bool False on failure */ public function selectFieldsRow( $fields = null, array $conditions = array(), - array $options = array(), $collapse = true, $functionName = null ); + array $options = array(), $collapse = true, $functionName = null ); /** * Returns if there is at least one record matching the provided conditions. @@ -229,7 +229,7 @@ interface IORMTable { * * @param array $conditions * - * @return boolean + * @return bool */ public function has( array $conditions = array() ); @@ -238,7 +238,7 @@ interface IORMTable { * * @since 1.21 * - * @return boolean + * @return bool */ public function exists(); @@ -254,7 +254,7 @@ interface IORMTable { * @param array $conditions * @param array $options * - * @return integer + * @return int */ public function count( array $conditions = array(), array $options = array() ); @@ -266,7 +266,7 @@ interface IORMTable { * @param array $conditions * @param string|null $functionName * - * @return boolean Success indicator + * @return bool Success indicator */ public function delete( array $conditions, $functionName = null ); @@ -275,8 +275,8 @@ interface IORMTable { * * @since 1.20 * - * @param boolean $requireParams - * @param boolean $setDefaults + * @param bool $requireParams + * @param bool $setDefaults * * @return array */ @@ -298,14 +298,14 @@ interface IORMTable { * * @since 1.20 * - * @return integer DB_ enum + * @return int DB_ enum */ public function getReadDb(); /** * Set the database type to use for read operations. * - * @param integer $db + * @param int $db * * @since 1.20 */ @@ -316,14 +316,16 @@ interface IORMTable { * * @since 1.20 * - * @return String|bool The target wiki, in a form that LBFactory understands (or false if the local wiki is used) + * @return string|bool The target wiki, in a form that LBFactory + * understands (or false if the local wiki is used) */ public function getTargetWiki(); /** * Set the ID of the any foreign wiki to use as a target for database operations * - * @param string|bool $wiki The target wiki, in a form that LBFactory understands (or false if the local wiki shall be used) + * @param string|bool $wiki The target wiki, in a form that LBFactory + * understands (or false if the local wiki shall be used) * * @since 1.20 */ @@ -370,7 +372,7 @@ interface IORMTable { * * @see LoadBalancer::reuseConnection * - * @param DatabaseBase $db the database + * @param DatabaseBase $db The database * * @since 1.20 */ @@ -386,7 +388,7 @@ interface IORMTable { * @param array $values * @param array $conditions * - * @return boolean Success indicator + * @return bool Success indicator */ public function update( array $values, array $conditions = array() ); @@ -419,7 +421,7 @@ interface IORMTable { * * @since 1.20 * - * @param array|string $fields + * @param array $fields * * @return array */ @@ -488,7 +490,7 @@ interface IORMTable { * @since 1.20 * * @param array $data - * @param boolean $loadDefaults + * @param bool $loadDefaults * * @return IORMRow */ @@ -510,8 +512,7 @@ interface IORMTable { * * @param string $name * - * @return boolean + * @return bool */ public function canHaveField( $name ); - } diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php index 16c43a00..73456e23 100644 --- a/includes/db/LBFactory.php +++ b/includes/db/LBFactory.php @@ -26,11 +26,8 @@ * @ingroup Database */ abstract class LBFactory { - - /** - * @var LBFactory - */ - static $instance; + /** @var LBFactory */ + protected static $instance; /** * Disables all access to the load balancer, will cause all database access @@ -38,7 +35,7 @@ abstract class LBFactory { */ public static function disableBackend() { global $wgLBFactoryConf; - self::$instance = new LBFactory_Fake( $wgLBFactoryConf ); + self::$instance = new LBFactoryFake( $wgLBFactoryConf ); } /** @@ -47,15 +44,47 @@ abstract class LBFactory { * @return LBFactory */ static function &singleton() { + global $wgLBFactoryConf; + if ( is_null( self::$instance ) ) { - global $wgLBFactoryConf; - $class = $wgLBFactoryConf['class']; + $class = self::getLBFactoryClass( $wgLBFactoryConf ); + self::$instance = new $class( $wgLBFactoryConf ); } + return self::$instance; } /** + * Returns the LBFactory class to use and the load balancer configuration. + * + * @param array $config (e.g. $wgLBFactoryConf) + * @return string Class name + */ + public static function getLBFactoryClass( array $config ) { + // For configuration backward compatibility after removing + // underscores from class names in MediaWiki 1.23. + $bcClasses = array( + 'LBFactory_Simple' => 'LBFactorySimple', + 'LBFactory_Single' => 'LBFactorySingle', + 'LBFactory_Multi' => 'LBFactoryMulti', + 'LBFactory_Fake' => 'LBFactoryFake', + ); + + $class = $config['class']; + + if ( isset( $bcClasses[$class] ) ) { + $class = $bcClasses[$class]; + wfDeprecated( + '$wgLBFactoryConf must be updated. See RELEASE-NOTES for details', + '1.23' + ); + } + + return $class; + } + + /** * Shut down, close connections and destroy the cached instance. */ static function destroyInstance() { @@ -69,7 +98,7 @@ abstract class LBFactory { /** * Set the instance to be the given object * - * @param $instance LBFactory + * @param LBFactory $instance */ static function setInstance( $instance ) { self::destroyInstance(); @@ -78,7 +107,7 @@ abstract class LBFactory { /** * Construct a factory based on a configuration array (typically from $wgLBFactoryConf) - * @param $conf + * @param array $conf */ abstract function __construct( $conf ); @@ -86,7 +115,7 @@ abstract class LBFactory { * Create a new load balancer object. The resulting object will be untracked, * not chronology-protected, and the caller is responsible for cleaning it up. * - * @param string $wiki wiki ID, or false for the current wiki + * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ abstract function newMainLB( $wiki = false ); @@ -94,7 +123,7 @@ abstract class LBFactory { /** * Get a cached (tracked) load balancer object. * - * @param string $wiki wiki ID, or false for the current wiki + * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ abstract function getMainLB( $wiki = false ); @@ -104,9 +133,8 @@ abstract class LBFactory { * untracked, not chronology-protected, and the caller is responsible for * cleaning it up. * - * @param string $cluster external storage cluster, or false for core - * @param string $wiki wiki ID, or false for the current wiki - * + * @param string $cluster External storage cluster, or false for core + * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ abstract function newExternalLB( $cluster, $wiki = false ); @@ -114,9 +142,8 @@ abstract class LBFactory { /** * Get a cached (tracked) load balancer for external storage * - * @param string $cluster external storage cluster, or false for core - * @param string $wiki wiki ID, or false for the current wiki - * + * @param string $cluster External storage cluster, or false for core + * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ abstract function &getExternalLB( $cluster, $wiki = false ); @@ -125,7 +152,8 @@ abstract class LBFactory { * Execute a function for each tracked load balancer * The callback is called with the load balancer as the first parameter, * and $params passed as the subsequent parameters. - * @param $callback string|array + * + * @param callable $callback * @param array $params */ abstract function forEachLB( $callback, $params = array() ); @@ -139,8 +167,9 @@ abstract class LBFactory { /** * Call a method of each tracked load balancer - * @param $methodName string - * @param $args array + * + * @param string $methodName + * @param array $args */ function forEachLBCallMethod( $methodName, $args = array() ) { $this->forEachLB( array( $this, 'callMethod' ), array( $methodName, $args ) ); @@ -148,9 +177,9 @@ abstract class LBFactory { /** * Private helper for forEachLBCallMethod - * @param $loadBalancer - * @param $methodName string - * @param $args + * @param LoadBalancer $loadBalancer + * @param string $methodName + * @param array $args */ function callMethod( $loadBalancer, $methodName, $args ) { call_user_func_array( array( $loadBalancer, $methodName ), $args ); @@ -162,39 +191,62 @@ abstract class LBFactory { function commitMasterChanges() { $this->forEachLBCallMethod( 'commitMasterChanges' ); } + + /** + * Rollback changes on all master connections + * @since 1.23 + */ + function rollbackMasterChanges() { + $this->forEachLBCallMethod( 'rollbackMasterChanges' ); + } + + /** + * Detemine if any master connection has pending changes. + * @since 1.23 + * @return bool + */ + function hasMasterChanges() { + $ret = false; + $this->forEachLB( function ( $lb ) use ( &$ret ) { + $ret = $ret || $lb->hasMasterChanges(); + } ); + return $ret; + } } /** * A simple single-master LBFactory that gets its configuration from the b/c globals */ -class LBFactory_Simple extends LBFactory { +class LBFactorySimple extends LBFactory { + /** @var LoadBalancer */ + protected $mainLB; - /** - * @var LoadBalancer - */ - var $mainLB; - var $extLBs = array(); + /** @var LoadBalancer[] */ + protected $extLBs = array(); - # Chronology protector - var $chronProt; + /** @var ChronologyProtector */ + protected $chronProt; function __construct( $conf ) { $this->chronProt = new ChronologyProtector; } /** - * @param $wiki + * @param bool|string $wiki * @return LoadBalancer */ function newMainLB( $wiki = false ) { - global $wgDBservers, $wgMasterWaitTimeout; + global $wgDBservers; if ( $wgDBservers ) { $servers = $wgDBservers; } else { global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql; global $wgDBssl, $wgDBcompress; - $flags = ( $wgDebugDumpSql ? DBO_DEBUG : 0 ) | DBO_DEFAULT; + $flags = DBO_DEFAULT; + if ( $wgDebugDumpSql ) { + $flags |= DBO_DEBUG; + } if ( $wgDBssl ) { $flags |= DBO_SSL; } @@ -210,17 +262,16 @@ class LBFactory_Simple extends LBFactory { 'type' => $wgDBtype, 'load' => 1, 'flags' => $flags - )); + ) ); } return new LoadBalancer( array( 'servers' => $servers, - 'masterWaitTimeout' => $wgMasterWaitTimeout - )); + ) ); } /** - * @param $wiki + * @param bool|string $wiki * @return LoadBalancer */ function getMainLB( $wiki = false ) { @@ -229,13 +280,14 @@ class LBFactory_Simple extends LBFactory { $this->mainLB->parentInfo( array( 'id' => 'main' ) ); $this->chronProt->initLB( $this->mainLB ); } + return $this->mainLB; } /** * @throws MWException - * @param $cluster - * @param $wiki + * @param string $cluster + * @param bool|string $wiki * @return LoadBalancer */ function newExternalLB( $cluster, $wiki = false ) { @@ -243,14 +295,15 @@ class LBFactory_Simple extends LBFactory { if ( !isset( $wgExternalServers[$cluster] ) ) { throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" ); } + return new LoadBalancer( array( 'servers' => $wgExternalServers[$cluster] - )); + ) ); } /** - * @param $cluster - * @param $wiki + * @param string $cluster + * @param bool|string $wiki * @return array */ function &getExternalLB( $cluster, $wiki = false ) { @@ -259,6 +312,7 @@ class LBFactory_Simple extends LBFactory { $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) ); $this->chronProt->initLB( $this->extLBs[$cluster] ); } + return $this->extLBs[$cluster]; } @@ -266,8 +320,9 @@ class LBFactory_Simple extends LBFactory { * Execute a function for each tracked load balancer * The callback is called with the load balancer as the first parameter, * and $params passed as the subsequent parameters. - * @param $callback - * @param $params array + * + * @param callable $callback + * @param array $params */ function forEachLB( $callback, $params = array() ) { if ( isset( $this->mainLB ) ) { @@ -296,7 +351,7 @@ class LBFactory_Simple extends LBFactory { * Call LBFactory::disableBackend() to start using this, and * LBFactory::enableBackend() to return to normal behavior */ -class LBFactory_Fake extends LBFactory { +class LBFactoryFake extends LBFactory { function __construct( $conf ) { } @@ -325,6 +380,7 @@ class LBFactory_Fake extends LBFactory { */ class DBAccessError extends MWException { function __construct() { - parent::__construct( "Mediawiki tried to access the database via wfGetDB(). This is not allowed." ); + parent::__construct( "Mediawiki tried to access the database via wfGetDB(). " . + "This is not allowed." ); } } diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactoryMulti.php index 3043946a..bac96523 100644 --- a/includes/db/LBFactory_Multi.php +++ b/includes/db/LBFactoryMulti.php @@ -26,49 +26,130 @@ * Ignores the old configuration globals * * Configuration: - * sectionsByDB A map of database names to section names + * sectionsByDB A map of database names to section names. * - * sectionLoads A 2-d map. For each section, gives a map of server names to load ratios. - * For example: array( 'section1' => array( 'db1' => 100, 'db2' => 100 ) ) + * sectionLoads A 2-d map. For each section, gives a map of server names to + * load ratios. For example: + * array( + * 'section1' => array( + * 'db1' => 100, + * 'db2' => 100 + * ) + * ) * - * serverTemplate A server info associative array as documented for $wgDBservers. The host, - * hostName and load entries will be overridden. + * serverTemplate A server info associative array as documented for $wgDBservers. + * The host, hostName and load entries will be overridden. * - * groupLoadsBySection A 3-d map giving server load ratios for each section and group. For example: - * array( 'section1' => array( 'group1' => array( 'db1' => 100, 'db2' => 100 ) ) ) + * groupLoadsBySection A 3-d map giving server load ratios for each section and group. + * For example: + * array( + * 'section1' => array( + * 'group1' => array( + * 'db1' => 100, + * 'db2' => 100 + * ) + * ) + * ) * * groupLoadsByDB A 3-d map giving server load ratios by DB name. * * hostsByName A map of hostname to IP address. * - * externalLoads A map of external storage cluster name to server load map + * externalLoads A map of external storage cluster name to server load map. * - * externalTemplateOverrides A set of server info keys overriding serverTemplate for external storage + * externalTemplateOverrides A set of server info keys overriding serverTemplate for external + * storage. * - * templateOverridesByServer A 2-d map overriding serverTemplate and externalTemplateOverrides on a - * server-by-server basis. Applies to both core and external storage. + * templateOverridesByServer A 2-d map overriding serverTemplate and + * externalTemplateOverrides on a server-by-server basis. Applies + * to both core and external storage. * - * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster + * templateOverridesByCluster A 2-d map overriding the server info by external storage cluster. * * masterTemplateOverrides An override array for all master servers. * - * readOnlyBySection A map of section name to read-only message. Missing or false for read/write. + * readOnlyBySection A map of section name to read-only message. + * Missing or false for read/write. * * @ingroup Database */ -class LBFactory_Multi extends LBFactory { +class LBFactoryMulti extends LBFactory { // Required settings - var $sectionsByDB, $sectionLoads, $serverTemplate; + + /** @var array A map of database names to section names */ + protected $sectionsByDB; + + /** + * @var array A 2-d map. For each section, gives a map of server names to + * load ratios + */ + protected $sectionLoads; + + /** + * @var array A server info associative array as documented for + * $wgDBservers. The host, hostName and load entries will be + * overridden + */ + protected $serverTemplate; + // Optional settings - var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array(); - var $externalLoads = array(), $externalTemplateOverrides, $templateOverridesByServer; - var $templateOverridesByCluster, $masterTemplateOverrides, $readOnlyBySection = array(); + + /** @var array A 3-d map giving server load ratios for each section and group */ + protected $groupLoadsBySection = array(); + + /** @var array A 3-d map giving server load ratios by DB name */ + protected $groupLoadsByDB = array(); + + /** @var array A map of hostname to IP address */ + protected $hostsByName = array(); + + /** @var array A map of external storage cluster name to server load map */ + protected $externalLoads = array(); + + /** + * @var array A set of server info keys overriding serverTemplate for + * external storage + */ + protected $externalTemplateOverrides; + + /** + * @var array A 2-d map overriding serverTemplate and + * externalTemplateOverrides on a server-by-server basis. Applies to both + * core and external storage + */ + protected $templateOverridesByServer; + + /** @var array A 2-d map overriding the server info by external storage cluster */ + protected $templateOverridesByCluster; + + /** @var array An override array for all master servers */ + protected $masterTemplateOverrides; + + /** + * @var array|bool A map of section name to read-only message. Missing or + * false for read/write + */ + protected $readOnlyBySection = array(); + // Other stuff - var $conf, $mainLBs = array(), $extLBs = array(); - var $lastWiki, $lastSection; + + /** @var array Load balancer factory configuration */ + protected $conf; + + /** @var LoadBalancer[] */ + protected $mainLBs = array(); + + /** @var LoadBalancer[] */ + protected $extLBs = array(); + + /** @var string */ + protected $lastWiki; + + /** @var string */ + protected $lastSection; /** - * @param $conf array + * @param array $conf * @throws MWException */ function __construct( $conf ) { @@ -102,7 +183,7 @@ class LBFactory_Multi extends LBFactory { } /** - * @param $wiki bool|string + * @param bool|string $wiki * @return string */ function getSectionForWiki( $wiki = false ) { @@ -117,11 +198,12 @@ class LBFactory_Multi extends LBFactory { } $this->lastSection = $section; $this->lastWiki = $wiki; + return $section; } /** - * @param $wiki bool|string + * @param bool|string $wiki * @return LoadBalancer */ function newMainLB( $wiki = false ) { @@ -131,14 +213,20 @@ class LBFactory_Multi extends LBFactory { if ( isset( $this->groupLoadsByDB[$dbName] ) ) { $groupLoads = $this->groupLoadsByDB[$dbName]; } + if ( isset( $this->groupLoadsBySection[$section] ) ) { $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] ); } - return $this->newLoadBalancer( $this->serverTemplate, $this->sectionLoads[$section], $groupLoads ); + + return $this->newLoadBalancer( + $this->serverTemplate, + $this->sectionLoads[$section], + $groupLoads + ); } /** - * @param $wiki bool|string + * @param bool|string $wiki * @return LoadBalancer */ function getMainLB( $wiki = false ) { @@ -149,12 +237,13 @@ class LBFactory_Multi extends LBFactory { $this->chronProt->initLB( $lb ); $this->mainLBs[$section] = $lb; } + return $this->mainLBs[$section]; } /** * @param string $cluster - * @param bool $wiki + * @param bool|string $wiki * @throws MWException * @return LoadBalancer */ @@ -169,12 +258,13 @@ class LBFactory_Multi extends LBFactory { if ( isset( $this->templateOverridesByCluster[$cluster] ) ) { $template = $this->templateOverridesByCluster[$cluster] + $template; } + return $this->newLoadBalancer( $template, $this->externalLoads[$cluster], array() ); } /** - * @param $cluster - * @param $wiki + * @param string $cluster External storage cluster, or false for core + * @param bool|string $wiki Wiki ID, or false for the current wiki * @return LoadBalancer */ function &getExternalLB( $cluster, $wiki = false ) { @@ -183,33 +273,33 @@ class LBFactory_Multi extends LBFactory { $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) ); $this->chronProt->initLB( $this->extLBs[$cluster] ); } + return $this->extLBs[$cluster]; } /** * Make a new load balancer object based on template and load array * - * @param $template - * @param $loads array - * @param $groupLoads + * @param array $template + * @param array $loads + * @param array $groupLoads * @return LoadBalancer */ function newLoadBalancer( $template, $loads, $groupLoads ) { - global $wgMasterWaitTimeout; $servers = $this->makeServerArray( $template, $loads, $groupLoads ); $lb = new LoadBalancer( array( 'servers' => $servers, - 'masterWaitTimeout' => $wgMasterWaitTimeout - )); + ) ); + return $lb; } /** * Make a server array as expected by LoadBalancer::__construct, using a template and load array * - * @param $template - * @param $loads array - * @param $groupLoads + * @param array $template + * @param array $loads + * @param array $groupLoads * @return array */ function makeServerArray( $template, $loads, $groupLoads ) { @@ -245,12 +335,13 @@ class LBFactory_Multi extends LBFactory { $serverInfo['load'] = $load; $servers[] = $serverInfo; } + return $servers; } /** * Take a group load array indexed by group then server, and reindex it by server then group - * @param $groupLoads + * @param array $groupLoads * @return array */ function reindexGroupLoads( $groupLoads ) { @@ -260,17 +351,19 @@ class LBFactory_Multi extends LBFactory { $reindexed[$server][$group] = $load; } } + return $reindexed; } /** * Get the database name and prefix based on the wiki ID - * @param $wiki bool + * @param bool|string $wiki * @return array */ function getDBNameAndPrefix( $wiki = false ) { if ( $wiki === false ) { global $wgDBname, $wgDBprefix; + return array( $wgDBname, $wgDBprefix ); } else { return wfSplitWikiID( $wiki ); @@ -281,8 +374,8 @@ class LBFactory_Multi extends LBFactory { * Execute a function for each tracked load balancer * The callback is called with the load balancer as the first parameter, * and $params passed as the subsequent parameters. - * @param $callback - * @param $params array + * @param callable $callback + * @param array $params */ function forEachLB( $callback, $params = array() ) { foreach ( $this->mainLBs as $lb ) { diff --git a/includes/db/LBFactory_Single.php b/includes/db/LBFactorySingle.php index 7dca06d7..3a4d829b 100644 --- a/includes/db/LBFactory_Single.php +++ b/includes/db/LBFactorySingle.php @@ -24,7 +24,8 @@ /** * An LBFactory class that always returns a single database object. */ -class LBFactory_Single extends LBFactory { +class LBFactorySingle extends LBFactory { + /** @var LoadBalancerSingle */ protected $lb; /** @@ -32,50 +33,46 @@ class LBFactory_Single extends LBFactory { * - connection: The DatabaseBase connection object */ function __construct( $conf ) { - $this->lb = new LoadBalancer_Single( $conf ); + $this->lb = new LoadBalancerSingle( $conf ); } /** - * @param $wiki bool|string - * - * @return LoadBalancer_Single + * @param bool|string $wiki + * @return LoadBalancerSingle */ function newMainLB( $wiki = false ) { return $this->lb; } /** - * @param $wiki bool|string - * - * @return LoadBalancer_Single + * @param bool|string $wiki + * @return LoadBalancerSingle */ function getMainLB( $wiki = false ) { return $this->lb; } /** - * @param $cluster - * @param $wiki bool|string - * - * @return LoadBalancer_Single + * @param string $cluster External storage cluster, or false for core + * @param bool|string $wiki Wiki ID, or false for the current wiki + * @return LoadBalancerSingle */ function newExternalLB( $cluster, $wiki = false ) { return $this->lb; } /** - * @param $cluster - * @param $wiki bool|string - * - * @return LoadBalancer_Single + * @param string $cluster External storage cluster, or false for core + * @param bool|string $wiki Wiki ID, or false for the current wiki + * @return LoadBalancerSingle */ function &getExternalLB( $cluster, $wiki = false ) { return $this->lb; } /** - * @param $callback string|array - * @param $params array + * @param string|callable $callback + * @param array $params */ function forEachLB( $callback, $params = array() ) { call_user_func_array( $callback, array_merge( array( $this->lb ), $params ) ); @@ -83,17 +80,14 @@ class LBFactory_Single extends LBFactory { } /** - * Helper class for LBFactory_Single. + * Helper class for LBFactorySingle. */ -class LoadBalancer_Single extends LoadBalancer { - - /** - * @var DatabaseBase - */ - var $db; +class LoadBalancerSingle extends LoadBalancer { + /** @var DatabaseBase */ + protected $db; /** - * @param $params array + * @param array $params */ function __construct( $params ) { $this->db = $params['connection']; @@ -107,8 +101,8 @@ class LoadBalancer_Single extends LoadBalancer { /** * - * @param $server string - * @param $dbNameOverride bool + * @param string $server + * @param bool $dbNameOverride * * @return DatabaseBase */ diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index 857109db..e517a025 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -28,19 +28,43 @@ * @ingroup Database */ class LoadBalancer { - private $mServers, $mConns, $mLoads, $mGroupLoads; + /** @var array Map of (server index => server config array) */ + private $mServers; + /** @var array Map of (local/foreignUsed/foreignFree => server index => DatabaseBase array) */ + private $mConns; + /** @var array Map of (server index => weight) */ + private $mLoads; + /** @var array Map of (group => server index => weight) */ + private $mGroupLoads; + /** @var bool Whether to disregard slave lag as a factor in slave selection */ + private $mAllowLagged; + /** @var integer Seconds to spend waiting on slave lag to resolve */ + private $mWaitTimeout; + + /** @var array LBFactory information */ + private $mParentInfo; + /** @var string The LoadMonitor subclass name */ + private $mLoadMonitorClass; + /** @var LoadMonitor */ + private $mLoadMonitor; + + /** @var bool|DatabaseBase Database connection that caused a problem */ private $mErrorConnection; - private $mReadIndex, $mAllowLagged; - private $mWaitForPos, $mWaitTimeout; - private $mLaggedSlaveMode, $mLastError = 'Unknown error'; - private $mParentInfo, $mLagTimes; - private $mLoadMonitorClass, $mLoadMonitor; + /** @var integer The generic (not query grouped) slave index (of $mServers) */ + private $mReadIndex; + /** @var bool|DBMasterPos False if not set */ + private $mWaitForPos; + /** @var bool Whether the generic reader fell back to a lagged slave */ + private $mLaggedSlaveMode; + /** @var string The last DB selection or connection error */ + private $mLastError = 'Unknown error'; + /** @var array Process cache of LoadMonitor::getLagTimes() */ + private $mLagTimes; /** - * @param array $params with keys: - * servers Required. Array of server info structures. - * masterWaitTimeout Replication lag wait timeout - * loadMonitor Name of a class used to fetch server lag and load. + * @param array $params Array with keys: + * servers Required. Array of server info structures. + * loadMonitor Name of a class used to fetch server lag and load. * @throws MWException */ function __construct( $params ) { @@ -48,12 +72,7 @@ class LoadBalancer { throw new MWException( __CLASS__ . ': missing servers parameter' ); } $this->mServers = $params['servers']; - - if ( isset( $params['waitTimeout'] ) ) { - $this->mWaitTimeout = $params['waitTimeout']; - } else { - $this->mWaitTimeout = 10; - } + $this->mWaitTimeout = 10; $this->mReadIndex = -1; $this->mWriteIndex = -1; @@ -72,9 +91,9 @@ class LoadBalancer { } else { $master = reset( $params['servers'] ); if ( isset( $master['type'] ) && $master['type'] === 'mysql' ) { - $this->mLoadMonitorClass = 'LoadMonitor_MySQL'; + $this->mLoadMonitorClass = 'LoadMonitorMySQL'; } else { - $this->mLoadMonitorClass = 'LoadMonitor_Null'; + $this->mLoadMonitorClass = 'LoadMonitorNull'; } } @@ -101,13 +120,14 @@ class LoadBalancer { $class = $this->mLoadMonitorClass; $this->mLoadMonitor = new $class( $this ); } + return $this->mLoadMonitor; } /** * Get or set arbitrary data used by the parent object, usually an LBFactory - * @param $x - * @return Mixed + * @param mixed $x + * @return mixed */ function parentInfo( $x = null ) { return wfSetVar( $this->mParentInfo, $x ); @@ -119,8 +139,7 @@ class LoadBalancer { * * @deprecated since 1.21, use ArrayUtils::pickRandom() * - * @param $weights array - * + * @param array $weights * @return bool|int|string */ function pickRandom( $weights ) { @@ -128,8 +147,8 @@ class LoadBalancer { } /** - * @param $loads array - * @param $wiki bool + * @param array $loads + * @param bool|string $wiki Wiki to get non-lagged for * @return bool|int|string */ function getRandomNonLagged( $loads, $wiki = false ) { @@ -138,10 +157,10 @@ class LoadBalancer { foreach ( $lags as $i => $lag ) { if ( $i != 0 ) { if ( $lag === false ) { - wfDebugLog( 'replication', "Server #$i is not replicating\n" ); + wfDebugLog( 'replication', "Server #$i is not replicating" ); unset( $loads[$i] ); } elseif ( isset( $this->mServers[$i]['max lag'] ) && $lag > $this->mServers[$i]['max lag'] ) { - wfDebugLog( 'replication', "Server #$i is excessively lagged ($lag seconds)\n" ); + wfDebugLog( 'replication', "Server #$i is excessively lagged ($lag seconds)" ); unset( $loads[$i] ); } } @@ -176,13 +195,13 @@ class LoadBalancer { * always return a consistent index during a given invocation * * Side effect: opens connections to databases - * @param $group bool - * @param $wiki bool + * @param bool|string $group + * @param bool|string $wiki * @throws MWException * @return bool|int|string */ function getReaderIndex( $group = false, $wiki = false ) { - global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype; + global $wgReadOnly, $wgDBtype; # @todo FIXME: For now, only go through all this for mysql databases if ( $wgDBtype != 'mysql' ) { @@ -192,17 +211,12 @@ class LoadBalancer { if ( count( $this->mServers ) == 1 ) { # Skip the load balancing if there's only one server return 0; - } elseif ( $group === false and $this->mReadIndex >= 0 ) { + } elseif ( $group === false && $this->mReadIndex >= 0 ) { # Shortcut if generic reader exists already return $this->mReadIndex; } - wfProfileIn( __METHOD__ ); - - $totalElapsed = 0; - - # convert from seconds to microseconds - $timeout = $wgDBClusterTimeout * 1e6; + $section = new ProfileSection( __METHOD__ ); # Find the relevant load array if ( $group !== false ) { @@ -211,15 +225,14 @@ class LoadBalancer { } else { # No loads for this group, return false and the caller can use some other group wfDebug( __METHOD__ . ": no loads for group $group\n" ); - wfProfileOut( __METHOD__ ); + return false; } } else { $nonErrorLoads = $this->mLoads; } - if ( !$nonErrorLoads ) { - wfProfileOut( __METHOD__ ); + if ( !count( $nonErrorLoads ) ) { throw new MWException( "Empty server array given to LoadBalancer" ); } @@ -228,92 +241,60 @@ class LoadBalancer { $laggedSlaveMode = false; + # No server found yet + $i = false; # First try quickly looking through the available servers for a server that # meets our criteria - do { - $totalThreadsConnected = 0; - $overloadedServers = 0; - $currentLoads = $nonErrorLoads; - while ( count( $currentLoads ) ) { - if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) { + $currentLoads = $nonErrorLoads; + while ( count( $currentLoads ) ) { + if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) { + $i = ArrayUtils::pickRandom( $currentLoads ); + } else { + $i = $this->getRandomNonLagged( $currentLoads, $wiki ); + if ( $i === false && count( $currentLoads ) != 0 ) { + # All slaves lagged. Switch to read-only mode + wfDebugLog( 'replication', "All slaves lagged. Switch to read-only mode" ); + $wgReadOnly = 'The database has been automatically locked ' . + 'while the slave database servers catch up to the master'; $i = ArrayUtils::pickRandom( $currentLoads ); - } else { - $i = $this->getRandomNonLagged( $currentLoads, $wiki ); - if ( $i === false && count( $currentLoads ) != 0 ) { - # All slaves lagged. Switch to read-only mode - 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 = ArrayUtils::pickRandom( $currentLoads ); - $laggedSlaveMode = true; - } - } - - if ( $i === false ) { - # pickRandom() returned false - # This is permanent and means the configuration or the load monitor - # wants us to return false. - wfDebugLog( 'connect', __METHOD__ . ": pickRandom() returned false\n" ); - wfProfileOut( __METHOD__ ); - return false; + $laggedSlaveMode = true; } + } - wfDebugLog( 'connect', __METHOD__ . ": Using reader #$i: {$this->mServers[$i]['host']}...\n" ); - $conn = $this->openConnection( $i, $wiki ); - - if ( !$conn ) { - wfDebugLog( 'connect', __METHOD__ . ": Failed connecting to $i/$wiki\n" ); - unset( $nonErrorLoads[$i] ); - unset( $currentLoads[$i] ); - continue; - } - - // Perform post-connection backoff - $threshold = isset( $this->mServers[$i]['max threads'] ) - ? $this->mServers[$i]['max threads'] : false; - $backoff = $this->getLoadMonitor()->postConnectionBackoff( $conn, $threshold ); - - // Decrement reference counter, we are finished with this connection. - // It will be incremented for the caller later. - if ( $wiki !== false ) { - $this->reuseConnection( $conn ); - } + if ( $i === false ) { + # pickRandom() returned false + # This is permanent and means the configuration or the load monitor + # wants us to return false. + wfDebugLog( 'connect', __METHOD__ . ": pickRandom() returned false" ); - if ( $backoff ) { - # Post-connection overload, don't use this server for now - $totalThreadsConnected += $backoff; - $overloadedServers++; - unset( $currentLoads[$i] ); - } else { - # Return this server - break 2; - } + return false; } - # No server found yet - $i = false; + wfDebugLog( 'connect', __METHOD__ . + ": Using reader #$i: {$this->mServers[$i]['host']}..." ); - # If all servers were down, quit now - if ( !count( $nonErrorLoads ) ) { - wfDebugLog( 'connect', "All servers down\n" ); - break; + $conn = $this->openConnection( $i, $wiki ); + if ( !$conn ) { + wfDebugLog( 'connect', __METHOD__ . ": Failed connecting to $i/$wiki" ); + unset( $nonErrorLoads[$i] ); + unset( $currentLoads[$i] ); + $i = false; + continue; } - # Some servers must have been overloaded - if ( $overloadedServers == 0 ) { - throw new MWException( __METHOD__ . ": unexpectedly found no overloaded servers" ); + // Decrement reference counter, we are finished with this connection. + // It will be incremented for the caller later. + if ( $wiki !== false ) { + $this->reuseConnection( $conn ); } - # Back off for a while - # Scale the sleep time by the number of connected threads, to produce a - # roughly constant global poll rate - $avgThreads = $totalThreadsConnected / $overloadedServers; - $totalElapsed += $this->sleep( $wgDBAvgStatusPoll * $avgThreads ); - } while ( $totalElapsed < $timeout ); - - if ( $totalElapsed >= $timeout ) { - wfDebugLog( 'connect', "All servers busy\n" ); - $this->mErrorConnection = false; - $this->mLastError = 'All servers busy'; + + # Return this server + break; + } + + # If all servers were down, quit now + if ( !count( $nonErrorLoads ) ) { + wfDebugLog( 'connect', "All servers down" ); } if ( $i !== false ) { @@ -324,17 +305,17 @@ class LoadBalancer { $this->mServers[$i]['slave pos'] = $conn->getSlavePos(); } } - if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $i !== false ) { + if ( $this->mReadIndex <= 0 && $this->mLoads[$i] > 0 && $group !== false ) { $this->mReadIndex = $i; } } - wfProfileOut( __METHOD__ ); + return $i; } /** * Wait for a specified number of microseconds, and return the period waited - * @param $t int + * @param int $t * @return int */ function sleep( $t ) { @@ -342,6 +323,7 @@ class LoadBalancer { wfDebug( __METHOD__ . ": waiting $t us\n" ); usleep( $t ); wfProfileOut( __METHOD__ ); + return $t; } @@ -349,7 +331,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 DBMasterPos + * @param DBMasterPos $pos */ public function waitFor( $pos ) { wfProfileIn( __METHOD__ ); @@ -367,22 +349,31 @@ class LoadBalancer { /** * Set the master wait position and wait for ALL slaves to catch up to it - * @param $pos DBMasterPos + * @param DBMasterPos $pos + * @param int $timeout Max seconds to wait; default is mWaitTimeout + * @return bool Success (able to connect and no timeouts reached) */ - public function waitForAll( $pos ) { + public function waitForAll( $pos, $timeout = null ) { wfProfileIn( __METHOD__ ); $this->mWaitForPos = $pos; - for ( $i = 1; $i < count( $this->mServers ); $i++ ) { - $this->doWait( $i, true ); + $serverCount = count( $this->mServers ); + + $ok = true; + for ( $i = 1; $i < $serverCount; $i++ ) { + if ( $this->mLoads[$i] > 0 ) { + $ok = $this->doWait( $i, true, $timeout ) && $ok; + } } wfProfileOut( __METHOD__ ); + + return $ok; } /** * Get any open connection to a given server index, local or foreign * Returns false if there is no connection open * - * @param $i int + * @param int $i * @return DatabaseBase|bool False on failure */ function getAnyOpenConnection( $i ) { @@ -391,40 +382,47 @@ class LoadBalancer { return reset( $conns[$i] ); } } + return false; } /** * Wait for a given slave to catch up to the master pos stored in $this - * @param $index - * @param $open bool + * @param int $index Server index + * @param bool $open Check the server even if a new connection has to be made + * @param int $timeout Max seconds to wait; default is mWaitTimeout * @return bool */ - protected function doWait( $index, $open = false ) { + protected function doWait( $index, $open = false, $timeout = null ) { # Find a connection to wait on $conn = $this->getAnyOpenConnection( $index ); if ( !$conn ) { if ( !$open ) { wfDebug( __METHOD__ . ": no connection open\n" ); + return false; } else { $conn = $this->openConnection( $index, '' ); if ( !$conn ) { wfDebug( __METHOD__ . ": failed to open connection\n" ); + return false; } } } wfDebug( __METHOD__ . ": Waiting for slave #$index to catch up...\n" ); - $result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout ); + $timeout = $timeout ?: $this->mWaitTimeout; + $result = $conn->masterPosWait( $this->mWaitForPos, $timeout ); if ( $result == -1 || is_null( $result ) ) { # Timed out waiting for slave, use master instead wfDebug( __METHOD__ . ": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" ); + return false; } else { wfDebug( __METHOD__ . ": Done\n" ); + return true; } } @@ -433,8 +431,8 @@ class LoadBalancer { * Get a connection by index * This is the main entry point for this class. * - * @param $i Integer: server index - * @param array $groups query groups + * @param int $i Server index + * @param array $groups Query groups * @param bool|string $wiki Wiki ID * * @throws MWException @@ -443,12 +441,10 @@ class LoadBalancer { public function &getConnection( $i, $groups = array(), $wiki = false ) { wfProfileIn( __METHOD__ ); - if ( $i == DB_LAST ) { + if ( $i === null || $i === false ) { 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' ); + throw new MWException( 'Attempt to call ' . __METHOD__ . + ' with invalid server index' ); } if ( $wiki === wfWikiID() ) { @@ -485,6 +481,7 @@ class LoadBalancer { if ( $i === false ) { $this->mLastError = 'No working slave server: ' . $this->mLastError; wfProfileOut( __METHOD__ ); + return $this->reportConnectionError(); } } @@ -493,10 +490,12 @@ class LoadBalancer { $conn = $this->openConnection( $i, $wiki ); if ( !$conn ) { wfProfileOut( __METHOD__ ); + return $this->reportConnectionError(); } wfProfileOut( __METHOD__ ); + return $conn; } @@ -511,15 +510,9 @@ class LoadBalancer { public function reuseConnection( $conn ) { $serverIndex = $conn->getLBInfo( 'serverIndex' ); $refCount = $conn->getLBInfo( 'foreignPoolRefCount' ); - $dbName = $conn->getDBname(); - $prefix = $conn->tablePrefix(); - if ( strval( $prefix ) !== '' ) { - $wiki = "$dbName-$prefix"; - } else { - $wiki = $dbName; - } if ( $serverIndex === null || $refCount === null ) { wfDebug( __METHOD__ . ": this connection was not opened as a foreign connection\n" ); + /** * This can happen in code like: * foreach ( $dbs as $db ) { @@ -530,10 +523,20 @@ class LoadBalancer { * When a connection to the local DB is opened in this way, reuseConnection() * should be ignored */ + return; } + + $dbName = $conn->getDBname(); + $prefix = $conn->tablePrefix(); + if ( strval( $prefix ) !== '' ) { + $wiki = "$dbName-$prefix"; + } else { + $wiki = $dbName; + } if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) { - throw new MWException( __METHOD__ . ": connection not found, has the connection been freed already?" ); + throw new MWException( __METHOD__ . ": connection not found, has " . + "the connection been freed already?" ); } $conn->setLBInfo( 'foreignPoolRefCount', --$refCount ); if ( $refCount <= 0 ) { @@ -552,9 +555,9 @@ class LoadBalancer { * * @see LoadBalancer::getConnection() for parameter information * - * @param integer $db + * @param int $db * @param mixed $groups - * @param string $wiki + * @param bool|string $wiki * @return DBConnRef */ public function getConnectionRef( $db, $groups = array(), $wiki = false ) { @@ -568,9 +571,9 @@ class LoadBalancer { * * @see LoadBalancer::getConnection() for parameter information * - * @param integer $db + * @param int $db * @param mixed $groups - * @param string $wiki + * @param bool|string $wiki * @return DBConnRef */ public function getLazyConnectionRef( $db, $groups = array(), $wiki = false ) { @@ -585,8 +588,8 @@ class LoadBalancer { * On error, returns false, and the connection which caused the * error will be available via $this->mErrorConnection. * - * @param $i Integer server index - * @param string $wiki wiki ID to open + * @param int $i Server index + * @param bool|string $wiki Wiki ID to open * @return DatabaseBase * * @access private @@ -596,6 +599,7 @@ class LoadBalancer { if ( $wiki !== false ) { $conn = $this->openForeignConnection( $i, $wiki ); wfProfileOut( __METHOD__ ); + return $conn; } if ( isset( $this->mConns['local'][$i][0] ) ) { @@ -614,6 +618,7 @@ class LoadBalancer { } } wfProfileOut( __METHOD__ ); + return $conn; } @@ -631,8 +636,8 @@ class LoadBalancer { * On error, returns false, and the connection which caused the * error will be available via $this->mErrorConnection. * - * @param $i Integer: server index - * @param string $wiki wiki ID to open + * @param int $i Server index + * @param string $wiki Wiki ID to open * @return DatabaseBase */ function openForeignConnection( $i, $wiki ) { @@ -688,13 +693,14 @@ class LoadBalancer { $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 ); } wfProfileOut( __METHOD__ ); + return $conn; } /** * Test if the specified index represents an open connection * - * @param $index Integer: server index + * @param int $index Server index * @access private * @return bool */ @@ -702,6 +708,7 @@ class LoadBalancer { if ( !is_integer( $index ) ) { return false; } + return (bool)$this->getAnyOpenConnection( $index ); } @@ -710,8 +717,8 @@ class LoadBalancer { * Returns a Database object whether or not the connection was successful. * @access private * - * @param $server - * @param $dbNameOverride bool + * @param array $server + * @param bool $dbNameOverride * @throws MWException * @return DatabaseBase */ @@ -741,6 +748,7 @@ class LoadBalancer { if ( isset( $server['fakeMaster'] ) ) { $db->setFakeMaster( true ); } + return $db; } @@ -753,15 +761,16 @@ class LoadBalancer { if ( !is_object( $conn ) ) { // No last connection, probably due to all servers being too busy - wfLogDBError( "LB failure with no last connection. Connection error: {$this->mLastError}\n" ); + wfLogDBError( "LB failure with no last connection. Connection error: {$this->mLastError}" ); // If all servers were busy, mLastError will contain something sensible throw new DBConnectionError( null, $this->mLastError ); } else { $server = $conn->getProperty( 'mServer' ); - wfLogDBError( "Connection error: {$this->mLastError} ({$server})\n" ); + wfLogDBError( "Connection error: {$this->mLastError} ({$server})" ); $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); // throws DBConnectionError } + return false; /* not reached */ } @@ -775,7 +784,7 @@ class LoadBalancer { /** * Returns true if the specified index is a valid server index * - * @param $i + * @param string $i * @return bool */ function haveIndex( $i ) { @@ -785,7 +794,7 @@ class LoadBalancer { /** * Returns true if the specified index is valid and has non-zero load * - * @param $i + * @param string $i * @return bool */ function isNonZeroLoad( $i ) { @@ -804,7 +813,7 @@ class LoadBalancer { /** * Get the host name or IP address of the server with the specified index * Prefer a readable name if available. - * @param $i + * @param string $i * @return string */ function getServerName( $i ) { @@ -819,8 +828,8 @@ class LoadBalancer { /** * Return the server info structure for a given index, or false if the index is invalid. - * @param $i - * @return bool + * @param int $i + * @return array|bool */ function getServerInfo( $i ) { if ( isset( $this->mServers[$i] ) ) { @@ -831,9 +840,10 @@ class LoadBalancer { } /** - * Sets the server info structure for the given index. Entry at index $i is created if it doesn't exist - * @param $i - * @param $serverInfo + * Sets the server info structure for the given index. Entry at index $i + * is created if it doesn't exist + * @param int $i + * @param array $serverInfo */ function setServerInfo( $i, $serverInfo ) { $this->mServers[$i] = $serverInfo; @@ -848,17 +858,21 @@ class LoadBalancer { # master (however unlikely that may be), then we can fetch the position from the slave. $masterConn = $this->getAnyOpenConnection( 0 ); if ( !$masterConn ) { - for ( $i = 1; $i < count( $this->mServers ); $i++ ) { + $serverCount = count( $this->mServers ); + for ( $i = 1; $i < $serverCount; $i++ ) { $conn = $this->getAnyOpenConnection( $i ); if ( $conn ) { wfDebug( "Master pos fetched from slave\n" ); + return $conn->getSlavePos(); } } } else { wfDebug( "Master pos fetched from master\n" ); + return $masterConn->getMasterPos(); } + return false; } @@ -868,6 +882,7 @@ class LoadBalancer { function closeAll() { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { + /** @var DatabaseBase $conn */ foreach ( $conns3 as $conn ) { $conn->close(); } @@ -881,21 +896,10 @@ class LoadBalancer { } /** - * Deprecated function, typo in function name - * - * @deprecated in 1.18 - * @param $conn - */ - function closeConnecton( $conn ) { - wfDeprecated( __METHOD__, '1.18' ); - $this->closeConnection( $conn ); - } - - /** * Close a connection * Using this function makes sure the LoadBalancer knows the connection is closed. * If you use $conn->close() directly, the load balancer won't update its state. - * @param $conn DatabaseBase + * @param DatabaseBase $conn */ function closeConnection( $conn ) { $done = false; @@ -922,6 +926,7 @@ class LoadBalancer { function commitAll() { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { + /** @var DatabaseBase[] $conns3 */ foreach ( $conns3 as $conn ) { if ( $conn->trxLevel() ) { $conn->commit( __METHOD__, 'flush' ); @@ -941,6 +946,7 @@ class LoadBalancer { if ( empty( $conns2[$masterIndex] ) ) { continue; } + /** @var DatabaseBase $conn */ foreach ( $conns2[$masterIndex] as $conn ) { if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) { $conn->commit( __METHOD__, 'flush' ); @@ -950,8 +956,59 @@ class LoadBalancer { } /** - * @param $value null - * @return Mixed + * Issue ROLLBACK only on master, only if queries were done on connection + * @since 1.23 + */ + function rollbackMasterChanges() { + // Always 0, but who knows.. :) + $masterIndex = $this->getWriterIndex(); + foreach ( $this->mConns as $conns2 ) { + if ( empty( $conns2[$masterIndex] ) ) { + continue; + } + /** @var DatabaseBase $conn */ + foreach ( $conns2[$masterIndex] as $conn ) { + if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) { + $conn->rollback( __METHOD__, 'flush' ); + } + } + } + } + + /** + * @return bool Whether a master connection is already open + * @since 1.24 + */ + function hasMasterConnection() { + return $this->isOpen( $this->getWriterIndex() ); + } + + /** + * Determine if there are any pending changes that need to be rolled back + * or committed. + * @since 1.23 + * @return bool + */ + function hasMasterChanges() { + // Always 0, but who knows.. :) + $masterIndex = $this->getWriterIndex(); + foreach ( $this->mConns as $conns2 ) { + if ( empty( $conns2[$masterIndex] ) ) { + continue; + } + /** @var DatabaseBase $conn */ + foreach ( $conns2[$masterIndex] as $conn ) { + if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) { + return true; + } + } + } + return false; + } + + /** + * @param mixed $value + * @return mixed */ function waitTimeout( $value = null ) { return wfSetVar( $this->mWaitTimeout, $value ); @@ -966,7 +1023,7 @@ class LoadBalancer { /** * Disables/enables lag checks - * @param $mode null + * @param null|bool $mode * @return bool */ function allowLagged( $mode = null ) { @@ -974,6 +1031,7 @@ class LoadBalancer { return $this->mAllowLagged; } $this->mAllowLagged = $mode; + return $this->mAllowLagged; } @@ -984,6 +1042,7 @@ class LoadBalancer { $success = true; foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { + /** @var DatabaseBase[] $conns3 */ foreach ( $conns3 as $conn ) { if ( !$conn->ping() ) { $success = false; @@ -991,12 +1050,13 @@ class LoadBalancer { } } } + return $success; } /** * Call a function with each open connection object - * @param $callback + * @param callable $callback * @param array $params */ function forEachOpenConnection( $callback, $params = array() ) { @@ -1016,16 +1076,29 @@ class LoadBalancer { * May attempt to open connections to slaves on the default DB. If there is * no lag, the maximum lag will be reported as -1. * - * @param string $wiki Wiki ID, or false for the default database - * + * @param bool|string $wiki Wiki ID, or false for the default database * @return array ( host, max lag, index of max lagged host ) */ function getMaxLag( $wiki = false ) { $maxLag = -1; $host = ''; $maxIndex = 0; - if ( $this->getServerCount() > 1 ) { // no replication = no lag + + if ( $this->getServerCount() <= 1 ) { // no replication = no lag + return array( $host, $maxLag, $maxIndex ); + } + + // Try to get the max lag info from the server cache + $key = 'loadbalancer:maxlag:cluster:' . $this->mServers[0]['host']; + $cache = ObjectCache::newAccelerator( array(), 'hash' ); + $maxLagInfo = $cache->get( $key ); // (host, lag, index) + + // Fallback to connecting to each slave and getting the lag + if ( !$maxLagInfo ) { foreach ( $this->mServers as $i => $conn ) { + if ( $i == $this->getWriterIndex() ) { + continue; // nothing to check + } $conn = false; if ( $wiki === false ) { $conn = $this->getAnyOpenConnection( $i ); @@ -1043,16 +1116,18 @@ class LoadBalancer { $maxIndex = $i; } } + $maxLagInfo = array( $host, $maxLag, $maxIndex ); + $cache->set( $key, $maxLagInfo, 5 ); } - return array( $host, $maxLag, $maxIndex ); + + return $maxLagInfo; } /** * Get lag time for each server * Results are cached for a short time in memcached, and indefinitely in the process cache * - * @param $wiki - * + * @param string|bool $wiki * @return array */ function getLagTimes( $wiki = false ) { @@ -1068,6 +1143,7 @@ class LoadBalancer { $this->mLagTimes = $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki ); } + return $this->mLagTimes; } @@ -1082,8 +1158,7 @@ class LoadBalancer { * function instead of Database::getLag() avoids a fatal error in this * case on many installations. * - * @param $conn DatabaseBase - * + * @param DatabaseBase $conn * @return int */ function safeGetLag( $conn ) { @@ -1112,14 +1187,16 @@ class LoadBalancer { class DBConnRef implements IDatabase { /** @var LoadBalancer */ protected $lb; + /** @var DatabaseBase|null */ protected $conn; - /** @var Array|null */ + + /** @var array|null */ protected $params; /** - * @param $lb LoadBalancer - * @param $conn DatabaseBase|array Connection or (server index, group, wiki ID) array + * @param LoadBalancer $lb + * @param DatabaseBase|array $conn Connection or (server index, group, wiki ID) array */ public function __construct( LoadBalancer $lb, $conn ) { $this->lb = $lb; @@ -1135,6 +1212,7 @@ class DBConnRef implements IDatabase { list( $db, $groups, $wiki ) = $this->params; $this->conn = $this->lb->getConnection( $db, $groups, $wiki ); } + return call_user_func_array( array( $this->conn, $name ), $arguments ); } diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index 519e2dfd..7281485b 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -32,61 +32,35 @@ interface LoadMonitor { * * @param LoadBalancer $parent */ - function __construct( $parent ); + public function __construct( $parent ); /** * Perform pre-connection load ratio adjustment. - * @param $loads array - * @param string $group the selected query group - * @param $wiki String + * @param array $loads + * @param string|bool $group The selected query group. Default: false + * @param string|bool $wiki Default: false */ - function scaleLoads( &$loads, $group = false, $wiki = false ); - - /** - * Perform post-connection backoff. - * - * If the connection is in overload, this should return a backoff factor - * which will be used to control polling time. The number of threads - * connected is a good measure. - * - * If there is no overload, zero can be returned. - * - * A threshold thread count is given, the concrete class may compare this - * to the running thread count. The threshold may be false, which indicates - * that the sysadmin has not configured this feature. - * - * @param $conn DatabaseBase - * @param $threshold Float - */ - function postConnectionBackoff( $conn, $threshold ); + public function scaleLoads( &$loads, $group = false, $wiki = false ); /** * Return an estimate of replication lag for each server * - * @param $serverIndexes - * @param $wiki + * @param array $serverIndexes + * @param string $wiki * * @return array */ - function getLagTimes( $serverIndexes, $wiki ); + public function getLagTimes( $serverIndexes, $wiki ); } -class LoadMonitor_Null implements LoadMonitor { - function __construct( $parent ) { - } - - function scaleLoads( &$loads, $group = false, $wiki = false ) { +class LoadMonitorNull implements LoadMonitor { + public function __construct( $parent ) { } - function postConnectionBackoff( $conn, $threshold ) { + public function scaleLoads( &$loads, $group = false, $wiki = false ) { } - /** - * @param $serverIndexes - * @param $wiki - * @return array - */ - function getLagTimes( $serverIndexes, $wiki ) { + public function getLagTimes( $serverIndexes, $wiki ) { return array_fill_keys( $serverIndexes, 0 ); } } @@ -97,58 +71,44 @@ class LoadMonitor_Null implements LoadMonitor { * * @ingroup Database */ -class LoadMonitor_MySQL implements LoadMonitor { +class LoadMonitorMySQL implements LoadMonitor { + /** @var LoadBalancer */ + public $parent; + /** @var BagOStuff */ + protected $cache; - /** - * @var LoadBalancer - */ - var $parent; + public function __construct( $parent ) { + global $wgMemc; - /** - * @param LoadBalancer $parent - */ - function __construct( $parent ) { $this->parent = $parent; + $this->cache = $wgMemc ?: wfGetMainCache(); } - /** - * @param $loads - * @param $group bool - * @param $wiki bool - */ - function scaleLoads( &$loads, $group = false, $wiki = false ) { + public function scaleLoads( &$loads, $group = false, $wiki = false ) { } - /** - * @param $serverIndexes - * @param $wiki - * @return array - */ - function getLagTimes( $serverIndexes, $wiki ) { + public function getLagTimes( $serverIndexes, $wiki ) { if ( count( $serverIndexes ) == 1 && reset( $serverIndexes ) == 0 ) { // Single server only, just return zero without caching return array( 0 => 0 ); } - wfProfileIn( __METHOD__ ); + $section = new ProfileSection( __METHOD__ ); + $expiry = 5; $requestRate = 10; - global $wgMemc; - if ( empty( $wgMemc ) ) { - $wgMemc = wfGetMainCache(); - } - + $cache = $this->cache; $masterName = $this->parent->getServerName( 0 ); $memcKey = wfMemcKey( 'lag_times', $masterName ); - $times = $wgMemc->get( $memcKey ); + $times = $cache->get( $memcKey ); 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'] ); // hide from caller - wfProfileOut( __METHOD__ ); + return $times; } wfIncrStats( 'lag_cache_miss_expired' ); @@ -157,15 +117,15 @@ class LoadMonitor_MySQL implements LoadMonitor { } # Cache key missing or expired - if ( $wgMemc->add( "$memcKey:lock", 1, 10 ) ) { + if ( $cache->add( "$memcKey:lock", 1, 10 ) ) { # Let this process alone update the cache value - $unlocker = new ScopedCallback( function() use ( $wgMemc, $memcKey ) { - $wgMemc->delete( $memcKey ); + $unlocker = new ScopedCallback( function () use ( $cache, $memcKey ) { + $cache->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; } @@ -177,34 +137,19 @@ class LoadMonitor_MySQL implements LoadMonitor { $times[$i] = $conn->getLag(); } elseif ( false !== ( $conn = $this->parent->openConnection( $i, $wiki ) ) ) { $times[$i] = $conn->getLag(); + // Close the connection to avoid sleeper connections piling up. + // Note that the caller will pick one of these DBs and reconnect, + // which is slightly inefficient, but this only matters for the lag + // time cache miss cache, which is far less common that cache hits. + $this->parent->closeConnection( $conn ); } } # Add a timestamp key so we know when it was cached $times['timestamp'] = time(); - $wgMemc->set( $memcKey, $times, $expiry + 10 ); + $cache->set( $memcKey, $times, $expiry + 10 ); unset( $times['timestamp'] ); // hide from caller - wfProfileOut( __METHOD__ ); return $times; } - - /** - * @param $conn DatabaseBase - * @param $threshold - * @return int - */ - function postConnectionBackoff( $conn, $threshold ) { - if ( !$threshold ) { - return 0; - } - $status = $conn->getMysqlStatus( "Thread%" ); - if ( $status['Threads_running'] > $threshold ) { - $server = $conn->getProperty( 'mServer' ); - wfLogDBError( "LB backoff from $server - Threads_running = {$status['Threads_running']}\n" ); - return $status['Threads_connected']; - } else { - return 0; - } - } } diff --git a/includes/db/ORMIterator.php b/includes/db/ORMIterator.php index 077eab0f..e8104b6f 100644 --- a/includes/db/ORMIterator.php +++ b/includes/db/ORMIterator.php @@ -27,5 +27,4 @@ * @author Jeroen De Dauw < jeroendedauw@gmail.com > */ interface ORMIterator extends Iterator { - } diff --git a/includes/db/ORMResult.php b/includes/db/ORMResult.php index 160033c4..327d20d9 100644 --- a/includes/db/ORMResult.php +++ b/includes/db/ORMResult.php @@ -30,14 +30,13 @@ */ class ORMResult implements ORMIterator { - /** * @var ResultWrapper */ protected $res; /** - * @var integer + * @var int */ protected $key; @@ -63,7 +62,7 @@ class ORMResult implements ORMIterator { } /** - * @param $row + * @param bool|object $row */ protected function setCurrent( $row ) { if ( $row === false ) { @@ -74,14 +73,14 @@ class ORMResult implements ORMIterator { } /** - * @return integer + * @return int */ public function count() { return $this->res->numRows(); } /** - * @return boolean + * @return bool */ public function isEmpty() { return $this->res->numRows() === 0; @@ -95,7 +94,7 @@ class ORMResult implements ORMIterator { } /** - * @return integer + * @return int */ public function key() { return $this->key; @@ -114,10 +113,9 @@ class ORMResult implements ORMIterator { } /** - * @return boolean + * @return bool */ public function valid() { return $this->current !== false; } - } diff --git a/includes/db/ORMRow.php b/includes/db/ORMRow.php index 5ce3794d..b0bade33 100644 --- a/includes/db/ORMRow.php +++ b/includes/db/ORMRow.php @@ -32,7 +32,6 @@ */ class ORMRow implements IORMRow { - /** * The fields of the object. * field name (w/o prefix) => value @@ -79,7 +78,7 @@ class ORMRow implements IORMRow { * * @param IORMTable|null $table Deprecated since 1.22 * @param array|null $fields - * @param boolean $loadDefaults Deprecated since 1.22 + * @param bool $loadDefaults Deprecated since 1.22 */ public function __construct( IORMTable $table = null, $fields = null, $loadDefaults = false ) { $this->table = $table; @@ -102,8 +101,8 @@ class ORMRow implements IORMRow { * @deprecated since 1.22 * * @param array|null $fields - * @param boolean $override - * @param boolean $skipLoaded + * @param bool $override + * @param bool $skipLoaded * * @return bool Success indicator */ @@ -130,8 +129,10 @@ class ORMRow implements IORMRow { if ( $result !== false ) { $this->setFields( $this->table->getFieldsFromDBResult( $result ), $override ); + return true; } + return false; } @@ -144,7 +145,7 @@ class ORMRow implements IORMRow { * @since 1.20 * * @param string $name Field name - * @param $default mixed: Default value to return when none is found + * @param mixed $default Default value to return when none is found * (default: null) * * @throws MWException @@ -166,7 +167,7 @@ class ORMRow implements IORMRow { * @since 1.20 * @deprecated since 1.22 * - * @param $name string + * @param string $name * * @return mixed */ @@ -194,7 +195,7 @@ class ORMRow implements IORMRow { * * @since 1.20 * - * @return integer|null + * @return int|null */ public function getId() { return $this->getField( 'id' ); @@ -205,7 +206,7 @@ class ORMRow implements IORMRow { * * @since 1.20 * - * @param integer|null $id + * @param int|null $id */ public function setId( $id ) { $this->setField( 'id', $id ); @@ -218,7 +219,7 @@ class ORMRow implements IORMRow { * * @param string $name * - * @return boolean + * @return bool */ public function hasField( $name ) { return array_key_exists( $name, $this->fields ); @@ -229,11 +230,10 @@ class ORMRow implements IORMRow { * * @since 1.20 * - * @return boolean + * @return bool */ public function hasIdField() { - return $this->hasField( 'id' ) - && !is_null( $this->getField( 'id' ) ); + return $this->hasField( 'id' ) && !is_null( $this->getField( 'id' ) ); } /** @@ -252,7 +252,7 @@ class ORMRow implements IORMRow { $value = $this->fields[$name]; // Skip null id fields so that the DBMS can set the default. - if ( $name === 'id' && is_null ( $value ) ) { + if ( $name === 'id' && is_null( $value ) ) { continue; } @@ -278,7 +278,7 @@ class ORMRow implements IORMRow { * @since 1.20 * * @param array $fields The fields to set - * @param boolean $override Override already set fields with the provided values? + * @param bool $override Override already set fields with the provided values? */ public function setFields( array $fields, $override = true ) { foreach ( $fields as $name => $value ) { @@ -295,7 +295,7 @@ class ORMRow implements IORMRow { * @since 1.20 * * @param null|array $fields - * @param boolean $incNullId + * @param bool $incNullId * * @return array */ @@ -328,7 +328,7 @@ class ORMRow implements IORMRow { * @since 1.20 * @deprecated since 1.22 * - * @param boolean $override + * @param bool $override */ public function loadDefaults( $override = true ) { $this->setFields( $this->table->getDefaults(), $override ); @@ -343,7 +343,7 @@ class ORMRow implements IORMRow { * * @param string|null $functionName * - * @return boolean Success indicator + * @return bool Success indicator */ public function save( $functionName = null ) { if ( $this->hasIdField() ) { @@ -361,7 +361,7 @@ class ORMRow implements IORMRow { * * @param string|null $functionName * - * @return boolean Success indicator + * @return bool Success indicator */ protected function saveExisting( $functionName = null ) { $dbw = $this->table->getWriteDbConnection(); @@ -400,7 +400,7 @@ class ORMRow implements IORMRow { * @param string|null $functionName * @param array|null $options * - * @return boolean Success indicator + * @return bool Success indicator */ protected function insert( $functionName = null, array $options = null ) { $dbw = $this->table->getWriteDbConnection(); @@ -430,7 +430,7 @@ class ORMRow implements IORMRow { * @since 1.20 * @deprecated since 1.22, use IORMTable->removeRow * - * @return boolean Success indicator + * @return bool Success indicator */ public function remove() { $this->beforeRemove(); @@ -456,8 +456,9 @@ class ORMRow implements IORMRow { /** * Before removal of an object happens, @see beforeRemove gets called. - * This method loads the fields of which the names have been returned by this one (or all fields if null is returned). - * This allows for loading info needed after removal to get rid of linked data and the like. + * This method loads the fields of which the names have been returned by + * this one (or all fields if null is returned). This allows for loading + * info needed after removal to get rid of linked data and the like. * * @since 1.20 * @@ -523,9 +524,9 @@ class ORMRow implements IORMRow { * @deprecated since 1.22, use IORMTable->addToField * * @param string $field - * @param integer $amount + * @param int $amount * - * @return boolean Success indicator + * @return bool Success indicator */ public function addToField( $field, $amount ) { return $this->table->addToField( $this->getUpdateConditions(), $field, $amount ); @@ -552,7 +553,6 @@ class ORMRow implements IORMRow { * @param array|string|null $summaryFields */ public function loadSummaryFields( $summaryFields = null ) { - } /** @@ -561,7 +561,7 @@ class ORMRow implements IORMRow { * @since 1.20 * @deprecated since 1.22 * - * @param boolean $update + * @param bool $update */ public function setUpdateSummaries( $update ) { $this->updateSummaries = $update; @@ -573,7 +573,7 @@ class ORMRow implements IORMRow { * @since 1.20 * @deprecated since 1.22 * - * @param boolean $summaryMode + * @param bool $summaryMode */ public function setSummaryMode( $summaryMode ) { $this->inSummaryMode = $summaryMode; @@ -590,5 +590,4 @@ class ORMRow implements IORMRow { public function getTable() { return $this->table; } - } diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php index 5f6723b9..2f898b75 100644 --- a/includes/db/ORMTable.php +++ b/includes/db/ORMTable.php @@ -29,7 +29,6 @@ */ class ORMTable extends DBAccessBase implements IORMTable { - /** * Cache for instances, used by the singleton method. * @@ -81,7 +80,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @since 1.20 * - * @var integer DB_ enum + * @var int DB_ enum */ protected $readDb = DB_SLAVE; @@ -96,7 +95,9 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param string|null $rowClass * @param string $fieldPrefix */ - public function __construct( $tableName = '', array $fields = array(), array $defaults = array(), $rowClass = null, $fieldPrefix = '' ) { + public function __construct( $tableName = '', array $fields = array(), + array $defaults = array(), $rowClass = null, $fieldPrefix = '' + ) { $this->tableName = $tableName; $this->fields = $fields; $this->defaults = $defaults; @@ -201,8 +202,10 @@ class ORMTable extends DBAccessBase implements IORMTable { * @return ORMResult */ public function select( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ) { + array $options = array(), $functionName = null + ) { $res = $this->rawSelect( $fields, $conditions, $options, $functionName ); + return new ORMResult( $this, $res ); } @@ -217,11 +220,12 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param array $options * @param string|null $functionName * - * @return array of row objects - * @throws DBQueryError if the query failed (even if the database was in ignoreErrors mode). + * @return array Array of row objects + * @throws DBQueryError If the query failed (even if the database was in ignoreErrors mode). */ public function selectObjects( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ) { + array $options = array(), $functionName = null + ) { $result = $this->selectFields( $fields, $conditions, $options, false, $functionName ); $objects = array(); @@ -239,19 +243,19 @@ class ORMTable extends DBAccessBase implements IORMTable { * @since 1.20 * * @param null|string|array $fields - * @param array $conditions - * @param array $options - * @param null|string $functionName - * + * @param array $conditions + * @param array $options + * @param null|string $functionName * @return ResultWrapper - * @throws DBQueryError if the quey failed (even if the database was in ignoreErrors mode). + * @throws DBQueryError If the query failed (even if the database was in + * ignoreErrors mode). */ public function rawSelect( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ) { + array $options = array(), $functionName = null + ) { if ( is_null( $fields ) ) { $fields = array_keys( $this->getFields() ); - } - else { + } else { $fields = (array)$fields; } @@ -307,13 +311,14 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param array|string|null $fields * @param array $conditions * @param array $options - * @param boolean $collapse Set to false to always return each result row as associative array. + * @param bool $collapse Set to false to always return each result row as associative array. * @param string|null $functionName * - * @return array of array + * @return array Array of array */ public function selectFields( $fields = null, array $conditions = array(), - array $options = array(), $collapse = true, $functionName = null ) { + array $options = array(), $collapse = true, $functionName = null + ) { $objects = array(); $result = $this->rawSelect( $fields, $conditions, $options, $functionName ); @@ -325,8 +330,7 @@ class ORMTable extends DBAccessBase implements IORMTable { if ( $collapse ) { if ( count( $fields ) === 1 ) { $objects = array_map( 'array_shift', $objects ); - } - elseif ( count( $fields ) === 2 ) { + } elseif ( count( $fields ) === 2 ) { $o = array(); foreach ( $objects as $object ) { @@ -354,7 +358,8 @@ class ORMTable extends DBAccessBase implements IORMTable { * @return IORMRow|bool False on failure */ public function selectRow( $fields = null, array $conditions = array(), - array $options = array(), $functionName = null ) { + array $options = array(), $functionName = null + ) { $options['LIMIT'] = 1; $objects = $this->select( $fields, $conditions, $options, $functionName ); @@ -373,10 +378,11 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param array $options * @param string|null $functionName * - * @return ResultWrapper + * @return stdClass */ public function rawSelectRow( array $fields, array $conditions = array(), - array $options = array(), $functionName = null ) { + array $options = array(), $functionName = null + ) { $dbr = $this->getReadDbConnection(); $result = $dbr->selectRow( @@ -388,6 +394,7 @@ class ORMTable extends DBAccessBase implements IORMTable { ); $this->releaseConnection( $dbr ); + return $result; } @@ -403,13 +410,14 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param array|string|null $fields * @param array $conditions * @param array $options - * @param boolean $collapse Set to false to always return each result row as associative array. + * @param bool $collapse Set to false to always return each result row as associative array. * @param string|null $functionName * * @return mixed|array|bool False on failure */ public function selectFieldsRow( $fields = null, array $conditions = array(), - array $options = array(), $collapse = true, $functionName = null ) { + array $options = array(), $collapse = true, $functionName = null + ) { $options['LIMIT'] = 1; $objects = $this->selectFields( $fields, $conditions, $options, $collapse, $functionName ); @@ -425,7 +433,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @param array $conditions * - * @return boolean + * @return bool */ public function has( array $conditions = array() ) { return $this->selectRow( array( 'id' ), $conditions ) !== false; @@ -436,7 +444,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @since 1.21 * - * @return boolean + * @return bool */ public function exists() { $dbr = $this->getReadDbConnection(); @@ -458,7 +466,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param array $conditions * @param array $options * - * @return integer + * @return int */ public function count( array $conditions = array(), array $options = array() ) { $res = $this->rawSelectRow( @@ -479,7 +487,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param array $conditions * @param string|null $functionName * - * @return boolean Success indicator + * @return bool Success indicator */ public function delete( array $conditions, $functionName = null ) { $dbw = $this->getWriteDbConnection(); @@ -491,6 +499,7 @@ class ORMTable extends DBAccessBase implements IORMTable { ) !== false; // DatabaseBase::delete does not always return true for success as documented... $this->releaseConnection( $dbw ); + return $result; } @@ -499,8 +508,8 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @since 1.20 * - * @param boolean $requireParams - * @param boolean $setDefaults + * @param bool $requireParams + * @param bool $setDefaults * * @return array */ @@ -535,7 +544,9 @@ class ORMTable extends DBAccessBase implements IORMTable { } if ( $setDefaults && $hasDefault ) { - $default = is_array( $defaults[$field] ) ? implode( '|', $defaults[$field] ) : $defaults[$field]; + $default = is_array( $defaults[$field] ) + ? implode( '|', $defaults[$field] ) + : $defaults[$field]; $params[$field][ApiBase::PARAM_DFLT] = $default; } } @@ -561,16 +572,17 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @since 1.20 * - * @return integer DB_ enum + * @return int DB_ enum */ public function getReadDb() { return $this->readDb; } /** - * Set the database ID to use for read operations, use DB_XXX constants or an index to the load balancer setup. + * Set the database ID to use for read operations, use DB_XXX constants or + * an index to the load balancer setup. * - * @param integer $db + * @param int $db * * @since 1.20 */ @@ -583,7 +595,8 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @since 1.20 * - * @return String|bool The target wiki, in a form that LBFactory understands (or false if the local wiki is used) + * @return string|bool The target wiki, in a form that LBFactory understands + * (or false if the local wiki is used) */ public function getTargetWiki() { return $this->wiki; @@ -592,7 +605,8 @@ class ORMTable extends DBAccessBase implements IORMTable { /** * Set the ID of the any foreign wiki to use as a target for database operations * - * @param string|bool $wiki The target wiki, in a form that LBFactory understands (or false if the local wiki shall be used) + * @param string|bool $wiki The target wiki, in a form that LBFactory + * understands (or false if the local wiki shall be used) * * @since 1.20 */ @@ -634,13 +648,15 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @see LoadBalancer::reuseConnection * - * @param DatabaseBase $db the database + * @param DatabaseBase $db * * @since 1.20 */ + // @codingStandardsIgnoreStart Suppress "useless method overriding" sniffer warning public function releaseConnection( DatabaseBase $db ) { parent::releaseConnection( $db ); // just make it public } + // @codingStandardsIgnoreEnd /** * Update the records matching the provided conditions by @@ -652,7 +668,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param array $values * @param array $conditions * - * @return boolean Success indicator + * @return bool Success indicator */ public function update( array $values, array $conditions = array() ) { $dbw = $this->getWriteDbConnection(); @@ -665,6 +681,7 @@ class ORMTable extends DBAccessBase implements IORMTable { ) !== false; // DatabaseBase::update does not always return true for success as documented... $this->releaseConnection( $dbw ); + return $result; } @@ -711,8 +728,7 @@ class ORMTable extends DBAccessBase implements IORMTable { if ( is_array( $value ) ) { $field = $value[0]; $value = $value[1]; - } - else { + } else { $value = explode( ' ', $value, 2 ); $value[0] = $this->getPrefixedField( $value[0] ); $prefixedValues[] = implode( ' ', $value ); @@ -732,7 +748,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @since 1.20 * - * @param array|string $fields + * @param array $fields * * @return array */ @@ -809,7 +825,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * @since 1.20 * * @param stdClass $result - * + * @throws MWException * @return array */ public function getFieldsFromDBResult( stdClass $result ) { @@ -872,7 +888,7 @@ class ORMTable extends DBAccessBase implements IORMTable { /** * @see ORMTable::newRowFromFromDBResult * - * @deprecated use newRowFromDBResult instead + * @deprecated since 1.20 use newRowFromDBResult instead * @since 1.20 * * @param stdClass $result @@ -899,11 +915,11 @@ class ORMTable extends DBAccessBase implements IORMTable { /** * @see ORMTable::newRow * - * @deprecated use newRow instead + * @deprecated since 1.20 use newRow instead * @since 1.20 * * @param array $data - * @param boolean $loadDefaults + * @param bool $loadDefaults * * @return IORMRow */ @@ -917,7 +933,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * @since 1.20 * * @param array $fields - * @param boolean $loadDefaults + * @param bool $loadDefaults * * @return IORMRow */ @@ -945,7 +961,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @param string $name * - * @return boolean + * @return bool */ public function canHaveField( $name ) { return array_key_exists( $name, $this->getFields() ); @@ -959,7 +975,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param IORMRow $row The row to save * @param string|null $functionName * - * @return boolean Success indicator + * @return bool Success indicator */ public function updateRow( IORMRow $row, $functionName = null ) { $dbw = $this->getWriteDbConnection(); @@ -986,7 +1002,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param string|null $functionName * @param array|null $options * - * @return boolean Success indicator + * @return bool Success indicator */ public function insertRow( IORMRow $row, $functionName = null, array $options = null ) { $dbw = $this->getWriteDbConnection(); @@ -1052,7 +1068,7 @@ class ORMTable extends DBAccessBase implements IORMTable { * @param IORMRow $row * @param string|null $functionName * - * @return boolean Success indicator + * @return bool Success indicator */ public function removeRow( IORMRow $row, $functionName = null ) { $success = $this->delete( @@ -1071,9 +1087,9 @@ class ORMTable extends DBAccessBase implements IORMTable { * * @param array $conditions * @param string $field - * @param integer $amount + * @param int $amount * - * @return boolean Success indicator + * @return bool Success indicator * @throws MWException */ public function addToField( array $conditions, $field, $amount ) { @@ -1103,5 +1119,4 @@ class ORMTable extends DBAccessBase implements IORMTable { return $success; } - } |