diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2011-12-03 13:29:22 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2011-12-03 13:29:22 +0100 |
commit | ca32f08966f1b51fcb19460f0996bb0c4048e6fe (patch) | |
tree | ec04cc15b867bc21eedca904cea9af0254531a11 /includes/db | |
parent | a22fbfc60f36f5f7ee10d5ae6fe347340c2ee67c (diff) |
Update to MediaWiki 1.18.0
* also update ArchLinux skin to chagnes in MonoBook
* Use only css to hide our menu bar when printing
Diffstat (limited to 'includes/db')
-rw-r--r-- | includes/db/CloneDatabase.php | 158 | ||||
-rw-r--r-- | includes/db/Database.php | 1763 | ||||
-rw-r--r-- | includes/db/DatabaseError.php | 314 | ||||
-rw-r--r-- | includes/db/DatabaseIbm_db2.php | 292 | ||||
-rw-r--r-- | includes/db/DatabaseMssql.php | 484 | ||||
-rw-r--r-- | includes/db/DatabaseMysql.php | 219 | ||||
-rw-r--r-- | includes/db/DatabaseOracle.php | 290 | ||||
-rw-r--r-- | includes/db/DatabasePostgres.php | 178 | ||||
-rw-r--r-- | includes/db/DatabaseSqlite.php | 299 | ||||
-rw-r--r-- | includes/db/DatabaseUtility.php | 268 | ||||
-rw-r--r-- | includes/db/LBFactory.php | 47 | ||||
-rw-r--r-- | includes/db/LBFactory_Multi.php | 22 | ||||
-rw-r--r-- | includes/db/LBFactory_Single.php | 42 | ||||
-rw-r--r-- | includes/db/LoadBalancer.php | 203 | ||||
-rw-r--r-- | includes/db/LoadMonitor.php | 67 |
15 files changed, 2846 insertions, 1800 deletions
diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php new file mode 100644 index 00000000..3357268d --- /dev/null +++ b/includes/db/CloneDatabase.php @@ -0,0 +1,158 @@ +<?php +/** + * Helper class for making a copy of the database, mostly for unit testing. + * + * Copyright © 2010 Chad Horohoe <chad@anyonecanedit.org> + * http://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 + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @ingroup Database + */ + +class CloneDatabase { + + /** + * Table prefix for cloning + * @var String + */ + private $newTablePrefix = ''; + + /** + * Current table prefix + * @var String + */ + private $oldTablePrefix = ''; + + /** + * List of tables to be cloned + * @var Array + */ + private $tablesToClone = array(); + + /** + * Should we DROP tables containing the new names? + * @var Bool + */ + private $dropCurrentTables = true; + + /** + * Whether to use temporary tables or not + * @var Bool + */ + private $useTemporaryTables = true; + + /** + * Constructor + * + * @param $db DatabaseBase A database subclass + * @param $tablesToClone Array An array of tables to clone, unprefixed + * @param $newTablePrefix String Prefix to assign to the tables + * @param $oldTablePrefix String Prefix on current tables, if not $wgDBprefix + * @param $dropCurrentTables bool + */ + public function __construct( DatabaseBase $db, array $tablesToClone, + $newTablePrefix, $oldTablePrefix = '', $dropCurrentTables = true ) + { + $this->db = $db; + $this->tablesToClone = $tablesToClone; + $this->newTablePrefix = $newTablePrefix; + $this->oldTablePrefix = $oldTablePrefix ? $oldTablePrefix : $this->db->tablePrefix(); + $this->dropCurrentTables = $dropCurrentTables; + } + + /** + * Set whether to use temporary tables or not + * @param $u Bool Use temporary tables when cloning the structure + */ + public function useTemporaryTables( $u = true ) { + $this->useTemporaryTables = $u; + } + + /** + * Clone the table structure + */ + public function cloneTableStructure() { + + foreach( $this->tablesToClone as $tbl ) { + # Clean up from previous aborted run. So that table escaping + # works correctly across DB engines, we need to change the pre- + # fix back and forth so tableName() works right. + + self::changePrefix( $this->oldTablePrefix ); + $oldTableName = $this->db->tableName( $tbl, false ); + + self::changePrefix( $this->newTablePrefix ); + $newTableName = $this->db->tableName( $tbl, false ); + + if( $this->dropCurrentTables && !in_array( $this->db->getType(), array( 'postgres' ) ) ) { + $this->db->dropTable( $tbl, __METHOD__ ); + wfDebug( __METHOD__." dropping {$newTableName}\n", true); + //Dropping the oldTable because the prefix was changed + } + + # Create new table + wfDebug( __METHOD__." duplicating $oldTableName to $newTableName\n", true ); + $this->db->duplicateTableStructure( $oldTableName, $newTableName, $this->useTemporaryTables ); + + } + + } + + /** + * Change the prefix back to the original. + * @param $dropTables bool Optionally drop the tables we created + */ + public function destroy( $dropTables = false ) { + if( $dropTables ) { + self::changePrefix( $this->newTablePrefix ); + foreach( $this->tablesToClone as $tbl ) { + $this->db->dropTable( $tbl ); + } + } + self::changePrefix( $this->oldTablePrefix ); + } + + /** + * Change the table prefix on all open DB connections/ + * + * @param $prefix + * @return void + */ + public static function changePrefix( $prefix ) { + global $wgDBprefix; + wfGetLBFactory()->forEachLB( array( 'CloneDatabase', 'changeLBPrefix' ), array( $prefix ) ); + $wgDBprefix = $prefix; + } + + /** + * @param $lb LoadBalancer + * @param $prefix + * @return void + */ + public static function changeLBPrefix( $lb, $prefix ) { + $lb->forEachOpenConnection( array( 'CloneDatabase', 'changeDBPrefix' ), array( $prefix ) ); + } + + /** + * @param $db DatabaseBase + * @param $prefix + * @return void + */ + public static function changeDBPrefix( $db, $prefix ) { + $db->tablePrefix( $prefix ); + } +} diff --git a/includes/db/Database.php b/includes/db/Database.php index 5acb67fa..75e6a91d 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -28,7 +28,7 @@ interface DatabaseType { * * @return string */ - public function getType(); + function getType(); /** * Open a connection to the database. Usually aborts on failure @@ -40,38 +40,28 @@ interface DatabaseType { * @return bool * @throws DBConnectionError */ - public function open( $server, $user, $password, $dbName ); - - /** - * The DBMS-dependent part of query() - * @todo Fixme: Make this private someday - * - * @param $sql String: SQL query. - * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure - * @private - */ - /*private*/ function doQuery( $sql ); + function open( $server, $user, $password, $dbName ); /** * Fetch the next row from the given result object, in object form. * Fields can be retrieved with $row->fieldname, with fields acting like * member variables. * - * @param $res SQL result object as returned from DatabaseBase::query(), etc. + * @param $res ResultWrapper|object as returned from DatabaseBase::query(), etc. * @return Row object * @throws DBUnexpectedError Thrown if the database returns an error */ - public function fetchObject( $res ); + function fetchObject( $res ); /** * Fetch the next row from the given result object, in associative array * form. Fields are retrieved with $row['fieldname']. * - * @param $res SQL result object as returned from DatabaseBase::query(), etc. + * @param $res ResultWrapper result object as returned from DatabaseBase::query(), etc. * @return Row object * @throws DBUnexpectedError Thrown if the database returns an error */ - public function fetchRow( $res ); + function fetchRow( $res ); /** * Get the number of rows in a result object @@ -79,7 +69,7 @@ interface DatabaseType { * @param $res Mixed: A SQL result * @return int */ - public function numRows( $res ); + function numRows( $res ); /** * Get the number of fields in a result object @@ -88,7 +78,7 @@ interface DatabaseType { * @param $res Mixed: A SQL result * @return int */ - public function numFields( $res ); + function numFields( $res ); /** * Get a field name in a result object @@ -98,7 +88,7 @@ interface DatabaseType { * @param $n Integer * @return string */ - public function fieldName( $res, $n ); + function fieldName( $res, $n ); /** * Get the inserted value of an auto-increment row @@ -112,7 +102,7 @@ interface DatabaseType { * * @return int */ - public function insertId(); + function insertId(); /** * Change the position of the cursor in a result object @@ -121,7 +111,7 @@ interface DatabaseType { * @param $res Mixed: A SQL result * @param $row Mixed: Either MySQL row or ResultWrapper */ - public function dataSeek( $res, $row ); + function dataSeek( $res, $row ); /** * Get the last error number @@ -129,7 +119,7 @@ interface DatabaseType { * * @return int */ - public function lastErrno(); + function lastErrno(); /** * Get a description of the last error @@ -137,7 +127,7 @@ interface DatabaseType { * * @return string */ - public function lastError(); + function lastError(); /** * mysql_fetch_field() wrapper @@ -145,8 +135,10 @@ interface DatabaseType { * * @param $table string: table name * @param $field string: field name + * + * @return Field */ - public function fieldInfo( $table, $field ); + function fieldInfo( $table, $field ); /** * Get information about an index into an object @@ -163,7 +155,7 @@ interface DatabaseType { * * @return int */ - public function affectedRows(); + function affectedRows(); /** * Wrapper for addslashes() @@ -171,7 +163,7 @@ interface DatabaseType { * @param $s string: to be slashed. * @return string: slashed string. */ - public function strencode( $s ); + function strencode( $s ); /** * Returns a wikitext link to the DB's website, e.g., @@ -181,7 +173,7 @@ interface DatabaseType { * * @return string: wikitext of a link to the server software's web site */ - public static function getSoftwareLink(); + static function getSoftwareLink(); /** * A string describing the current software version, like from @@ -189,7 +181,7 @@ interface DatabaseType { * * @return string: Version information from the database server. */ - public function getServerVersion(); + function getServerVersion(); /** * A string describing the current software version, and possibly @@ -198,7 +190,7 @@ interface DatabaseType { * * @return string: Version information from the database server */ - public function getServerInfo(); + function getServerInfo(); } /** @@ -215,7 +207,12 @@ abstract class DatabaseBase implements DatabaseType { protected $mDoneWrites = false; protected $mPHPError = false; - protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname; + protected $mServer, $mUser, $mPassword, $mDBname; + + /** + * @var DatabaseBase + */ + protected $mConn = null; protected $mOpened = false; protected $mTablePrefix; @@ -244,15 +241,39 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Boolean, controls output of large amounts of debug information + * Boolean, controls output of large amounts of debug information. + * @param $debug: + * - true to enable debugging + * - false to disable debugging + * - omitted or null to do nothing + * + * @return The previous value of the flag */ function debug( $debug = null ) { return wfSetBit( $this->mFlags, DBO_DEBUG, $debug ); } /** - * Turns buffering of SQL result sets on (true) or off (false). - * Default is "on" and it should not be changed without good reasons. + * Turns buffering of SQL result sets on (true) or off (false). Default is + * "on". + * + * Unbuffered queries are very troublesome in MySQL: + * + * - If another query is executed while the first query is being read + * out, the first query is killed. This means you can't call normal + * MediaWiki functions while you are reading an unbuffered query result + * from a normal wfGetDB() connection. + * + * - Unbuffered queries cause the MySQL server to use large amounts of + * memory and to hold broad locks which block other queries. + * + * If you want to limit client-side memory, it's almost always better to + * split up queries into batches using a LIMIT clause than to switch off + * buffering. + * + * @param $buffer null|bool + * + * @return The previous value of the flag */ function bufferResults( $buffer = null ) { if ( is_null( $buffer ) ) { @@ -268,32 +289,50 @@ abstract class DatabaseBase implements DatabaseType { * database errors. Default is on (false). When turned off, the * code should use lastErrno() and lastError() to handle the * situation as appropriate. + * + * @return The previous value of the flag. */ function ignoreErrors( $ignoreErrors = null ) { return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors ); } /** - * The current depth of nested transactions - * @param $level Integer: , default NULL. + * Gets or sets 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 $level An integer (0 or 1), or omitted to leave it unchanged. + * @return The previous value */ function trxLevel( $level = null ) { return wfSetVar( $this->mTrxLevel, $level ); } /** - * Number of errors logged, only useful when errors are ignored + * Get/set the number of errors logged. Only useful when errors are ignored + * @param $count The count to set, or omitted to leave it unchanged. + * @return The error count */ function errorCount( $count = null ) { return wfSetVar( $this->mErrorCount, $count ); } + /** + * Get/set the table prefix. + * @param $prefix The table prefix to set, or omitted to leave it unchanged. + * @return The previous table prefix. + */ function tablePrefix( $prefix = null ) { - return wfSetVar( $this->mTablePrefix, $prefix ); + return wfSetVar( $this->mTablePrefix, $prefix, true ); } /** - * Properties passed down from the server info array of the load balancer + * Get properties passed down from the server info array of the load + * balancer. + * + * @param $name The entry of the info array to get, or null to get the + * whole array */ function getLBInfo( $name = null ) { if ( is_null( $name ) ) { @@ -307,6 +346,15 @@ abstract class DatabaseBase implements DatabaseType { } } + /** + * Set the LB info array, or a member of it. If called with one parameter, + * 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 + * @return void + */ function setLBInfo( $name, $value = null ) { if ( is_null( $value ) ) { $this->mLBInfo = $name; @@ -400,20 +448,28 @@ abstract class DatabaseBase implements DatabaseType { * Return the last query that went through DatabaseBase::query() * @return String */ - function lastQuery() { return $this->mLastQuery; } + function lastQuery() { + return $this->mLastQuery; + } /** * Returns true if the connection may have been used for write queries. * Should return true if unsure. + * + * @return bool */ - function doneWrites() { return $this->mDoneWrites; } + function doneWrites() { + return $this->mDoneWrites; + } /** * Is a connection to the database open? * @return Boolean */ - function isOpen() { return $this->mOpened; } + function isOpen() { + return $this->mOpened; + } /** * Set a flag for this connection @@ -457,6 +513,9 @@ abstract class DatabaseBase implements DatabaseType { return $this->$name; } + /** + * @return string + */ function getWikiID() { if ( $this->mTablePrefix ) { return "{$this->mDBname}-{$this->mTablePrefix}"; @@ -466,9 +525,11 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Return a path to the DBMS-specific schema, otherwise default to tables.sql + * Return a path to the DBMS-specific schema file, otherwise default to tables.sql + * + * @return string */ - public function getSchema() { + public function getSchemaPath() { global $IP; if ( file_exists( "$IP/maintenance/" . $this->getType() . "/tables.sql" ) ) { return "$IP/maintenance/" . $this->getType() . "/tables.sql"; @@ -493,12 +554,8 @@ abstract class DatabaseBase implements DatabaseType { function __construct( $server = false, $user = false, $password = false, $dbName = false, $flags = 0, $tablePrefix = 'get from global' ) { - global $wgOut, $wgDBprefix, $wgCommandLineMode; + global $wgDBprefix, $wgCommandLineMode; - # Can't get a reference if it hasn't been set yet - if ( !isset( $wgOut ) ) { - $wgOut = null; - } $this->mFlags = $flags; if ( $this->mFlags & DBO_DEFAULT ) { @@ -509,13 +566,6 @@ abstract class DatabaseBase implements DatabaseType { } } - /* - // Faster read-only access - if ( wfReadOnly() ) { - $this->mFlags |= DBO_PERSISTENT; - $this->mFlags &= ~DBO_TRX; - }*/ - /** Get the default table prefix*/ if ( $tablePrefix == 'get from global' ) { $this->mTablePrefix = $wgDBprefix; @@ -523,14 +573,16 @@ abstract class DatabaseBase implements DatabaseType { $this->mTablePrefix = $tablePrefix; } - if ( $server ) { + if ( $user ) { $this->open( $server, $user, $password, $dbName ); } } /** * Same as new DatabaseMysql( ... ), kept for backward compatibility - * @deprecated + * @deprecated since 1.17 + * + * @return DatabaseMysql */ static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { wfDeprecated( __METHOD__ ); @@ -554,10 +606,10 @@ abstract class DatabaseBase implements DatabaseType { * * @param $dbType String A possible DB type * @param $p Array An array of options to pass to the constructor. - * Valid options are: host, user, password, dbname, flags, tableprefix + * Valid options are: host, user, password, dbname, flags, tablePrefix * @return DatabaseBase subclass or null */ - public final static function newFromType( $dbType, $p = array() ) { + public final static function factory( $dbType, $p = array() ) { $canonicalDBTypes = array( 'mysql', 'postgres', 'sqlite', 'oracle', 'mssql', 'ibm_db2' ); @@ -571,7 +623,7 @@ abstract class DatabaseBase implements DatabaseType { isset( $p['password'] ) ? $p['password'] : false, isset( $p['dbname'] ) ? $p['dbname'] : false, isset( $p['flags'] ) ? $p['flags'] : 0, - isset( $p['tableprefix'] ) ? $p['tableprefix'] : 'get from global' + isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global' ); } else { return null; @@ -627,36 +679,51 @@ abstract class DatabaseBase implements DatabaseType { } /** + * The DBMS-dependent part of query() + * + * @param $sql String: SQL query. + * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure + */ + protected abstract function doQuery( $sql ); + + /** * Determine whether a query writes to the DB. * Should return true if unsure. + * + * @return bool */ function isWriteQuery( $sql ) { return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW|\(SELECT)\b/i', $sql ); } /** - * Usually aborts on failure. If errors are explicitly ignored, returns success. + * Run an SQL query and return the result. Normally throws a DBQueryError + * on failure. If errors are ignored, returns false instead. + * + * In new code, the query wrappers select(), insert(), update(), delete(), + * etc. should be used where possible, since they give much better DBMS + * independence and automatically quote or validate user input in a variety + * of contexts. This function is generally only useful for queries which are + * explicitly DBMS-dependent and are unsupported by the query wrappers, such + * as CREATE TABLE. + * + * 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 * comment (you can use __METHOD__ or add some extra info) * @param $tempIgnore Boolean: Whether to avoid throwing an exception on errors... * maybe best to catch the exception instead? - * @return boolean or ResultWrapper. true for a successful write query, ResultWrapper object for a successful read query, - * or false on failure if $tempIgnore set + * @return boolean|ResultWrapper. true for a successful write query, ResultWrapper object + * for a successful read query, or false on failure if $tempIgnore set * @throws DBQueryError Thrown when the database returns an error of any kind */ public function query( $sql, $fname = '', $tempIgnore = false ) { - global $wgProfiler; - $isMaster = !is_null( $this->getLBInfo( 'master' ) ); - if ( isset( $wgProfiler ) ) { + 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. - # Who's been wasting my precious column space? -- TS - # $profName = 'query: ' . $fname . ' ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); - if ( $isMaster ) { $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); $totalProf = 'DatabaseBase::query-master'; @@ -671,34 +738,30 @@ abstract class DatabaseBase implements DatabaseType { $this->mLastQuery = $sql; if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) { - // Set a flag indicating that writes have been done + # Set a flag indicating that writes have been done wfDebug( __METHOD__ . ": Writes done: $sql\n" ); $this->mDoneWrites = true; } # Add a comment for easy SHOW PROCESSLIST interpretation - # if ( $fname ) { - global $wgUser; - if ( is_object( $wgUser ) && $wgUser->mDataLoaded ) { - $userName = $wgUser->getName(); - if ( mb_strlen( $userName ) > 15 ) { - $userName = mb_substr( $userName, 0, 15 ) . '...'; - } - $userName = str_replace( '/', '', $userName ); - } else { - $userName = ''; + global $wgUser; + if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) { + $userName = $wgUser->getName(); + if ( mb_strlen( $userName ) > 15 ) { + $userName = mb_substr( $userName, 0, 15 ) . '...'; } - $commentedSql = preg_replace( '/\s/', " /* $fname $userName */ ", $sql, 1 ); - # } else { - # $commentedSql = $sql; - # } + $userName = str_replace( '/', '', $userName ); + } else { + $userName = ''; + } + $commentedSql = preg_replace( '/\s/', " /* $fname $userName */ ", $sql, 1 ); # If DBO_TRX is set, start a transaction if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && $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 + # avoid establishing transactions for SHOW and SET statements too - + # that would delay transaction initializations to once connection + # is really used by application $sqlstart = substr( $sql, 0, 10 ); // very much worth it, benchmark certified(tm) if ( strpos( $sqlstart, "SHOW " ) !== 0 and strpos( $sqlstart, "SET " ) !== 0 ) $this->begin(); @@ -751,7 +814,7 @@ abstract class DatabaseBase implements DatabaseType { $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore ); } - if ( isset( $wgProfiler ) ) { + if ( !Profiler::instance()->isStub() ) { wfProfileOut( $queryProf ); wfProfileOut( $totalProf ); } @@ -760,6 +823,9 @@ abstract class DatabaseBase implements 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 @@ -782,7 +848,6 @@ abstract class DatabaseBase implements DatabaseType { } } - /** * Intended to be compatible with the PEAR::DB wrapper functions. * http://pear.php.net/manual/en/package.database.db.intro-execute.php @@ -791,6 +856,12 @@ abstract class DatabaseBase implements DatabaseType { * ! = raw SQL bit (a function for instance) * & = filename; reads the file and inserts as a blob * (we don't use this though...) + * + * This function should not be used directly by new code outside of the + * database classes. The query wrapper functions (select() etc.) should be + * used instead. + * + * @return array */ function prepare( $sql, $func = 'DatabaseBase::prepare' ) { /* MySQL doesn't support prepared statements (yet), so just @@ -799,6 +870,9 @@ abstract class DatabaseBase implements DatabaseType { return array( 'query' => $sql, 'func' => $func ); } + /** + * Free a prepared query, generated by prepare(). + */ function freePrepared( $prepared ) { /* No-op by default */ } @@ -807,6 +881,8 @@ abstract class DatabaseBase implements DatabaseType { * Execute a prepared query with the various arguments * @param $prepared String: the prepared sql * @param $args Mixed: Either an array here, or put scalars as varargs + * + * @return ResultWrapper */ function execute( $prepared, $args = null ) { if ( !is_array( $args ) ) { @@ -823,8 +899,15 @@ abstract class DatabaseBase implements DatabaseType { /** * Prepare & execute an SQL statement, quoting and inserting arguments * in the appropriate places. + * + * This function should not be used directly by new code outside of the + * database classes. The query wrapper functions (select() etc.) should be + * used instead. + * * @param $query String * @param $args ... + * + * @return ResultWrapper */ function safeQuery( $query, $args = null ) { $prepared = $this->prepare( $query, 'DatabaseBase::safeQuery' ); @@ -886,21 +969,24 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Free a result object + * Free a result object returned by query() or select(). It's usually not + * 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 */ function freeResult( $res ) { - # Stub. Might not really need to be overridden, since results should - # be freed by PHP when the variable goes out of scope anyway. } /** - * Simple UPDATE wrapper - * Usually aborts on failure + * Simple UPDATE wrapper. + * Usually throws a DBQueryError on failure. * If errors are explicitly ignored, returns success * * This function exists for historical reasons, DatabaseBase::update() has a more standard * calling convention and feature set + * + * @return bool */ function set( $table, $var, $value, $cond, $fname = 'DatabaseBase::set' ) { $table = $this->tableName( $table ); @@ -911,11 +997,25 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Simple SELECT wrapper, returns a single field, input must be encoded - * Usually aborts on failure - * If errors are explicitly ignored, returns FALSE on failure + * A SELECT wrapper which returns a single field from a single result row. + * + * Usually throws a DBQueryError on failure. If errors are explicitly + * ignored, returns false on failure. + * + * If no result rows are returned from the query, false is returned. + * + * @param $table string|array Table name. See DatabaseBase::select() for details. + * @param $var string The field name to select. This must be a valid SQL + * fragment: do not use unvalidated user input. + * @param $cond string|array The condition array. See DatabaseBase::select() for details. + * @param $fname string The function name of the caller. + * @param $options string|array The query options. See DatabaseBase::select() for details. + * + * @return false|mixed The value from the field, or false on failure. */ - function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField', $options = array() ) { + function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField', + $options = array() ) + { if ( !is_array( $options ) ) { $options = array( $options ); } @@ -939,13 +1039,12 @@ abstract class DatabaseBase implements DatabaseType { /** * Returns an optional USE INDEX clause to go after the table, and a - * string to go at the end of the query - * - * @private + * string to go at the end of the query. * * @param $options Array: associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return Array + * @see DatabaseBase::select() */ function makeSelectOptions( $options ) { $preLimitTail = $postLimitTail = ''; @@ -960,7 +1059,10 @@ abstract class DatabaseBase implements DatabaseType { } if ( isset( $options['GROUP BY'] ) ) { - $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; + $gb = is_array( $options['GROUP BY'] ) + ? implode( ',', $options['GROUP BY'] ) + : $options['GROUP BY']; + $preLimitTail .= " GROUP BY {$gb}"; } if ( isset( $options['HAVING'] ) ) { @@ -968,7 +1070,10 @@ abstract class DatabaseBase implements DatabaseType { } if ( isset( $options['ORDER BY'] ) ) { - $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; + $ob = is_array( $options['ORDER BY'] ) + ? implode( ',', $options['ORDER BY'] ) + : $options['ORDER BY']; + $preLimitTail .= " ORDER BY {$ob}"; } // if (isset($options['LIMIT'])) { @@ -1032,49 +1137,176 @@ abstract class DatabaseBase implements DatabaseType { } /** - * SELECT wrapper + * Execute a SELECT query constructed using the various parameters provided. + * See below for full details of the parameters. + * + * @param $table String|Array Table name + * @param $vars String|Array Field names + * @param $conds String|Array Conditions + * @param $fname String Caller function name + * @param $options Array Query options + * @param $join_conds Array Join conditions + * + * + * @param $table string|array + * + * 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: + * + * array( 'a' => 'user' ) + * + * This includes the user table in the query, with the alias "a" available + * for use in field names (e.g. a.user_name). + * + * All of the table names given here are automatically run through + * DatabaseBase::tableName(), which causes the table prefix (if any) to be + * added, and various other table name mappings to be performed. + * + * + * @param $vars string|array + * + * May be either a field name or an array of field names. The field names + * here are complete fragments of SQL, for direct inclusion into the SELECT + * query. Expressions and aliases may be specified as in SQL, for example: + * + * array( 'MAX(rev_id) AS maxrev' ) + * + * If an expression is given, care must be taken to ensure that it is + * DBMS-independent. + * + * + * @param $conds string|array + * + * May be either a string containing a single condition, or an array of + * conditions. If an array is given, the conditions constructed from each + * element are combined with AND. + * + * Array elements may take one of two forms: + * + * - Elements with a numeric key are interpreted as raw SQL fragments. + * - Elements with a string key are interpreted as equality conditions, + * where the key is the field name. + * - If the value of such an array element is a scalar (such as a + * string), it will be treated as data and thus quoted appropriately. + * If it is null, an IS NULL clause will be added. + * - If the value is an array, an IN(...) clause will be constructed, + * such that the field name may match any of the elements in the + * array. The elements of the array will be quoted. + * + * Note that expressions are often DBMS-dependent in their syntax. + * DBMS-independent wrappers are provided for constructing several types of + * expression commonly used in condition queries. See: + * - DatabaseBase::buildLike() + * - DatabaseBase::conditional() + * + * + * @param $options string|array + * + * Optional: Array of query options. Boolean options are specified by + * including them in the array as a string value with a numeric key, for + * example: + * + * array( 'FOR UPDATE' ) + * + * The supported options are: + * + * - OFFSET: Skip this many rows at the start of the result set. OFFSET + * with LIMIT can theoretically be used for paging through a result set, + * but this is discouraged in MediaWiki for performance reasons. + * + * - LIMIT: Integer: return at most this many rows. The rows are sorted + * and then the first rows are taken until the limit is reached. LIMIT + * is applied to a result set after OFFSET. + * + * - FOR UPDATE: Boolean: lock the returned rows so that they can't be + * changed until the next COMMIT. + * + * - DISTINCT: Boolean: return only unique result rows. + * + * - GROUP BY: May be either an SQL fragment string naming a field or + * expression to group by, or an array of such SQL fragments. + * + * - HAVING: A string containing a HAVING clause. + * + * - ORDER BY: May be either an SQL fragment giving a field name or + * expression to order by, or an array of such SQL fragments. + * + * - USE INDEX: This may be either a string giving the index name to use + * for the query, or an array. If it is an associative array, each key + * gives the table name (or alias), each value gives the index name to + * use for that table. All strings are SQL fragments and so should be + * validated by the caller. + * + * - EXPLAIN: In MySQL, this causes an EXPLAIN SELECT query to be run, + * instead of SELECT. + * + * And also the following boolean MySQL extensions, see the MySQL manual + * for documentation: + * + * - LOCK IN SHARE MODE + * - STRAIGHT_JOIN + * - HIGH_PRIORITY + * - SQL_BIG_RESULT + * - SQL_BUFFER_RESULT + * - SQL_SMALL_RESULT + * - SQL_CALC_FOUND_ROWS + * - SQL_CACHE + * - SQL_NO_CACHE + * * - * @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 $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), - * see DatabaseBase::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 DatabaseBase::fetchObject or whatever), or false on failure + * @param $join_conds string|array + * + * Optional associative array of table-specific join conditions. In the + * most common case, this is unnecessary, since the join condition can be + * in $conds. However, it is useful for doing a LEFT JOIN. + * + * The key of the array contains the table name or alias. The value is an + * array with two elements, numbered 0 and 1. The first gives the type of + * join, the second is an SQL fragment giving the join condition for that + * table. For example: + * + * array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) + * + * @return ResultWrapper. 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. */ - function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) { + function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', + $options = array(), $join_conds = array() ) { $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); return $this->query( $sql, $fname ); } /** - * SELECT wrapper + * The equivalent of DatabaseBase::select() except that the constructed SQL + * is returned, instead of being immediately executed. + * + * @param $table string|array Table name + * @param $vars string|array Field names + * @param $conds string|array Conditions + * @param $fname string Caller function name + * @param $options string|array Query options + * @param $join_conds string|array Join conditions * - * @param $table Mixed: Array or string, table name(s) (prefix auto-added). Array keys are table aliases (optional) - * @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 $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), - * see DatabaseBase::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 + * @return SQL query string. + * @see DatabaseBase::select() */ function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) { if ( is_array( $vars ) ) { $vars = implode( ',', $vars ); } - if ( !is_array( $options ) ) { - $options = array( $options ); - } + $options = (array)$options; if ( is_array( $table ) ) { - if ( !empty( $join_conds ) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) ) { - $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds ); + $useIndex = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) ) + ? $options['USE INDEX'] + : array(); + if ( count( $join_conds ) || count( $useIndex ) ) { + $from = ' FROM ' . + $this->tableNamesWithUseIndexOrJOIN( $table, $useIndex, $join_conds ); } else { $from = ' FROM ' . implode( ',', $this->tableNamesWithAlias( $table ) ); } @@ -1099,9 +1331,10 @@ abstract class DatabaseBase implements DatabaseType { $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail"; } - if ( isset( $options['LIMIT'] ) ) + if ( isset( $options['LIMIT'] ) ) { $sql = $this->limitResult( $sql, $options['LIMIT'], isset( $options['OFFSET'] ) ? $options['OFFSET'] : false ); + } $sql = "$sql $postLimitTail"; if ( isset( $options['EXPLAIN'] ) ) { @@ -1112,24 +1345,22 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Single row SELECT wrapper - * Aborts or returns FALSE on error + * Single row SELECT wrapper. Equivalent to DatabaseBase::select(), except + * that a single row object is returned. If the query returns no rows, + * false is returned. * - * @param $table String: table name - * @param $vars String: the selected variables - * @param $conds Array: a condition map, terms are ANDed together. - * Items with numeric keys are taken to be literal conditions - * Takes an array of selected variables, and a condition map, which is ANDed - * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" => - * NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where - * $obj- >page_id is the ID of the Astronomy article - * @param $fname String: Calling function name - * @param $options Array - * @param $join_conds Array + * @param $table string|array Table name + * @param $vars string|array Field names + * @param $conds|array Conditions + * @param $fname string Caller function name + * @param $options string|array Query options + * @param $join_conds array|string Join conditions * - * @todo migrate documentation to phpdocumentor format + * @return ResultWrapper|bool */ - function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow', $options = array(), $join_conds = array() ) { + function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow', + $options = array(), $join_conds = array() ) + { $options['LIMIT'] = 1; $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds ); @@ -1147,10 +1378,17 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Estimate rows in dataset - * Returns estimated count - not necessarily an accurate estimate across different databases, - * so use sparingly - * Takes same arguments as DatabaseBase::select() + * Estimate 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 + * index cardinality statistics, and is notoriously inaccurate, especially + * when large numbers of rows have recently been added or deleted. + * + * For DBMSs that don't support fast result size estimation, this function + * will actually perform the SELECT COUNT(*). + * + * Takes the same arguments as DatabaseBase::select(). * * @param $table String: table name * @param $vars Array: unused @@ -1159,7 +1397,9 @@ abstract class DatabaseBase implements DatabaseType { * @param $options Array: options for select * @return Integer: row count */ - public function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseBase::estimateRowCount', $options = array() ) { + public function estimateRowCount( $table, $vars = '*', $conds = '', + $fname = 'DatabaseBase::estimateRowCount', $options = array() ) + { $rows = 0; $res = $this->select ( $table, 'COUNT(*) AS rowcount', $conds, $fname, $options ); @@ -1213,8 +1453,10 @@ abstract class DatabaseBase implements DatabaseType { /** * Determines whether an index exists - * Usually aborts on failure + * Usually throws a DBQueryError on failure * If errors are explicitly ignored, returns NULL on failure + * + * @return bool|null */ function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) { $info = $this->indexInfo( $table, $index, $fname ); @@ -1227,6 +1469,10 @@ abstract class DatabaseBase implements DatabaseType { /** * Query whether a given table exists + * + * @string table + * + * @return bool */ function tableExists( $table ) { $table = $this->tableName( $table ); @@ -1250,6 +1496,11 @@ abstract class DatabaseBase implements DatabaseType { /** * Determines if a given index is unique + * + * @param $table string + * @param $index string + * + * @return bool */ function indexUnique( $table, $index ) { $indexInfo = $this->indexInfo( $table, $index ); @@ -1262,18 +1513,45 @@ abstract class DatabaseBase implements DatabaseType { } /** - * INSERT wrapper, inserts an array into a table + * Helper for DatabaseBase::insert(). + * + * @param $options array + * @return string + */ + function makeInsertOptions( $options ) { + return implode( ' ', $options ); + } + + /** + * INSERT wrapper, inserts an array into a table. * - * $a may be a single associative array, or an array of these with numeric keys, for - * multi-row insert. + * $a may be either: * - * Usually aborts on failure - * If errors are explicitly ignored, returns success + * - A single associative array. The array keys are the field names, and + * the values are the values to insert. The values are treated as data + * and will be quoted appropriately. If NULL is inserted, this will be + * converted to a database NULL. + * - An array with numeric keys, holding a list of associative arrays. + * This causes a multi-row INSERT on DBMSs that support it. The keys in + * each subarray must be identical to each other, and in the same order. + * + * Usually throws a DBQueryError on failure. If errors are explicitly ignored, + * returns success. + * + * $options is an array of options, with boolean options encoded as values + * with numeric keys, in the same style as $options in + * DatabaseBase::select(). Supported options are: + * + * - IGNORE: Boolean: if present, duplicate key errors are ignored, and + * any rows which cause duplicate key errors are not inserted. It's + * possible to determine how many rows were successfully inserted using + * DatabaseBase::affectedRows(). * - * @param $table String: table name (prefix auto-added) - * @param $a Array: Array of rows to insert - * @param $fname String: Calling function name (use __METHOD__) for logs/profiling - * @param $options Mixed: Associative array of options + * @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 $options Array of options * * @return bool */ @@ -1289,6 +1567,8 @@ abstract class DatabaseBase implements DatabaseType { $options = array( $options ); } + $options = $this->makeInsertOptions( $options ); + if ( isset( $a[0] ) && is_array( $a[0] ) ) { $multi = true; $keys = array_keys( $a[0] ); @@ -1297,7 +1577,7 @@ abstract class DatabaseBase implements DatabaseType { $keys = array_keys( $a ); } - $sql = 'INSERT ' . implode( ' ', $options ) . + $sql = 'INSERT ' . $options . " INTO $table (" . implode( ',', $keys ) . ') VALUES '; if ( $multi ) { @@ -1320,7 +1600,6 @@ abstract class DatabaseBase implements DatabaseType { /** * Make UPDATE options for the DatabaseBase::update function * - * @private * @param $options Array: The options passed to DatabaseBase::update * @return string */ @@ -1343,15 +1622,26 @@ abstract class DatabaseBase implements DatabaseType { } /** - * UPDATE wrapper, takes a condition array and a SET array + * 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 $values Array: 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 $table String: The table to UPDATE - * @param $values Array: An array of values to SET - * @param $conds Array: An array of conditions (WHERE). Use '*' to update all rows. - * @param $fname String: The Class::Function calling this function - * (for the log) - * @param $options Array: An array of UPDATE options, can be one or - * more of IGNORE, LOW_PRIORITY + * @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 $options Array: An array of UPDATE options, can be: + * - IGNORE: Ignore unique key conflicts + * - LOW_PRIORITY: MySQL-specific, see MySQL manual. * @return Boolean */ function update( $table, $values, $conds, $fname = 'DatabaseBase::update', $options = array() ) { @@ -1368,12 +1658,16 @@ abstract class DatabaseBase implements DatabaseType { /** * Makes an encoded list of strings from an array - * $mode: - * LIST_COMMA - comma separated, no field names - * LIST_AND - ANDed WHERE clause (without the WHERE) - * 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 $a Array containing the data + * @param $mode: + * - 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 + * + * @return string */ function makeList( $a, $mode = LIST_COMMA ) { if ( !is_array( $a ) ) { @@ -1434,7 +1728,8 @@ abstract class DatabaseBase implements 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 $data Array: organized as 2-d array(baseKeyVal => array(subKeyVal => <ignored>, ...), ...) + * @param $data Array: organized as 2-d + * array(baseKeyVal => array(subKeyVal => <ignored>, ...), ...) * @param $baseKey String: field name to match the base-level keys to (eg 'pl_namespace') * @param $subKey String: field name to match the sub-level keys to (eg 'pl_title') * @return Mixed: string SQL fragment, or false if no items in array. @@ -1462,14 +1757,28 @@ abstract class DatabaseBase implements DatabaseType { * Bitwise operations */ + /** + * @param $field + * @return string + */ function bitNot( $field ) { return "(~$field)"; } + /** + * @param $fieldLeft + * @param $fieldRight + * @return string + */ function bitAnd( $fieldLeft, $fieldRight ) { return "($fieldLeft & $fieldRight)"; } + /** + * @param $fieldLeft + * @param $fieldRight + * @return string + */ function bitOr( $fieldLeft, $fieldRight ) { return "($fieldLeft | $fieldRight)"; } @@ -1484,6 +1793,7 @@ abstract class DatabaseBase implements DatabaseType { # 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; } @@ -1512,15 +1822,17 @@ abstract class DatabaseBase implements DatabaseType { * when calling query() directly. * * @param $name String: database table name + * @param $quoted Boolean: Automatically pass the table name through + * addIdentifierQuotes() so that it can be used in a query. * @return String: full database name */ - function tableName( $name ) { + function tableName( $name, $quoted = true ) { global $wgSharedDB, $wgSharedPrefix, $wgSharedTables; # 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 # to query a database table with a dot in the name. - if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) { + if ( $this->isQuotedIdentifier( $name ) ) { return $name; } @@ -1540,23 +1852,23 @@ abstract class DatabaseBase implements DatabaseType { # the correct table. $dbDetails = array_reverse( explode( '.', $name, 2 ) ); if ( isset( $dbDetails[1] ) ) { - @list( $table, $database ) = $dbDetails; + list( $table, $database ) = $dbDetails; } else { - @list( $table ) = $dbDetails; + list( $table ) = $dbDetails; } $prefix = $this->mTablePrefix; # Default prefix - # A database name has been specified in input. Quote the table name - # because we don't want any prefixes added. + # A database name has been specified in input. We don't want any + # prefixes added. if ( isset( $database ) ) { - $table = ( $table[0] == '`' ? $table : "`{$table}`" ); + $prefix = ''; } # Note that we use the long format because php will complain in in_array if # the input is not an array, and will complain in is_array if it is not set. if ( !isset( $database ) # Don't use shared database if pre selected. && isset( $wgSharedDB ) # We have a shared database - && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`' + && !$this->isQuotedIdentifier( $table ) # Paranoia check to prevent shared tables listing '`table`' && isset( $wgSharedTables ) && is_array( $wgSharedTables ) && in_array( $table, $wgSharedTables ) ) { # A shared table is selected @@ -1566,9 +1878,13 @@ abstract class DatabaseBase implements DatabaseType { # Quote the $database and $table and apply the prefix if not quoted. if ( isset( $database ) ) { - $database = ( $database[0] == '`' ? $database : "`{$database}`" ); + $database = ( !$quoted || $this->isQuotedIdentifier( $database ) ? $database : $this->addIdentifierQuotes( $database ) ); + } + + $table = "{$prefix}{$table}"; + if ( $quoted && !$this->isQuotedIdentifier( $table ) ) { + $table = $this->addIdentifierQuotes( "{$table}" ); } - $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" ); # Merge our database and table into our final table name. $tableName = ( isset( $database ) ? "{$database}.{$table}" : "{$table}" ); @@ -1650,45 +1966,54 @@ abstract class DatabaseBase implements DatabaseType { } /** - * @private + * Get the aliased table name clause for a FROM clause + * which might have a JOIN and/or USE INDEX clause + * + * @param $tables array( [alias] => table ) + * @param $use_index array() Same as for select() + * @param $join_conds array() Same as for select() + * @return string */ - function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) { + protected 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(); + $use_index = (array)$use_index; + $join_conds = (array)$join_conds; foreach ( $tables as $alias => $table ) { if ( !is_string( $alias ) ) { // No alias? Set it equal to the table name $alias = $table; } - // Is there a JOIN and INDEX clause for this table? - if ( isset( $join_conds_safe[$alias] ) && isset( $use_index_safe[$alias] ) ) { - $tableClause = $join_conds_safe[$alias][0] . ' ' . $this->tableNameWithAlias( $table, $alias ); - $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$alias] ) ); - $on = $this->makeList( (array)$join_conds_safe[$alias][1], LIST_AND ); + // Is there a JOIN clause for this table? + if ( isset( $join_conds[$alias] ) ) { + list( $joinType, $conds ) = $join_conds[$alias]; + $tableClause = $joinType; + $tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias ); + if ( isset( $use_index[$alias] ) ) { // has USE INDEX? + $use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) ); + if ( $use != '' ) { + $tableClause .= ' ' . $use; + } + } + $on = $this->makeList( (array)$conds, LIST_AND ); if ( $on != '' ) { $tableClause .= ' ON (' . $on . ')'; } $retJOIN[] = $tableClause; - // Is there an INDEX clause? - } else if ( isset( $use_index_safe[$alias] ) ) { + // Is there an INDEX clause for this table? + } elseif ( isset( $use_index[$alias] ) ) { $tableClause = $this->tableNameWithAlias( $table, $alias ); - $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$alias] ) ); - $ret[] = $tableClause; - // Is there a JOIN clause? - } else if ( isset( $join_conds_safe[$alias] ) ) { - $tableClause = $join_conds_safe[$alias][0] . ' ' . $this->tableNameWithAlias( $table, $alias ); - $on = $this->makeList( (array)$join_conds_safe[$alias][1], LIST_AND ); - if ( $on != '' ) { - $tableClause .= ' ON (' . $on . ')'; - } + $tableClause .= ' ' . $this->useIndexClause( + implode( ',', (array)$use_index[$alias] ) ); - $retJOIN[] = $tableClause; + $ret[] = $tableClause; } else { $tableClause = $this->tableNameWithAlias( $table, $alias ); + $ret[] = $tableClause; } } @@ -1703,6 +2028,10 @@ abstract class DatabaseBase implements DatabaseType { /** * Get the name of an index in a given table + * + * @param $index + * + * @return string */ function indexName( $index ) { // Backwards-compatibility hack @@ -1722,6 +2051,10 @@ abstract class DatabaseBase implements DatabaseType { /** * If it's a string, adds quotes and backslashes * Otherwise returns as-is + * + * @param $s string + * + * @return string */ function addQuotes( $s ) { if ( $s === null ) { @@ -1740,16 +2073,32 @@ abstract class DatabaseBase implements DatabaseType { * MySQL uses `backticks` while basically everything else uses double quotes. * Since MySQL is the odd one out here the double quotes are our generic * and we implement backticks in DatabaseMysql. - */ + * + * @return string + */ public function addIdentifierQuotes( $s ) { return '"' . str_replace( '"', '""', $s ) . '"'; } /** + * Returns if the given identifier looks quoted or not according to + * the database convention for quoting identifiers . + * + * @param $name string + * + * @return boolean + */ + public function isQuotedIdentifier( $name ) { + return $name[0] == '"' && substr( $name, -1, 1 ) == '"'; + } + + /** * Backwards compatibility, identifier quoting originated in DatabasePostgres * which used quote_ident which does not follow our naming conventions * was renamed to addIdentifierQuotes. - * @deprecated use addIdentifierQuotes + * @deprecated since 1.18 use addIdentifierQuotes + * + * @return string */ function quote_ident( $s ) { wfDeprecated( __METHOD__ ); @@ -1760,7 +2109,7 @@ abstract class DatabaseBase implements DatabaseType { * Escape string for safe LIKE usage. * WARNING: you should almost never use this function directly, * instead use buildLike() that escapes everything automatically - * Deprecated in 1.17, warnings in 1.17, removed in ??? + * @deprecated since 1.17, warnings in 1.17, removed in ??? */ public function escapeLike( $s ) { wfDeprecated( __METHOD__ ); @@ -1809,6 +2158,8 @@ abstract class DatabaseBase implements DatabaseType { /** * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query + * + * @return LikeMatch */ function anyChar() { return new LikeMatch( '_' ); @@ -1816,6 +2167,8 @@ abstract class DatabaseBase implements DatabaseType { /** * Returns a token for buildLike() that denotes a '%' to be used in a LIKE query + * + * @rerturn LikeMatch */ function anyString() { return new LikeMatch( '%' ); @@ -1843,15 +2196,24 @@ abstract class DatabaseBase implements DatabaseType { } /** - * REPLACE query wrapper - * PostgreSQL simulates this with a DELETE followed by INSERT - * $row is the row to insert, an associative array - * $uniqueIndexes is an array of indexes. Each element may be either a - * field name or an array of field names + * REPLACE query wrapper. * - * It may be more efficient to leave off unique indexes which are unlikely to collide. - * However if you do this, you run the risk of encountering errors which wouldn't have - * occurred in MySQL + * REPLACE is a very handy MySQL extension, which functions like an INSERT + * except that when there is a duplicate key error, the old row is deleted + * and the new row is inserted in its place. + * + * We simulate this with standard SQL with a DELETE followed by INSERT. To + * perform the delete, we need to know what the unique indexes are so that + * we know how to find the conflicting rows. + * + * It may be more efficient to leave off unique indexes which are unlikely + * to collide. However if you do this, you run the risk of encountering + * errors which wouldn't have occurred in MySQL. + * + * @param $rows Can be either a single row to insert, or multiple rows, + * in the same format as for DatabaseBase::insert() + * @param $uniqueIndexes is an array of indexes. Each element may be either + * a field name or an array of field names * * @param $table String: The table to replace the row(s) in. * @param $uniqueIndexes Array: An associative array of indexes @@ -1859,6 +2221,61 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String: Calling function name (use __METHOD__) for logs/profiling */ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) { + $quotedTable = $this->tableName( $table ); + + if ( count( $rows ) == 0 ) { + return; + } + + # Single row case + if ( !is_array( reset( $rows ) ) ) { + $rows = array( $rows ); + } + + foreach( $rows as $row ) { + # Delete rows which collide + if ( $uniqueIndexes ) { + $sql = "DELETE FROM $quotedTable WHERE "; + $first = true; + foreach ( $uniqueIndexes as $index ) { + if ( $first ) { + $first = false; + $sql .= '( '; + } else { + $sql .= ' ) OR ( '; + } + if ( is_array( $index ) ) { + $first2 = true; + foreach ( $index as $col ) { + if ( $first2 ) { + $first2 = false; + } else { + $sql .= ' AND '; + } + $sql .= $col . '=' . $this->addQuotes( $row[$col] ); + } + } else { + $sql .= $index . '=' . $this->addQuotes( $row[$index] ); + } + } + $sql .= ' )'; + $this->query( $sql, $fname ); + } + + # Now insert the row + $this->insert( $table, $row ); + } + } + + /** + * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE + * statement. + * + * @param $table Table name + * @param $rows Rows to insert + * @param $fname Caller function name + */ + protected function nativeReplace( $table, $rows, $fname ) { $table = $this->tableName( $table ); # Single row case @@ -1883,35 +2300,42 @@ abstract class DatabaseBase implements DatabaseType { } /** - * DELETE where the condition is a join - * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects + * DELETE where the condition is a join. * - * For safety, an empty $conds will not delete everything. If you want to delete all rows where the - * join condition matches, set $conds='*' + * MySQL overrides this to use a multi-table DELETE syntax, in other databases + * we use sub-selects * - * DO NOT put the join condition in $conds + * For safety, an empty $conds will not delete everything. If you want to + * delete all rows where the join condition matches, set $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 + * 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 */ - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) { + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, + $fname = 'DatabaseBase::deleteJoin' ) + { if ( !$conds ) { - throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' ); + throw new DBUnexpectedError( $this, + 'DatabaseBase::deleteJoin() called with empty $conds' ); } $delTable = $this->tableName( $delTable ); $joinTable = $this->tableName( $joinTable ); - $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar "; - + $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; if ( $conds != '*' ) { - $sql .= ' AND ' . $this->makeList( $conds, LIST_AND ); + $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); } + $sql .= ')'; - return $this->query( $sql, $fname ); + $this->query( $sql, $fname ); } /** @@ -1939,16 +2363,22 @@ abstract class DatabaseBase implements DatabaseType { * 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 supported, or a blank string otherwise + * @return string Returns the text of the low priority option if it is + * supported, or a blank string otherwise */ function lowPriorityOption() { return ''; } /** - * DELETE query wrapper + * DELETE query wrapper. * - * Use $conds == "*" to delete all rows + * @param $table Array Table name + * @param $conds String|Array of conditions. See $conds in DatabaseBase::select() for + * the format. Use $conds == "*" to delete all rows + * @param $fname String name of the calling function + * + * @return bool */ function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) { if ( !$conds ) { @@ -1966,13 +2396,33 @@ abstract class DatabaseBase implements DatabaseType { } /** - * 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 DatabaseBase::addQuotes() - * $conds may be "*" to copy the whole table - * srcTable may be an array of tables. + * INSERT SELECT wrapper. Takes data from a SELECT query and inserts it + * into another table. + * + * @param $destTable The table name to insert into + * @param $srcTable May be either a table name, or an array of table names + * to include in a join. + * + * @param $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() + * + * @param $conds Condition array. See $conds in DatabaseBase::select() for + * the details of the format of condition arrays. May be "*" to copy the + * whole table. + * + * @param $fname The function name of the caller, from __METHOD__ + * + * @param $insertOptions Options for the INSERT part of the query, see + * DatabaseBase::insert() for details. + * @param $selectOptions Options for the SELECT part of the query, see + * DatabaseBase::select() for details. + * + * @return ResultWrapper */ - function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseBase::insertSelect', + function insertSelect( $destTable, $srcTable, $varMap, $conds, + $fname = 'DatabaseBase::insertSelect', $insertOptions = array(), $selectOptions = array() ) { $destTable = $this->tableName( $destTable ); @@ -2023,6 +2473,8 @@ abstract class DatabaseBase implements DatabaseType { * @param $sql String: SQL query we will append the limit too * @param $limit Integer: the SQL limit * @param $offset Integer the SQL offset (default false) + * + * @return string */ function limitResult( $sql, $limit, $offset = false ) { if ( !is_numeric( $limit ) ) { @@ -2080,24 +2532,18 @@ abstract class DatabaseBase implements DatabaseType { * @param $orig String: column to modify * @param $old String: column to seek * @param $new String: column to replace with + * + * @return string */ function strreplace( $orig, $old, $new ) { return "REPLACE({$orig}, {$old}, {$new})"; } /** - * Convert a field to an unix timestamp - * - * @param $field String: field name - * @return String: SQL statement - */ - public function unixTimestamp( $field ) { - return "EXTRACT(epoch FROM $field)"; - } - - /** * Determines if the last failure was due to a deadlock * STUB + * + * @return bool */ function wasDeadlock() { return false; @@ -2107,6 +2553,8 @@ abstract class DatabaseBase implements DatabaseType { * Determines if the last query error was something that should be dealt * with by pinging the connection and reissuing the query. * STUB + * + * @return bool */ function wasErrorReissuable() { return false; @@ -2115,6 +2563,8 @@ abstract class DatabaseBase implements DatabaseType { /** * Determines if the last failure was due to the database being read-only. * STUB + * + * @return bool */ function wasReadOnlyError() { return false; @@ -2180,20 +2630,20 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Do a SELECT MASTER_POS_WAIT() + * Wait for the slave to catch up to a given master position. + * + * @param $pos DBMasterPos object + * @param $timeout Integer: the maximum number of seconds to wait for + * synchronisation * - * @param $pos MySQLMasterPos object - * @param $timeout Integer: the maximum number of seconds to wait for synchronisation + * @return An integer: 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( MySQLMasterPos $pos, $timeout ) { + function masterPosWait( DBMasterPos $pos, $timeout ) { $fname = 'DatabaseBase::masterPosWait'; wfProfileIn( $fname ); - # Commit any open transactions - if ( $this->mTrxLevel ) { - $this->commit(); - } - if ( !is_null( $this->mFakeSlaveLag ) ) { $wait = intval( ( $pos->pos - microtime( true ) + $this->mFakeSlaveLag ) * 1e6 ); @@ -2213,55 +2663,36 @@ abstract class DatabaseBase implements DatabaseType { } } - # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set - $encFile = $this->addQuotes( $pos->file ); - $encPos = intval( $pos->pos ); - $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)"; - $res = $this->doQuery( $sql ); + wfProfileOut( $fname ); - if ( $res && $row = $this->fetchRow( $res ) ) { - wfProfileOut( $fname ); - return $row[0]; - } else { - wfProfileOut( $fname ); - return false; - } + # Real waits are implemented in the subclass. + return 0; } /** - * Get the position of the master from SHOW SLAVE STATUS + * Get the replication position of this slave + * + * @return DBMasterPos, or false if this is not a slave. */ function getSlavePos() { if ( !is_null( $this->mFakeSlaveLag ) ) { $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; - return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos ); } else { + # Stub return false; } } /** - * Get the position of the master from SHOW MASTER STATUS + * Get the position of this master + * + * @return DBMasterPos, or false if this is not a master */ function getMasterPos() { if ( $this->mFakeMaster ) { return new MySQLMasterPos( 'fake', microtime( true ) ); - } - - $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' ); - $row = $this->fetchObject( $res ); - - if ( $row ) { - return new MySQLMasterPos( $row->File, $row->Position ); } else { return false; } @@ -2297,28 +2728,12 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Begin a transaction, committing any previously open transaction - * @deprecated use begin() - */ - function immediateBegin( $fname = 'DatabaseBase::immediateBegin' ) { - wfDeprecated( __METHOD__ ); - $this->begin(); - } - - /** - * Commit transaction, if one is open - * @deprecated use commit() - */ - function immediateCommit( $fname = 'DatabaseBase::immediateCommit' ) { - wfDeprecated( __METHOD__ ); - $this->commit(); - } - - /** * Creates a new table with structure copied from existing table * Note that unlike most database abstraction functions, this function does not * automatically append database prefix, because it works at a lower * abstraction level. + * The table names passed to this function shall not be quoted (this + * function calls addIdentifierQuotes when needed). * * @param $oldName String: name of table whose structure should be copied * @param $newName String: name of table to be created @@ -2326,19 +2741,46 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String: calling function name * @return Boolean: true if operation was successful */ - function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseBase::duplicateTableStructure' ) { - throw new MWException( 'DatabaseBase::duplicateTableStructure is not implemented in descendant class' ); + function duplicateTableStructure( $oldName, $newName, $temporary = false, + $fname = 'DatabaseBase::duplicateTableStructure' ) + { + throw new MWException( + 'DatabaseBase::duplicateTableStructure is not implemented in descendant class' ); } /** - * Return MW-style timestamp used for MySQL schema + * List all tables on the database + * + * @param $prefix Only show tables with this prefix, e.g. mw_ + * @param $fname String: calling function name + */ + function listTables( $prefix = null, $fname = 'DatabaseBase::listTables' ) { + throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' ); + } + + /** + * Convert a timestamp in one of the formats accepted by wfTimestamp() + * to the format used for inserting into timestamp fields in this DBMS. + * + * The result is unquoted, and needs to be passed through addQuotes() + * before it can be included in raw SQL. + * + * @return string */ function timestamp( $ts = 0 ) { return wfTimestamp( TS_MW, $ts ); } /** - * Local database timestamp format or null + * Convert a timestamp in one of the formats accepted by wfTimestamp() + * to the format used for inserting into timestamp fields in this DBMS. If + * NULL is input, it is passed through, allowing NULL values to be inserted + * into timestamp fields. + * + * The result is unquoted, and needs to be passed through addQuotes() + * before it can be included in raw SQL. + * + * @return string */ function timestampOrNull( $ts = null ) { if ( is_null( $ts ) ) { @@ -2349,7 +2791,15 @@ abstract class DatabaseBase implements DatabaseType { } /** - * @todo document + * Take the result from a query, and wrap it in a ResultWrapper if + * necessary. Boolean values are passed through as is, to indicate success + * of write queries or failure. + * + * Once upon a time, DatabaseBase::query() returned a bare MySQL result + * resource, and it was necessary to call this function to convert it to + * a wrapper. Nowadays, raw database objects are never exposed to external + * callers, so this is unnecessary in external code. For compatibility with + * old code, ResultWrapper objects are passed through unaltered. */ function resultObject( $result ) { if ( empty( $result ) ) { @@ -2382,8 +2832,12 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Get slave lag. - * Currently supported only by MySQL + * Get slave lag. Currently supported only by MySQL. + * + * Note that this function will generate a fatal error on many + * installations. Most callers should use LoadBalancer::safeGetLag() + * instead. + * * @return Database replication lag in seconds */ function getLag() { @@ -2391,30 +2845,29 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Get status information from SHOW STATUS in an associative array - */ - function getStatus( $which = "%" ) { - $res = $this->query( "SHOW STATUS LIKE '{$which}'" ); - $status = array(); - - foreach ( $res as $row ) { - $status[$row->Variable_name] = $row->Value; - } - - return $status; - } - - /** * Return the maximum number of items allowed in a list, or 0 for unlimited. + * + * return int */ function maxListLen() { return 0; } + /** + * Some DBMSs have a special format for inserting into blob fields, they + * 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(). + */ function encodeBlob( $b ) { return $b; } + /** + * Some DBMSs return a special placeholder object representing blob fields + * in result objects. Pass the object through this function to return the + * original string. + */ function decodeBlob( $b ) { return $b; } @@ -2431,21 +2884,23 @@ abstract class DatabaseBase implements DatabaseType { /** * Read and execute SQL commands from a file. - * Returns true on success, error string or exception on failure (depending on object's error ignore settings) + * + * Returns true on success, error string or exception on failure (depending + * on object's error ignore settings). + * * @param $filename String: File name to open * @param $lineCallback Callback: Optional function called before reading each line * @param $resultCallback Callback: Optional function called for each MySQL result - * @param $fname String: Calling function name or false if name should be generated dynamically - * using $filename + * @param $fname String: Calling function name or false if name should be + * generated dynamically using $filename */ function sourceFile( $filename, $lineCallback = false, $resultCallback = false, $fname = false ) { + wfSuppressWarnings(); $fp = fopen( $filename, 'r' ); + wfRestoreWarnings(); if ( false === $fp ) { - if ( !defined( "MEDIAWIKI_INSTALL" ) ) - throw new MWException( "Could not open \"{$filename}\".\n" ); - else - return "Could not open \"{$filename}\".\n"; + throw new MWException( "Could not open \"{$filename}\".\n" ); } if ( !$fname ) { @@ -2456,12 +2911,8 @@ abstract class DatabaseBase implements DatabaseType { $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname ); } catch ( MWException $e ) { - if ( defined( "MEDIAWIKI_INSTALL" ) ) { - $error = $e->getMessage(); - } else { - fclose( $fp ); - throw $e; - } + fclose( $fp ); + throw $e; } fclose( $fp ); @@ -2490,7 +2941,7 @@ abstract class DatabaseBase implements DatabaseType { /** * Set variables to be used in sourceFile/sourceStream, in preference to the - * ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at + * 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 $vars False, or array mapping variable name to value. @@ -2500,14 +2951,19 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Read and execute commands from an open file handle - * Returns true on success, error string or exception on failure (depending on object's error ignore settings) - * @param $fp String: File handle + * Read and execute commands from an open file handle. + * + * 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 line * @param $resultCallback Callback: Optional function called for each MySQL result * @param $fname String: Calling function name */ - function sourceStream( $fp, $lineCallback = false, $resultCallback = false, $fname = 'DatabaseBase::sourceStream' ) { + function sourceStream( $fp, $lineCallback = false, $resultCallback = false, + $fname = 'DatabaseBase::sourceStream' ) + { $cmd = ""; $done = false; $dollarquote = false; @@ -2517,7 +2973,7 @@ abstract class DatabaseBase implements DatabaseType { call_user_func( $lineCallback ); } - $line = trim( fgets( $fp, 1024 ) ); + $line = trim( fgets( $fp ) ); $sl = strlen( $line ) - 1; if ( $sl < 0 ) { @@ -2538,7 +2994,7 @@ abstract class DatabaseBase implements DatabaseType { $dollarquote = true; } } - else if ( !$dollarquote ) { + elseif ( !$dollarquote ) { if ( ';' == $line { $sl } && ( $sl < 2 || ';' != $line { $sl - 1 } ) ) { $done = true; $line = substr( $line, 0, $sl ); @@ -2574,16 +3030,19 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Database independent variable replacement, replaces a set of variables - * in a sql statement with their contents as given by $this->getSchemaVars(). - * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables - * - * '{$var}' should be used for text and is passed through the database's addQuotes method - * `{$var}` should be used for identifiers (eg: table and database names), it is passed through - * the database's addIdentifierQuotes method which can be overridden if the database - * uses something other than backticks. - * / *$var* / is just encoded, besides traditional dbprefix and tableoptions it's use should be avoided - * + * Database independent variable replacement. Replaces a set of variables + * in an SQL statement with their contents as given by $this->getSchemaVars(). + * + * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables. + * + * - '{$var}' should be used for text and is passed through the database's + * addQuotes method. + * - `{$var}` should be used for identifiers (eg: table and database names), + * it is passed through the database's addIdentifierQuotes method which + * can be overridden if the database uses something other than backticks. + * - / *$var* / is just encoded, besides traditional table prefix and + * table options its use should be avoided. + * * @param $ins String: SQL statement to replace variables in * @return String The new SQL statement with variables replaced */ @@ -2602,6 +3061,10 @@ abstract class DatabaseBase implements DatabaseType { /** * Replace variables in sourced SQL + * + * @param $ins string + * + * @return string */ protected function replaceVars( $ins ) { $ins = $this->replaceSchemaVars( $ins ); @@ -2631,8 +3094,11 @@ abstract class DatabaseBase implements DatabaseType { /** * Get schema variables to use if none have been set via setSchemaVars(). - * Override this in derived classes to provide variables for tables.sql - * and SQL patch files. + * + * Override this in derived classes to provide variables for tables.sql + * and SQL patch files. + * + * @return array */ protected function getDefaultSchemaVars() { return array(); @@ -2640,7 +3106,10 @@ abstract class DatabaseBase implements DatabaseType { /** * Table name callback - * @private + * + * @param $matches array + * + * @return string */ protected function tableNameCallback( $matches ) { return $this->tableName( $matches[1] ); @@ -2648,6 +3117,10 @@ abstract class DatabaseBase implements DatabaseType { /** * Index name callback + * + * @param $matches array + * + * @return string */ protected function indexNameCallback( $matches ) { return $this->indexName( $matches[1] ); @@ -2698,6 +3171,8 @@ abstract class DatabaseBase implements DatabaseType { * @param $write Array of tables to lock for write access * @param $method String name of caller * @param $lowPriority bool Whether to indicate writes to be LOW PRIORITY + * + * @return bool */ public function lockTables( $read, $write, $method, $lowPriority = true ) { return true; @@ -2707,12 +3182,31 @@ abstract class DatabaseBase implements DatabaseType { * Unlock specific tables * * @param $method String the caller + * + * @return bool */ public function unlockTables( $method ) { return true; } /** + * Delete a table + * @param $tableName string + * @param $fName string + * @return bool|ResultWrapper + */ + public function dropTable( $tableName, $fName = 'DatabaseBase::dropTable' ) { + if( !$this->tableExists( $tableName ) ) { + return false; + } + $sql = "DROP TABLE " . $this->tableName( $tableName ); + if( $this->cascadingDeletes() ) { + $sql .= " CASCADE"; + } + return $this->query( $sql, $fName ); + } + + /** * Get search engine class. All subclasses of this need to implement this * if they wish to use searching. * @@ -2723,537 +3217,40 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Allow or deny "big selects" for this session only. This is done by setting - * the sql_big_selects session variable. - * - * This is a MySQL-specific feature. + * Find out when 'infinity' is. Most DBMSes support this. This is a special + * keyword for timestamps in PostgreSQL, and works with CHAR(14) as well + * because "i" sorts after all numbers. * - * @param $value Mixed: true for allow, false for deny, or "default" to restore the initial value - */ - public function setBigSelects( $value = true ) { - // no-op - } -} - -/****************************************************************************** - * Utility classes - *****************************************************************************/ - -/** - * Utility class. - * @ingroup Database - */ -class DBObject { - public $mData; - - function __construct( $data ) { - $this->mData = $data; - } - - function isLOB() { - return false; - } - - function data() { - return $this->mData; - } -} - -/** - * Utility class - * @ingroup Database - * - * This allows us to distinguish a blob from a normal string and an array of strings - */ -class Blob { - private $mData; - - function __construct( $data ) { - $this->mData = $data; - } - - function fetch() { - return $this->mData; - } -} - -/** - * Base for all database-specific classes representing information about database fields - * @ingroup Database - */ -interface Field { - /** - * Field name - * @return string - */ - function name(); - - /** - * Name of table this field belongs to - * @return string - */ - function tableName(); - - /** - * Database type - * @return string - */ - function type(); - - /** - * Whether this field can store NULL values - * @return bool - */ - function isNullable(); -} - -/****************************************************************************** - * Error classes - *****************************************************************************/ - -/** - * Database error base class - * @ingroup Database - */ -class DBError extends MWException { - public $db; - - /** - * Construct a database error - * @param $db Database object which threw the error - * @param $error A simple error message to be used for debugging + * @return String */ - function __construct( DatabaseBase &$db, $error ) { - $this->db =& $db; - parent::__construct( $error ); - } - - function getText() { - global $wgShowDBErrorBacktrace; - - $s = $this->getMessage() . "\n"; - - if ( $wgShowDBErrorBacktrace ) { - $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n"; - } - - return $s; - } -} - -/** - * @ingroup Database - */ -class DBConnectionError extends DBError { - public $error; - - function __construct( DatabaseBase &$db, $error = 'unknown error' ) { - $msg = 'DB connection error'; - - if ( trim( $error ) != '' ) { - $msg .= ": $error"; - } - - $this->error = $error; - - parent::__construct( $db, $msg ); - } - - function useOutputPage() { - // Not likely to work - return false; - } - - function useMessageCache() { - // Not likely to work - return false; - } - - function getLogMessage() { - # Don't send to the exception log - return false; - } - - function getPageTitle() { - global $wgSitename, $wgLang; - - $header = "$wgSitename has a problem"; - - if ( $wgLang instanceof Language ) { - $header = htmlspecialchars( $wgLang->getMessage( 'dberr-header' ) ); - } - - return $header; - } - - function getHTML() { - global $wgLang, $wgMessageCache, $wgUseFileCache, $wgShowDBErrorBacktrace; - - $sorry = 'Sorry! This site is experiencing technical difficulties.'; - $again = 'Try waiting a few minutes and reloading.'; - $info = '(Can\'t contact the database server: $1)'; - - if ( $wgLang instanceof Language ) { - $sorry = htmlspecialchars( $wgLang->getMessage( 'dberr-problems' ) ); - $again = htmlspecialchars( $wgLang->getMessage( 'dberr-again' ) ); - $info = htmlspecialchars( $wgLang->getMessage( 'dberr-info' ) ); - } - - # No database access - if ( is_object( $wgMessageCache ) ) { - $wgMessageCache->disable(); - } - - if ( trim( $this->error ) == '' ) { - $this->error = $this->db->getProperty( 'mServer' ); - } - - $noconnect = "<p><strong>$sorry</strong><br />$again</p><p><small>$info</small></p>"; - $text = str_replace( '$1', $this->error, $noconnect ); - - if ( $wgShowDBErrorBacktrace ) { - $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ); - } - - $extra = $this->searchForm(); - - if ( $wgUseFileCache ) { - try { - $cache = $this->fileCachedPage(); - # Cached version on file system? - if ( $cache !== null ) { - # Hack: extend the body for error messages - $cache = str_replace( array( '</html>', '</body>' ), '', $cache ); - # Add cache notice... - $cachederror = "This is a cached copy of the requested page, and may not be up to date. "; - - # Localize it if possible... - if ( $wgLang instanceof Language ) { - $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) ); - } - - $warning = "<div style='color:red;font-size:150%;font-weight:bold;'>$cachederror</div>"; - - # Output cached page with notices on bottom and re-close body - return "{$cache}{$warning}<hr />$text<hr />$extra</body></html>"; - } - } catch ( MWException $e ) { - // Do nothing, just use the default page - } - } - - # Headers needed here - output is just the error message - return $this->htmlHeader() . "$text<hr />$extra" . $this->htmlFooter(); - } - - function searchForm() { - global $wgSitename, $wgServer, $wgLang; - - $usegoogle = "You can try searching via Google in the meantime."; - $outofdate = "Note that their indexes of our content may be out of date."; - $googlesearch = "Search"; - - if ( $wgLang instanceof Language ) { - $usegoogle = htmlspecialchars( $wgLang->getMessage( 'dberr-usegoogle' ) ); - $outofdate = htmlspecialchars( $wgLang->getMessage( 'dberr-outofdate' ) ); - $googlesearch = htmlspecialchars( $wgLang->getMessage( 'searchbutton' ) ); - } - - $search = htmlspecialchars( @$_REQUEST['search'] ); - - $server = htmlspecialchars( $wgServer ); - $sitename = htmlspecialchars( $wgSitename ); - - $trygoogle = <<<EOT -<div style="margin: 1.5em">$usegoogle<br /> -<small>$outofdate</small></div> -<!-- SiteSearch Google --> -<form method="get" action="http://www.google.com/search" id="googlesearch"> - <input type="hidden" name="domains" value="$server" /> - <input type="hidden" name="num" value="50" /> - <input type="hidden" name="ie" value="UTF-8" /> - <input type="hidden" name="oe" value="UTF-8" /> - - <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> -</form> -<!-- SiteSearch Google --> -EOT; - return $trygoogle; - } - - function fileCachedPage() { - global $wgTitle, $wgLang, $wgOut; - - if ( $wgOut->isDisabled() ) { - return; // Done already? - } - - $mainpage = 'Main Page'; - - if ( $wgLang instanceof Language ) { - $mainpage = htmlspecialchars( $wgLang->getMessage( 'mainpage' ) ); - } - - if ( $wgTitle ) { - $t =& $wgTitle; - } else { - $t = Title::newFromText( $mainpage ); - } - - $cache = new HTMLFileCache( $t ); - if ( $cache->isFileCached() ) { - return $cache->fetchPageText(); - } else { - return ''; - } + public function getInfinity() { + return 'infinity'; } - function htmlBodyOnly() { - return true; - } -} - -/** - * @ingroup Database - */ -class DBQueryError extends DBError { - public $error, $errno, $sql, $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: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . - "Query: $sql\n" . - "Function: $fname\n" . - "Error: $errno $error\n"; - - parent::__construct( $db, $message ); - - $this->error = $error; - $this->errno = $errno; - $this->sql = $sql; - $this->fname = $fname; - } - - function getText() { - global $wgShowDBErrorBacktrace; - - if ( $this->useMessageCache() ) { - $s = wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ), - htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n"; - - if ( $wgShowDBErrorBacktrace ) { - $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n"; - } - - return $s; - } else { - return parent::getText(); - } - } - - function getSQL() { - global $wgShowSQLErrors; - - if ( !$wgShowSQLErrors ) { - return $this->msg( 'sqlhidden', 'SQL hidden' ); - } else { - return $this->sql; - } - } - - function getLogMessage() { - # Don't send to the exception log - return false; - } - - function getPageTitle() { - return $this->msg( 'databaseerror', 'Database error' ); - } - - function getHTML() { - global $wgShowDBErrorBacktrace; - - if ( $this->useMessageCache() ) { - $s = wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ), - htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ); - } else { - $s = nl2br( htmlspecialchars( $this->getMessage() ) ); - } - - if ( $wgShowDBErrorBacktrace ) { - $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ); - } - - return $s; - } -} - -/** - * @ingroup Database - */ -class DBUnexpectedError extends DBError {} - - -/** - * Result wrapper for grabbing data queried by someone else - * @ingroup Database - */ -class ResultWrapper implements Iterator { - var $db, $result, $pos = 0, $currentRow = null; - /** - * Create a new result object from a result resource and a Database object + * Encode an expiry time + * + * @param $expiry String: timestamp for expiry, or the 'infinity' string + * @return String */ - function __construct( $database, $result ) { - $this->db = $database; - - if ( $result instanceof ResultWrapper ) { - $this->result = $result->result; + public function encodeExpiry( $expiry ) { + if ( $expiry == '' || $expiry == $this->getInfinity() ) { + return $this->getInfinity(); } else { - $this->result = $result; + return $this->timestamp( $expiry ); } } /** - * Get the number of rows in a result object - */ - function numRows() { - return $this->db->numRows( $this ); - } - - /** - * Fetch the next row from the given result object, in object form. - * Fields can be retrieved with $row->fieldname, with fields acting like - * member variables. + * Allow or deny "big selects" for this session only. This is done by setting + * the sql_big_selects session variable. * - * @return MySQL row object - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchObject() { - return $this->db->fetchObject( $this ); - } - - /** - * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. + * This is a MySQL-specific feature. * - * @return MySQL row object - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchRow() { - return $this->db->fetchRow( $this ); - } - - /** - * Free a result object + * @param $value Mixed: true for allow, false for deny, or "default" to + * restore the initial value */ - function free() { - $this->db->freeResult( $this ); - unset( $this->result ); - unset( $this->db ); - } - - /** - * Change the position of the cursor in a result object - * See mysql_data_seek() - */ - function seek( $row ) { - $this->db->dataSeek( $this, $row ); - } - - /********************* - * Iterator functions - * Note that using these in combination with the non-iterator functions - * above may cause rows to be skipped or repeated. - */ - - function rewind() { - if ( $this->numRows() ) { - $this->db->dataSeek( $this, 0 ); - } - $this->pos = 0; - $this->currentRow = null; - } - - function current() { - if ( is_null( $this->currentRow ) ) { - $this->next(); - } - return $this->currentRow; - } - - function key() { - return $this->pos; - } - - function next() { - $this->pos++; - $this->currentRow = $this->fetchObject(); - return $this->currentRow; - } - - function valid() { - return $this->current() !== false; - } -} - -/** - * Overloads the relevant methods of the real ResultsWrapper so it - * 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; - - function __construct( $array ) { - $this->result = $array; - } - - function numRows() { - return count( $this->result ); - } - - function fetchRow() { - $this->currentRow = $this->result[$this->pos++]; - return $this->currentRow; - } - - function seek( $row ) { - $this->pos = $row; - } - - function free() {} - - // Callers want to be able to access fields with $this->fieldName - function fetchObject() { - $this->currentRow = $this->result[$this->pos++]; - return (object)$this->currentRow; - } - - function rewind() { - $this->pos = 0; - $this->currentRow = null; - } -} - -/** - * 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 { - private $str; - - public function __construct( $s ) { - $this->str = $s; - } - - public function toString() { - return $this->str; + public function setBigSelects( $value = true ) { + // no-op } } diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php new file mode 100644 index 00000000..b7fb1b22 --- /dev/null +++ b/includes/db/DatabaseError.php @@ -0,0 +1,314 @@ +<?php + +/** + * Database error base class + * @ingroup Database + */ +class DBError extends MWException { + + /** + * @var DatabaseBase + */ + public $db; + + /** + * Construct a database error + * @param $db DatabaseBase object which threw the error + * @param $error String A simple error message to be used for debugging + */ + function __construct( DatabaseBase &$db, $error ) { + $this->db = $db; + parent::__construct( $error ); + } + + /** + * @param $html string + * @return string + */ + protected function getContentMessage( $html ) { + if ( $html ) { + return nl2br( htmlspecialchars( $this->getMessage() ) ); + } else { + return $this->getMessage(); + } + } + + /** + * @return string + */ + function getText() { + global $wgShowDBErrorBacktrace; + + $s = $this->getContentMessage( false ) . "\n"; + + if ( $wgShowDBErrorBacktrace ) { + $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n"; + } + + return $s; + } + + /** + * @return string + */ + function getHTML() { + global $wgShowDBErrorBacktrace; + + $s = $this->getContentMessage( true ); + + if ( $wgShowDBErrorBacktrace ) { + $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ); + } + + return $s; + } +} + +/** + * @ingroup Database + */ +class DBConnectionError extends DBError { + public $error; + + function __construct( DatabaseBase &$db, $error = 'unknown error' ) { + $msg = 'DB connection error'; + + if ( trim( $error ) != '' ) { + $msg .= ": $error"; + } + + $this->error = $error; + + parent::__construct( $db, $msg ); + } + + function useOutputPage() { + // Not likely to work + return false; + } + + function msg( $key, $fallback /*[, params...] */ ) { + global $wgLang; + + $args = array_slice( func_get_args(), 2 ); + + if ( $this->useMessageCache() ) { + $message = $wgLang->getMessage( $key ); + } else { + $message = $fallback; + } + return wfMsgReplaceArgs( $message, $args ); + } + + function getLogMessage() { + # Don't send to the exception log + return false; + } + + /** + * @return string + */ + function getPageTitle() { + global $wgSitename; + return htmlspecialchars( $this->msg( 'dberr-header', "$wgSitename has a problem" ) ); + } + + /** + * @return string + */ + function getHTML() { + global $wgShowDBErrorBacktrace; + + $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.' ) ); + $info = htmlspecialchars( $this->msg( 'dberr-info', '(Can\'t contact the database server: $1)' ) ); + + # No database access + MessageCache::singleton()->disable(); + + if ( trim( $this->error ) == '' ) { + $this->error = $this->db->getProperty( 'mServer' ); + } + + $this->error = Html::element( 'span', array( 'dir' => 'ltr' ), $this->error ); + + $noconnect = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>"; + $text = str_replace( '$1', $this->error, $noconnect ); + + if ( $wgShowDBErrorBacktrace ) { + $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ); + } + + $extra = $this->searchForm(); + + return "$text<hr />$extra"; + } + + public function reportHTML(){ + global $wgUseFileCache; + + # 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? + if ( $cache !== null ) { + # 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;">'. + htmlspecialchars( $this->msg( 'dberr-cachederror', + '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 + echo "{$cache}<hr />{$this->getHTML()}</body></html>"; + return; + } + } catch ( MWException $e ) { + // Do nothing, just use the default page + } + } + + # We can't, cough and die in the usual fashion + return parent::reportHTML(); + } + + /** + * @return string + */ + function searchForm() { + global $wgSitename, $wgServer, $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.' ) ); + $googlesearch = htmlspecialchars( $this->msg( 'searchbutton', 'Search' ) ); + + $search = htmlspecialchars( $wgRequest->getVal( 'search' ) ); + + $server = htmlspecialchars( $wgServer ); + $sitename = htmlspecialchars( $wgSitename ); + + $trygoogle = <<<EOT +<div style="margin: 1.5em">$usegoogle<br /> +<small>$outofdate</small></div> +<!-- SiteSearch Google --> +<form method="get" action="http://www.google.com/search" id="googlesearch"> + <input type="hidden" name="domains" value="$server" /> + <input type="hidden" name="num" value="50" /> + <input type="hidden" name="ie" value="UTF-8" /> + <input type="hidden" name="oe" value="UTF-8" /> + + <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> +</form> +<!-- SiteSearch Google --> +EOT; + return $trygoogle; + } + + /** + * @return string + */ + private function fileCachedPage() { + global $wgTitle, $wgOut; + + if ( $wgOut->isDisabled() ) { + return; // Done already? + } + + if ( $wgTitle ) { + $t =& $wgTitle; + } else { + $t = Title::newFromText( $this->msg( 'mainpage', 'Main Page' ) ); + } + + $cache = new HTMLFileCache( $t ); + if ( $cache->isFileCached() ) { + return $cache->fetchPageText(); + } else { + return ''; + } + } +} + +/** + * @ingroup Database + */ +class DBQueryError extends DBError { + public $error, $errno, $sql, $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: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . + "Query: $sql\n" . + "Function: $fname\n" . + "Error: $errno $error\n"; + global $wgShowDBErrorBacktrace; + if( $wgShowDBErrorBacktrace ) { + $message .= $this->getTraceAsString(); + } + parent::__construct( $db, $message ); + + $this->error = $error; + $this->errno = $errno; + $this->sql = $sql; + $this->fname = $fname; + } + + /** + * @param $html string + * @return string + */ + function getContentMessage( $html ) { + if ( $this->useMessageCache() ) { + if ( $html ) { + $msg = 'dberrortext'; + $sql = htmlspecialchars( $this->getSQL() ); + $fname = htmlspecialchars( $this->fname ); + $error = htmlspecialchars( $this->error ); + } else { + $msg = 'dberrortextcl'; + $sql = $this->getSQL(); + $fname = $this->fname; + $error = $this->error; + } + return wfMsg( $msg, $sql, $fname, $this->errno, $error ); + } else { + return parent::getContentMessage( $html ); + } + } + + /** + * @return String + */ + function getSQL() { + global $wgShowSQLErrors; + + if ( !$wgShowSQLErrors ) { + return $this->msg( 'sqlhidden', 'SQL hidden' ); + } else { + return $this->sql; + } + } + + function getLogMessage() { + # Don't send to the exception log + return false; + } + + /** + * @return String + */ + function getPageTitle() { + return $this->msg( 'databaseerror', 'Database error' ); + } +} + +/** + * @ingroup Database + */ +class DBUnexpectedError extends DBError {} diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php index becca11e..147b9d59 100644 --- a/includes/db/DatabaseIbm_db2.php +++ b/includes/db/DatabaseIbm_db2.php @@ -114,7 +114,7 @@ class DatabaseIbm_db2 extends DatabaseBase { protected $mPHPError = false; protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname; - protected $mOut, $mOpened = false; + protected $mOpened = false; protected $mTablePrefix; protected $mFlags; @@ -144,7 +144,7 @@ class DatabaseIbm_db2 extends DatabaseBase { public $mStmtOptions = array(); /** Default schema */ - const USE_GLOBAL = 'mediawiki'; + const USE_GLOBAL = 'get from global'; /** Option that applies to nothing */ const NONE_OPTION = 0x00; @@ -268,6 +268,10 @@ class DatabaseIbm_db2 extends DatabaseBase { } // configure the connection and statement objects + /* + $this->setDB2Option( 'cursor', 'DB2_SCROLLABLE', + self::CONN_OPTION | self::STMT_OPTION ); + */ $this->setDB2Option( 'db2_attr_case', 'DB2_CASE_LOWER', self::CONN_OPTION | self::STMT_OPTION ); $this->setDB2Option( 'deferred_prepare', 'DB2_DEFERRED_PREPARE_ON', @@ -321,27 +325,17 @@ class DatabaseIbm_db2 extends DatabaseBase { * @return a fresh connection */ public function open( $server, $user, $password, $dbName ) { - // Load the port number - global $wgDBport; wfProfileIn( __METHOD__ ); - // Load IBM DB2 driver if missing + # Load IBM DB2 driver if missing wfDl( 'ibm_db2' ); - // Test for IBM DB2 support, to avoid suppressed fatal error + # Test for IBM DB2 support, to avoid suppressed fatal error if ( !function_exists( 'db2_connect' ) ) { - $error = <<<ERROR -DB2 functions missing, have you enabled the ibm_db2 extension for PHP? - -ERROR; - $this->installPrint( $error ); - $this->reportConnectionError( $error ); + throw new DBConnectionError( $this, "DB2 functions missing, have you enabled the ibm_db2 extension for PHP?" ); } - if ( strlen( $user ) < 1 ) { - wfProfileOut( __METHOD__ ); - return null; - } + global $wgDBport; // Close existing connection $this->close(); @@ -354,24 +348,26 @@ ERROR; $this->openUncataloged( $dbName, $user, $password, $server, $port ); - // Apply connection config - db2_set_option( $this->mConn, $this->mConnOptions, 1 ); - // Some MediaWiki code is still transaction-less (?). - // The strategy is to keep AutoCommit on for that code - // but switch it off whenever a transaction is begun. - db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON ); - if ( !$this->mConn ) { $this->installPrint( "DB connection error\n" ); $this->installPrint( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); $this->installPrint( $this->lastError() . "\n" ); - wfProfileOut( __METHOD__ ); - return null; + wfDebug( "DB connection error\n" ); + wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); + wfDebug( $this->lastError() . "\n" ); + throw new DBConnectionError( $this, $this->lastError() ); } + // Apply connection config + db2_set_option( $this->mConn, $this->mConnOptions, 1 ); + // Some MediaWiki code is still transaction-less (?). + // The strategy is to keep AutoCommit on for that code + // but switch it off whenever a transaction is begun. + db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON ); + $this->mOpened = true; $this->applySchema(); @@ -383,7 +379,9 @@ ERROR; * Opens a cataloged database connection, sets mConn */ protected function openCataloged( $dbName, $user, $password ) { - @$this->mConn = db2_pconnect( $dbName, $user, $password ); + wfSuppressWarnings(); + $this->mConn = db2_pconnect( $dbName, $user, $password ); + wfRestoreWarnings(); } /** @@ -391,16 +389,10 @@ ERROR; */ protected function openUncataloged( $dbName, $user, $password, $server, $port ) { - $str = "DRIVER={IBM DB2 ODBC DRIVER};"; - $str .= "DATABASE=$dbName;"; - $str .= "HOSTNAME=$server;"; - // port was formerly validated to not be 0 - $str .= "PORT=$port;"; - $str .= "PROTOCOL=TCPIP;"; - $str .= "UID=$user;"; - $str .= "PWD=$password;"; - - @$this->mConn = db2_pconnect( $str, $user, $password ); + $dsn = "DRIVER={IBM DB2 ODBC DRIVER};DATABASE=$dbName;CHARSET=UTF-8;HOSTNAME=$server;PORT=$port;PROTOCOL=TCPIP;UID=$user;PWD=$password;"; + wfSuppressWarnings(); + $this->mConn = db2_pconnect($dsn, "", "", array()); + wfRestoreWarnings(); } /** @@ -420,23 +412,6 @@ ERROR; } /** - * Returns a fresh instance of this class - * - * @param $server String: hostname of database server - * @param $user String: username - * @param $password String - * @param $dbName String: database name on the server - * @param $flags Integer: database behaviour flags (optional, unused) - * @return DatabaseIbm_db2 object - */ - static function newFromParams( $server, $user, $password, $dbName, - $flags = 0 ) - { - return new DatabaseIbm_db2( $server, $user, $password, $dbName, - $flags ); - } - - /** * Retrieves the most current database error * Forces a database rollback */ @@ -482,15 +457,19 @@ ERROR; * The DBMS-dependent part of query() * @param $sql String: SQL query. * @return object Result object for fetch functions or false on failure - * @access private */ - /*private*/ - public function doQuery( $sql ) { + protected function doQuery( $sql ) { $this->applySchema(); - + + // Needed to handle any UTF-8 encoding issues in the raw sql + // Note that we fully support prepared statements for DB2 + // prepare() and execute() should be used instead of doQuery() whenever possible + $sql = utf8_decode($sql); + $ret = db2_exec( $this->mConn, $sql, $this->mStmtOptions ); if( $ret == false ) { $error = db2_stmt_errormsg(); + $this->installPrint( "<pre>$sql</pre>" ); $this->installPrint( $error ); throw new DBUnexpectedError( $this, 'SQL error: ' @@ -515,17 +494,18 @@ ERROR; */ public function tableExists( $table ) { $schema = $this->mSchema; - $sql = <<< EOF -SELECT COUNT( * ) FROM SYSIBM.SYSTABLES ST -WHERE ST.NAME = '$table' AND ST.CREATOR = '$schema' -EOF; + + $sql = "SELECT COUNT( * ) FROM SYSIBM.SYSTABLES ST WHERE ST.NAME = '" . + strtoupper( $table ) . + "' AND ST.CREATOR = '" . + strtoupper( $schema ) . "'"; $res = $this->query( $sql ); if ( !$res ) { return false; } // If the table exists, there should be one of it - @$row = $this->fetchRow( $res ); + $row = $this->fetchRow( $res ); $count = $row[0]; if ( $count == '1' || $count == 1 ) { return true; @@ -547,7 +527,9 @@ EOF; if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @$row = db2_fetch_object( $res ); + wfSuppressWarnings(); + $row = db2_fetch_object( $res ); + wfRestoreWarnings(); if( $this->lastErrno() ) { throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); @@ -567,51 +549,17 @@ EOF; if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @$row = db2_fetch_array( $res ); - if ( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' - . htmlspecialchars( $this->lastError() ) ); - } - return $row; - } - - /** - * Create tables, stored procedures, and so on - */ - public function setup_database() { - try { - // TODO: switch to root login if available - - // Switch into the correct namespace - $this->applySchema(); - $this->begin(); - - $res = $this->sourceFile( "../maintenance/ibm_db2/tables.sql" ); - if ( $res !== true ) { - print ' <b>FAILED</b>: ' . htmlspecialchars( $res ) . '</li>'; - } else { - print ' done</li>'; - } - $res = $this->sourceFile( "../maintenance/ibm_db2/foreignkeys.sql" ); - if ( $res !== true ) { - print ' <b>FAILED</b>: ' . htmlspecialchars( $res ) . '</li>'; - } else { - print '<li>Foreign keys done</li>'; - } - - // TODO: populate interwiki links - - if ( $this->lastError() ) { - $this->installPrint( - 'Errors encountered during table creation -- rolled back' ); - $this->installPrint( 'Please install again' ); - $this->rollback(); - } else { - $this->commit(); + if ( db2_num_rows( $res ) > 0) { + wfSuppressWarnings(); + $row = db2_fetch_array( $res ); + wfRestoreWarnings(); + if ( $this->lastErrno() ) { + throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' + . htmlspecialchars( $this->lastError() ) ); } - } catch ( MWException $mwe ) { - print "<br><pre>$mwe</pre><br>"; + return $row; } + return false; } /** @@ -797,9 +745,10 @@ EOF; * Handle reserved keyword replacement in table names * * @param $name Object + * @param $name Boolean * @return String */ - public function tableName( $name ) { + public function tableName( $name, $quoted = true ) { // we want maximum compatibility with MySQL schema return $name; } @@ -915,9 +864,9 @@ EOF; } else { $sql .= '( ?' . str_repeat( ',?', $key_count-1 ) . ' )'; } - //$this->installPrint( "Preparing the following SQL:" ); - //$this->installPrint( "$sql" ); - //$this->installPrint( print_r( $args, true )); + $this->installPrint( "Preparing the following SQL:" ); + $this->installPrint( "$sql" ); + $this->installPrint( print_r( $args, true )); $stmt = $this->prepare( $sql ); // start a transaction/enter transaction mode @@ -990,18 +939,22 @@ EOF; */ private function removeNullPrimaryKeys( $table, $args ) { $schema = $this->mSchema; + // find out the primary keys - $keyres = db2_primary_keys( $this->mConn, null, strtoupper( $schema ), - strtoupper( $table ) - ); + $keyres = $this->doQuery( "SELECT NAME FROM SYSIBM.SYSCOLUMNS WHERE TBNAME = '" + . strtoupper( $table ) + . "' AND TBCREATOR = '" + . strtoupper( $schema ) + . "' AND KEYSEQ > 0" ); + $keys = array(); for ( - $row = $this->fetchObject( $keyres ); + $row = $this->fetchRow( $keyres ); $row != null; - $row = $this->fetchObject( $keyres ) + $row = $this->fetchRow( $keyres ) ) { - $keys[] = strtolower( $row->column_name ); + $keys[] = strtolower( $row[0] ); } // remove primary keys foreach ( $args as $ai => $row ) { @@ -1084,66 +1037,6 @@ EOF; } /** - * Simulates REPLACE with a DELETE followed by INSERT - * @param $table Object - * @param $uniqueIndexes Array consisting of indexes and arrays of indexes - * @param $rows Array: rows to insert - * @param $fname String: name of the function for profiling - * @return nothing - */ - function replace( $table, $uniqueIndexes, $rows, - $fname = 'DatabaseIbm_db2::replace' ) - { - $table = $this->tableName( $table ); - - if ( count( $rows )==0 ) { - return; - } - - # Single row case - if ( !is_array( reset( $rows ) ) ) { - $rows = array( $rows ); - } - - foreach( $rows as $row ) { - # Delete rows which collide - if ( $uniqueIndexes ) { - $sql = "DELETE FROM $table WHERE "; - $first = true; - foreach ( $uniqueIndexes as $index ) { - if ( $first ) { - $first = false; - $sql .= '( '; - } else { - $sql .= ' ) OR ( '; - } - if ( is_array( $index ) ) { - $first2 = true; - foreach ( $index as $col ) { - if ( $first2 ) { - $first2 = false; - } else { - $sql .= ' AND '; - } - $sql .= $col . '=' . $this->addQuotes( $row[$col] ); - } - } else { - $sql .= $index . '=' . $this->addQuotes( $row[$index] ); - } - } - $sql .= ' )'; - $this->query( $sql, $fname ); - } - - # Now insert the row - $sql = "INSERT INTO $table ( " - . $this->makeList( array_keys( $row ), LIST_NAMES ) - .' ) VALUES ( ' . $this->makeList( $row, LIST_COMMA ) . ' )'; - $this->query( $sql, $fname ); - } - } - - /** * Returns the number of rows in the result set * Has to be called right after the corresponding select query * @param $res Object result set @@ -1153,6 +1046,7 @@ EOF; if ( $res instanceof ResultWrapper ) { $res = $res->result; } + if ( $this->mNumRows ) { return $this->mNumRows; } else { @@ -1186,7 +1080,10 @@ EOF; if ( $res instanceof ResultWrapper ) { $res = $res->result; } - if ( !@db2_free_result( $res ) ) { + wfSuppressWarnings(); + $ok = db2_free_result( $res ); + wfRestoreWarnings(); + if ( !$ok ) { throw new DBUnexpectedError( $this, "Unable to free DB2 result\n" ); } } @@ -1365,14 +1262,6 @@ EOF; ###################################### /** * Not implemented - * @return string '' - */ - public function getStatus( $which = '%' ) { - $this->installPrint( 'Not implemented for DB2: getStatus()' ); - return ''; - } - /** - * Not implemented * @return string $sql */ public function limitResultForUpdate( $sql, $num ) { @@ -1496,39 +1385,6 @@ SQL; } /** - * DELETE where the condition is a join - * @param $delTable String: deleting from this table - * @param $joinTable String: using data from this table - * @param $delVar String: variable in deleteable table - * @param $joinVar String: variable in data table - * @param $conds Array: conditionals for join table - * @param $fname String: function name for profiling - */ - public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, - $conds, $fname = "DatabaseIbm_db2::deleteJoin" ) - { - if ( !$conds ) { - throw new DBUnexpectedError( $this, - 'DatabaseIbm_db2::deleteJoin() called with empty $conds' ); - } - - $delTable = $this->tableName( $delTable ); - $joinTable = $this->tableName( $joinTable ); - $sql = <<<SQL -DELETE FROM $delTable -WHERE $delVar IN ( - SELECT $joinVar FROM $joinTable - -SQL; - if ( $conds != '*' ) { - $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); - } - $sql .= ' )'; - - $this->query( $sql, $fname ); - } - - /** * Description is left as an exercise for the reader * @param $b Mixed: data to be encoded * @return IBM_DB2Blob diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 41ba2d08..cbdf89ca 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -17,6 +17,8 @@ class DatabaseMssql extends DatabaseBase { var $mLastResult = NULL; var $mAffectedRows = NULL; + var $mPort; + function cascadingDeletes() { return true; } @@ -42,10 +44,6 @@ class DatabaseMssql extends DatabaseBase { return false; } - static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { - return new DatabaseMssql( $server, $user, $password, $dbName, $flags ); - } - /** * Usually aborts on failure */ @@ -83,7 +81,7 @@ class DatabaseMssql extends DatabaseBase { $ntAuthPassTest = strtolower( $password ); // Decide which auth scenerio to use - if( ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) ){ + if( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ){ // Don't add credentials to $connectionInfo } else { $connectionInfo['UID'] = $user; @@ -91,7 +89,9 @@ class DatabaseMssql extends DatabaseBase { } // End NT Auth Hack - $this->mConn = @sqlsrv_connect( $server, $connectionInfo ); + wfSuppressWarnings(); + $this->mConn = sqlsrv_connect( $server, $connectionInfo ); + wfRestoreWarnings(); if ( $this->mConn === false ) { wfDebug( "DB connection error\n" ); @@ -117,7 +117,7 @@ class DatabaseMssql extends DatabaseBase { } } - function doQuery( $sql ) { + protected function doQuery( $sql ) { wfDebug( "SQL: [$sql]\n" ); $this->offset = 0; @@ -241,16 +241,18 @@ class DatabaseMssql extends DatabaseBase { function lastError() { if ( $this->mConn ) { return $this->getErrors(); - } - else { + } else { return "No database connection"; } } function lastErrno() { $err = sqlsrv_errors( SQLSRV_ERR_ALL ); - if ( $err[0] ) return $err[0]['code']; - else return 0; + if ( $err[0] ) { + return $err[0]['code']; + } else { + return 0; + } } function affectedRows() { @@ -321,7 +323,6 @@ class DatabaseMssql extends DatabaseBase { return $rows; } - /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure @@ -344,7 +345,7 @@ class DatabaseMssql extends DatabaseBase { $row->Column_name = trim( $col ); $result[] = clone $row; } - } else if ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) { + } elseif ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) { $row->Non_unique = 0; $cols = explode( ", ", $row->index_keys ); foreach ( $cols as $col ) { @@ -383,7 +384,6 @@ class DatabaseMssql extends DatabaseBase { $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 @@ -421,7 +421,6 @@ class DatabaseMssql extends DatabaseBase { $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; @@ -436,7 +435,7 @@ class DatabaseMssql extends DatabaseBase { // 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 == true ) { + if ( $ignoreClause ) { $prival = $a[$keys[0]]; $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')"; } @@ -453,14 +452,14 @@ class DatabaseMssql extends DatabaseBase { $sql .= ','; } if ( is_string( $value ) ) { - $sql .= $this->addIdentifierQuotes( $value ); + $sql .= $this->addQuotes( $value ); } elseif ( is_null( $value ) ) { $sql .= 'null'; } elseif ( is_array( $value ) || is_object( $value ) ) { if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) { - $sql .= $this->addIdentifierQuotes( $value->fetch() ); + $sql .= $this->addQuotes( $value ); } else { - $sql .= $this->addIdentifierQuotes( serialize( $value ) ); + $sql .= $this->addQuotes( serialize( $value ) ); } } else { $sql .= $value; @@ -497,12 +496,11 @@ class DatabaseMssql extends DatabaseBase { * srcTable may be an array of tables. */ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect', - $insertOptions = array(), $selectOptions = array() ) - { + $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 ); + throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), /*$sql*/ '', $fname ); } elseif ( $ret != NULL ) { // remember number of rows affected $this->mAffectedRows = sqlsrv_rows_affected( $ret ); @@ -512,35 +510,6 @@ class DatabaseMssql extends DatabaseBase { } /** - * Format a table name ready for use in constructing an SQL query - * - * This does two important things: it brackets table names which as necessary, - * and it adds a table prefix if there is one. - * - * All functions of this object which require a table name call this function - * themselves. Pass the canonical name to such functions. This is only needed - * when calling query() directly. - * - * @param $name String: database table name - */ - function tableName( $name ) { - global $wgSharedDB; - # Skip quoted literals - if ( $name != '' && $name { 0 } != '[' ) { - if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) { - $name = "{$this->mTablePrefix}$name"; - } - if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) { - $name = "[$wgSharedDB].[$name]"; - } else { - # Standard quoting - if ( $name != '' ) $name = "[$name]"; - } - } - return $name; - } - - /** * Return the next in a sequence, save the value for retrieval via insertId() */ function nextSequenceValue( $seqName ) { @@ -570,82 +539,6 @@ class DatabaseMssql extends DatabaseBase { } } - - # REPLACE query wrapper - # MSSQL simulates this with a DELETE followed by INSERT - # $row is the row to insert, an associative array - # $uniqueIndexes is an array of indexes. Each element may be either a - # field name or an array of field names - # - # It may be more efficient to leave off unique indexes which are unlikely to collide. - # However if you do this, you run the risk of encountering errors which wouldn't have - # occurred in MySQL - function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMssql::replace' ) { - $table = $this->tableName( $table ); - - if ( count( $rows ) == 0 ) { - return; - } - - # Single row case - if ( !is_array( reset( $rows ) ) ) { - $rows = array( $rows ); - } - - foreach ( $rows as $row ) { - # Delete rows which collide - if ( $uniqueIndexes ) { - $sql = "DELETE FROM $table WHERE "; - $first = true; - foreach ( $uniqueIndexes as $index ) { - if ( $first ) { - $first = false; - $sql .= "("; - } else { - $sql .= ') OR ('; - } - if ( is_array( $index ) ) { - $first2 = true; - foreach ( $index as $col ) { - if ( $first2 ) { - $first2 = false; - } else { - $sql .= ' AND '; - } - $sql .= $col . '=' . $this->addQuotes( $row[$col] ); - } - } else { - $sql .= $index . '=' . $this->addQuotes( $row[$index] ); - } - } - $sql .= ')'; - $this->query( $sql, $fname ); - } - - # Now insert the row - $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) . ') VALUES (' . - $this->makeList( $row, LIST_COMMA ) . ')'; - $this->query( $sql, $fname ); - } - } - - # DELETE where the condition is a join - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseMssql::deleteJoin" ) { - if ( !$conds ) { - throw new DBUnexpectedError( $this, 'DatabaseMssql::deleteJoin() called with empty $conds' ); - } - - $delTable = $this->tableName( $delTable ); - $joinTable = $this->tableName( $joinTable ); - $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; - if ( $conds != '*' ) { - $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); - } - $sql .= ')'; - - $this->query( $sql, $fname ); - } - # Returns the size of a text field, or -1 for "unlimited" function textFieldSize( $table, $field ) { $table = $this->tableName( $table ); @@ -654,7 +547,9 @@ class DatabaseMssql extends DatabaseBase { $res = $this->query( $sql ); $row = $this->fetchRow( $res ); $size = -1; - if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) $size = $row['CHARACTER_MAXIMUM_LENGTH']; + if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) { + $size = $row['CHARACTER_MAXIMUM_LENGTH']; + } return $size; } @@ -713,7 +608,6 @@ class DatabaseMssql extends DatabaseBase { return $sql; } - function timestamp( $ts = 0 ) { return wfTimestamp( TS_ISO_8601, $ts ); } @@ -731,7 +625,9 @@ class DatabaseMssql extends DatabaseBase { function getServerVersion() { $server_info = sqlsrv_server_info( $this->mConn ); $version = 'Error'; - if ( isset( $server_info['SQLServerVersion'] ) ) $version = $server_info['SQLServerVersion']; + if ( isset( $server_info['SQLServerVersion'] ) ) { + $version = $server_info['SQLServerVersion']; + } return $version; } @@ -742,10 +638,11 @@ class DatabaseMssql extends DatabaseBase { print( "Error in tableExists query: " . $this->getErrors() ); return false; } - if ( sqlsrv_fetch( $res ) ) + if ( sqlsrv_fetch( $res ) ) { return true; - else + } else { return false; + } } /** @@ -759,10 +656,11 @@ class DatabaseMssql extends DatabaseBase { print( "Error in fieldExists query: " . $this->getErrors() ); return false; } - if ( sqlsrv_fetch( $res ) ) + if ( sqlsrv_fetch( $res ) ) { return true; - else + } else { return false; + } } function fieldInfo( $table, $field ) { @@ -780,10 +678,6 @@ class DatabaseMssql extends DatabaseBase { return false; } - public function unixTimestamp( $field ) { - return "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),$field)"; - } - /** * Begin a transaction, committing any previously open transaction */ @@ -809,48 +703,6 @@ class DatabaseMssql extends DatabaseBase { $this->mTrxLevel = 0; } - function setup_database() { - global $wgDBuser; - - // Make sure that we can write to the correct schema - $ctest = "mediawiki_test_table"; - if ( $this->tableExists( $ctest ) ) { - $this->doQuery( "DROP TABLE $ctest" ); - } - $SQL = "CREATE TABLE $ctest (a int)"; - $res = $this->doQuery( $SQL ); - if ( !$res ) { - print "<b>FAILED</b>. Make sure that the user " . htmlspecialchars( $wgDBuser ) . " can write to the database</li>\n"; - dieout( ); - } - $this->doQuery( "DROP TABLE $ctest" ); - - $res = $this->sourceFile( "../maintenance/mssql/tables.sql" ); - if ( $res !== true ) { - echo " <b>FAILED</b></li>"; - dieout( htmlspecialchars( $res ) ); - } - - # Avoid the non-standard "REPLACE INTO" syntax - $f = fopen( "../maintenance/interwiki.sql", 'r' ); - if ( $f == false ) { - dieout( "<li>Could not find the interwiki.sql file" ); - } - # We simply assume it is already empty as we have just created it - $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES "; - while ( ! feof( $f ) ) { - $line = fgets( $f, 1024 ); - $matches = array(); - if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) { - continue; - } - $this->query( "$SQL $matches[1],$matches[2])" ); - } - print " (table interwiki successfully populated)...\n"; - - $this->commit(); - } - /** * Escapes a identifier for use inm SQL. * Throws an exception if it is invalid. @@ -957,12 +809,12 @@ class DatabaseMssql extends DatabaseBase { $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')'; $retJOIN[] = $tableClause; // Is there an INDEX clause? - } else if ( isset( $use_index_safe[$table] ) ) { + } 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? - } else if ( isset( $join_conds_safe[$table] ) ) { + } 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; @@ -990,6 +842,15 @@ class DatabaseMssql extends DatabaseBase { } } + public function addIdentifierQuotes( $s ) { + // http://msdn.microsoft.com/en-us/library/aa223962.aspx + return '[' . $s . ']'; + } + + public function isQuotedIdentifier( $name ) { + return $name[0] == '[' && substr( $name, -1, 1 ) == ']'; + } + function selectDB( $db ) { return ( $this->query( "SET DATABASE $db" ) !== false ); } @@ -1012,11 +873,19 @@ class DatabaseMssql extends DatabaseBase { } } - if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}"; - if ( isset( $options['HAVING'] ) ) $tailOpts .= " HAVING {$options['GROUP BY']}"; - if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}"; + if ( isset( $options['GROUP BY'] ) ) { + $tailOpts .= " GROUP BY {$options['GROUP BY']}"; + } + if ( isset( $options['HAVING'] ) ) { + $tailOpts .= " HAVING {$options['GROUP BY']}"; + } + if ( isset( $options['ORDER BY'] ) ) { + $tailOpts .= " ORDER BY {$options['ORDER BY']}"; + } - if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; + if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) { + $startOpts .= 'DISTINCT'; + } // we want this to be compatible with the output of parent::makeSelectOptions() return array( $startOpts, '' , $tailOpts, '' ); @@ -1037,6 +906,14 @@ class DatabaseMssql extends DatabaseBase { return "SearchMssql"; } + /** + * Since MSSQL doesn't recognize the infinity keyword, set date manually. + * @todo Remove magic date + */ + public function getInfinity() { + return '3000-01-31 00:00:00.000'; + } + } // end DatabaseMssql class /** @@ -1051,9 +928,10 @@ class MssqlField implements Field { $this->tablename = $info['TABLE_NAME']; $this->default = $info['COLUMN_DEFAULT']; $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; - $this->nullable = ( strtolower( $info['IS_NULLABLE'] ) == 'no' ) ? false:true; + $this->nullable = !( strtolower( $info['IS_NULLABLE'] ) == 'no' ); $this->type = $info['DATA_TYPE']; } + function name() { return $this->name; } @@ -1087,26 +965,29 @@ class MssqlField implements Field { */ 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 ); - while ( $row = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC ) ) { - 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" ); + public function __construct( $queryresult = false ) { + $this->mCursor = 0; + $this->mRows = array(); + $this->mNumFields = sqlsrv_num_fields( $queryresult ); + $this->mFieldMeta = sqlsrv_field_metadata( $queryresult ); + + $rows = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC ); + + 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 } - $this->mRows[] = $row;// read results into memory, cursors are not supported } + $this->mRowCount = count( $this->mRows ); + sqlsrv_free_stmt( $queryresult ); } - $this->mRowCount = count( $this->mRows ); - sqlsrv_free_stmt( $queryresult ); - } - private function array_to_obj( $array, &$obj ) { + private function array_to_obj( $array, &$obj ) { foreach ( $array as $key => $value ) { if ( is_array( $value ) ) { $obj->$key = new stdClass(); @@ -1118,109 +999,108 @@ class MssqlResult { } } return $obj; - } - - public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) { - if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) { - return false; } - $arrNum = array(); - if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) { - foreach ( $this->mRows[$this->mCursor] as $value ) { - $arrNum[] = $value; + + public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) { + if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) { + 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; + } + + public function get( $pos, $fld ) { + return $this->mRows[$pos][$fld]; } - 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; - } - - public function get( $pos, $fld ) { - return $this->mRows[$pos][$fld]; - } - - public function numrows() { - return $this->mRowCount; - } - - public function seek( $iRow ) { - $this->mCursor = min( $iRow, $this->mRowCount ); - } - - public function numfields() { - return $this->mNumFields; - } - - public function fieldname( $nr ) { - $arrKeys = array_keys( $this->mRows[0] ); - return $arrKeys[$nr]; - } - - public function fieldtype( $nr ) { - $i = 0; - $intType = -1; - foreach ( $this->mFieldMeta as $meta ) { - if ( $nr == $i ) { - $intType = $meta['Type']; - break; + + public function numrows() { + return $this->mRowCount; + } + + public function seek( $iRow ) { + $this->mCursor = min( $iRow, $this->mRowCount ); + } + + public function numfields() { + return $this->mNumFields; + } + + public function fieldname( $nr ) { + $arrKeys = array_keys( $this->mRows[0] ); + return $arrKeys[$nr]; + } + + public function fieldtype( $nr ) { + $i = 0; + $intType = -1; + foreach ( $this->mFieldMeta as $meta ) { + if ( $nr == $i ) { + $intType = $meta['Type']; + break; + } + $i++; } - $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; - } - return $strType; - } - - public function free() { - unset( $this->mRows ); - return; - } + // 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; + } + return $strType; + } + public function free() { + unset( $this->mRows ); + return; + } } diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php index ed276ec5..9cbd455e 100644 --- a/includes/db/DatabaseMysql.php +++ b/includes/db/DatabaseMysql.php @@ -18,7 +18,7 @@ class DatabaseMysql extends DatabaseBase { return 'mysql'; } - /*private*/ function doQuery( $sql ) { + protected function doQuery( $sql ) { if( $this->bufferResults() ) { $ret = mysql_query( $sql, $this->mConn ); } else { @@ -95,7 +95,9 @@ class DatabaseMysql extends DatabaseBase { wfProfileOut("dbconnect-$server"); if ( $dbName != '' && $this->mConn !== false ) { - $success = @/**/mysql_select_db( $dbName, $this->mConn ); + wfSuppressWarnings(); + $success = mysql_select_db( $dbName, $this->mConn ); + wfRestoreWarnings(); if ( !$success ) { $error = "Error selecting database $dbName on server {$this->mServer} " . "from client host " . wfHostname() . "\n"; @@ -152,7 +154,10 @@ class DatabaseMysql extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - if ( !@/**/mysql_free_result( $res ) ) { + wfSuppressWarnings(); + $ok = mysql_free_result( $res ); + wfRestoreWarnings(); + if ( !$ok ) { throw new DBUnexpectedError( $this, "Unable to free MySQL result" ); } } @@ -161,7 +166,9 @@ class DatabaseMysql extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @/**/$row = mysql_fetch_object( $res ); + wfSuppressWarnings(); + $row = mysql_fetch_object( $res ); + wfRestoreWarnings(); if( $this->lastErrno() ) { throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); } @@ -172,7 +179,9 @@ class DatabaseMysql extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @/**/$row = mysql_fetch_array( $res ); + wfSuppressWarnings(); + $row = mysql_fetch_array( $res ); + wfRestoreWarnings(); if ( $this->lastErrno() ) { throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); } @@ -183,7 +192,9 @@ class DatabaseMysql extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @/**/$n = mysql_num_rows( $res ); + wfSuppressWarnings(); + $n = mysql_num_rows( $res ); + wfRestoreWarnings(); if( $this->lastErrno() ) { throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) ); } @@ -241,6 +252,10 @@ class DatabaseMysql extends DatabaseBase { function affectedRows() { return mysql_affected_rows( $this->mConn ); } + function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMysql::replace' ) { + return $this->nativeReplace( $table, $rows, $fname ); + } + /** * Estimate rows in dataset * Returns estimated count, based on EXPLAIN output @@ -329,6 +344,10 @@ class DatabaseMysql extends DatabaseBase { return "`" . $this->strencode( $s ) . "`"; } + public function isQuotedIdentifier( $name ) { + return strlen($name) && $name[0] == '`' && substr( $name, -1, 1 ) == '`'; + } + function ping() { $ping = mysql_ping( $this->mConn ); if ( $ping ) { @@ -344,7 +363,11 @@ class DatabaseMysql extends DatabaseBase { /** * Returns slave lag. - * At the moment, this will only work if the DB user has the PROCESS privilege + * + * On MySQL 4.1.9 and later, this will do a SHOW SLAVE STATUS. On earlier + * versions of MySQL, it uses SHOW PROCESSLIST, which requires the PROCESS + * privilege. + * * @result int */ function getLag() { @@ -352,6 +375,31 @@ class DatabaseMysql extends DatabaseBase { wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" ); return $this->mFakeSlaveLag; } + + if ( version_compare( $this->getServerVersion(), '4.1.9', '>=' ) ) { + return $this->getLagFromSlaveStatus(); + } else { + return $this->getLagFromProcesslist(); + } + } + + function getLagFromSlaveStatus() { + $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ ); + if ( !$res ) { + return false; + } + $row = $res->fetchObject(); + if ( !$row ) { + return false; + } + if ( strval( $row->Seconds_Behind_Master ) === '' ) { + return false; + } else { + return intval( $row->Seconds_Behind_Master ); + } + } + + function getLagFromProcesslist() { $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ ); if( !$res ) { return false; @@ -384,6 +432,83 @@ class DatabaseMysql extends DatabaseBase { } return false; } + + /** + * Wait for the slave to catch up to a given master position. + * + * @param $pos DBMasterPos object + * @param $timeout Integer: the maximum number of seconds to wait for synchronisation + */ + function masterPosWait( DBMasterPos $pos, $timeout ) { + $fname = 'DatabaseBase::masterPosWait'; + wfProfileIn( $fname ); + + # Commit any open transactions + if ( $this->mTrxLevel ) { + $this->commit(); + } + + if ( !is_null( $this->mFakeSlaveLag ) ) { + $status = parent::masterPosWait( $pos, $timeout ); + wfProfileOut( $fname ); + return $status; + } + + # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set + $encFile = $this->addQuotes( $pos->file ); + $encPos = intval( $pos->pos ); + $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)"; + $res = $this->doQuery( $sql ); + + if ( $res && $row = $this->fetchRow( $res ) ) { + wfProfileOut( $fname ); + return $row[0]; + } else { + wfProfileOut( $fname ); + return false; + } + } + + /** + * Get the position of the master from SHOW SLAVE STATUS + * + * @return MySQLMasterPos|false + */ + function getSlavePos() { + if ( !is_null( $this->mFakeSlaveLag ) ) { + return parent::getSlavePos(); + } + + $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' ); + $row = $this->fetchObject( $res ); + + if ( $row ) { + $pos = isset( $row->Exec_master_log_pos ) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos; + return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos ); + } else { + return false; + } + } + + /** + * Get the position of the master from SHOW MASTER STATUS + * + * @return MySQLMasterPos|false + */ + function getMasterPos() { + if ( $this->mFakeMaster ) { + return parent::getMasterPos(); + } + + $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' ); + $row = $this->fetchObject( $res ); + + if ( $row ) { + return new MySQLMasterPos( $row->File, $row->Position ); + } else { + return false; + } + } function getServerVersion() { return mysql_get_server_info( $this->mConn ); @@ -478,8 +603,23 @@ class DatabaseMysql extends DatabaseBase { $this->query( "SET sql_big_selects=$encValue", __METHOD__ ); } - public function unixTimestamp( $field ) { - return "UNIX_TIMESTAMP($field)"; + /** + * DELETE where the condition is a join. MySql uses multi-table deletes. + */ + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) { + if ( !$conds ) { + throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' ); + } + + $delTable = $this->tableName( $delTable ); + $joinTable = $this->tableName( $joinTable ); + $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar "; + + if ( $conds != '*' ) { + $sql .= ' AND ' . $this->makeList( $conds, LIST_AND ); + } + + return $this->query( $sql, $fname ); } /** @@ -516,26 +656,79 @@ class DatabaseMysql extends DatabaseBase { # Note that we don't bother changing around the prefixes here be- # cause we know we're using MySQL anyway. - $res = $this->query( "SHOW CREATE TABLE $oldName" ); + $res = $this->query( 'SHOW CREATE TABLE ' . $this->addIdentifierQuotes( $oldName ) ); $row = $this->fetchRow( $res ); $oldQuery = $row[1]; $query = preg_replace( '/CREATE TABLE `(.*?)`/', - "CREATE $tmp TABLE `$newName`", $oldQuery ); + "CREATE $tmp TABLE " . $this->addIdentifierQuotes( $newName ), $oldQuery ); if ($oldQuery === $query) { # Couldn't do replacement throw new MWException( "could not create temporary table $newName" ); } } else { + $newName = $this->addIdentifierQuotes( $newName ); + $oldName = $this->addIdentifierQuotes( $oldName ); $query = "CREATE $tmp TABLE $newName (LIKE $oldName)"; } $this->query( $query, $fname ); } + + /** + * List all tables on the database + * + * @param $prefix Only show tables with this prefix, e.g. mw_ + * @param $fname String: calling function name + */ + function listTables( $prefix = null, $fname = 'DatabaseMysql::listTables' ) { + $result = $this->query( "SHOW TABLES", $fname); + + $endArray = array(); + + foreach( $result as $table ) { + $vars = get_object_vars($table); + $table = array_pop( $vars ); + + if( !$prefix || strpos( $table, $prefix ) === 0 ) { + $endArray[] = $table; + } + } + + return $endArray; + } + + public function dropTable( $tableName, $fName = 'DatabaseMysql::dropTable' ) { + if( !$this->tableExists( $tableName ) ) { + return false; + } + return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName ); + } + /** + * @return array + */ protected function getDefaultSchemaVars() { $vars = parent::getDefaultSchemaVars(); - $vars['wgDBTableOptions'] = $GLOBALS['wgDBTableOptions']; + $vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] ); + $vars['wgDBTableOptions'] = str_replace( 'CHARSET=mysql4', 'CHARSET=binary', $GLOBALS['wgDBTableOptions'] ); return $vars; } + + /** + * Get status information from SHOW STATUS in an associative array + * + * @return array + */ + function getMysqlStatus( $which = "%" ) { + $res = $this->query( "SHOW STATUS LIKE '{$which}'" ); + $status = array(); + + foreach ( $res as $row ) { + $status[$row->Variable_name] = $row->Value; + } + + return $status; + } + } /** @@ -593,7 +786,7 @@ class MySQLField implements Field { } } -class MySQLMasterPos { +class MySQLMasterPos implements DBMasterPos { var $file, $pos; function __construct( $file, $pos ) { diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 3c4d00ac..b64e66c2 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -16,7 +16,7 @@ class ORAResult { private $rows; private $cursor; private $nrows; - + private $columns = array(); private function array_unique_md( $array_in ) { @@ -34,6 +34,11 @@ class ORAResult { return $array_out; } + /** + * @param $db DatabaseBase + * @param $stmt + * @param bool $unique + */ function __construct( &$db, $stmt, $unique = false ) { $this->db =& $db; @@ -165,7 +170,6 @@ class ORAField implements Field { class DatabaseOracle extends DatabaseBase { var $mInsertId = null; var $mLastResult = null; - var $numeric_version = null; var $lastResult = null; var $cursor = 0; var $mAffectedRows; @@ -180,7 +184,8 @@ class DatabaseOracle extends DatabaseBase { function __construct( $server = false, $user = false, $password = false, $dbName = false, $flags = 0, $tablePrefix = 'get from global' ) { - $tablePrefix = $tablePrefix == 'get from global' ? $tablePrefix : strtoupper( $tablePrefix ); + global $wgDBprefix; + $tablePrefix = $tablePrefix == 'get from global' ? strtoupper( $wgDBprefix ) : strtoupper( $tablePrefix ); parent::__construct( $server, $user, $password, $dbName, $flags, $tablePrefix ); wfRunHooks( 'DatabaseOraclePostInit', array( $this ) ); } @@ -219,11 +224,6 @@ class DatabaseOracle extends DatabaseBase { return true; } - static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) - { - return new DatabaseOracle( $server, $user, $password, $dbName, $flags ); - } - /** * Usually aborts on failure */ @@ -232,6 +232,7 @@ class DatabaseOracle extends DatabaseBase { 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" ); } + $this->close(); $this->mUser = $user; $this->mPassword = $password; // changed internal variables functions @@ -245,7 +246,7 @@ class DatabaseOracle extends DatabaseBase { $this->mServer = $server; if ( !$dbName ) { $this->mDBname = $user; - } else { + } else { $this->mDBname = $dbName; } } @@ -255,11 +256,13 @@ class DatabaseOracle extends DatabaseBase { } $session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT; + wfSuppressWarnings(); if ( $this->mFlags & DBO_DEFAULT ) { $this->mConn = oci_new_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode ); } else { $this->mConn = oci_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode ); } + wfRestoreWarnings(); if ( $this->mUser != $this->mDBname ) { //change current schema in session @@ -276,7 +279,6 @@ 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; } @@ -297,10 +299,10 @@ class DatabaseOracle extends DatabaseBase { } function execFlags() { - return $this->mTrxLevel ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS; + return $this->mTrxLevel ? OCI_NO_AUTO_COMMIT : OCI_COMMIT_ON_SUCCESS; } - function doQuery( $sql ) { + protected function doQuery( $sql ) { wfDebug( "SQL: [$sql]\n" ); if ( !mb_check_encoding( $sql ) ) { throw new MWException( "SQL encoding is invalid\n$sql" ); @@ -357,7 +359,7 @@ class DatabaseOracle extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - + $res->free(); } @@ -365,7 +367,7 @@ class DatabaseOracle extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - + return $res->fetchObject(); } @@ -478,16 +480,16 @@ class DatabaseOracle extends DatabaseBase { private function fieldBindStatement ( $table, $col, &$val, $includeCol = false ) { $col_info = $this->fieldInfoMulti( $table, $col ); $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; - + $bind = ''; if ( is_numeric( $col ) ) { $bind = $val; $val = null; - return $bind; - } else if ( $includeCol ) { + return $bind; + } elseif ( $includeCol ) { $bind = "$col = "; } - + if ( $val == '' && $val !== 0 && $col_type != 'BLOB' && $col_type != 'CLOB' ) { $val = null; } @@ -505,7 +507,7 @@ class DatabaseOracle extends DatabaseBase { } else { $bind .= ':' . $col; } - + return $bind; } @@ -525,7 +527,7 @@ class DatabaseOracle extends DatabaseBase { } else { $first = false; } - + $sql .= $this->fieldBindStatement( $table, $col, $val ); } $sql .= ')'; @@ -551,7 +553,7 @@ class DatabaseOracle extends DatabaseBase { } $val = ( $wgContLang != null ) ? $wgContLang->checkTitleEncoding( $val ) : $val; - if ( oci_bind_by_name( $stmt, ":$col", $val ) === false ) { + 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; @@ -652,8 +654,7 @@ class DatabaseOracle extends DatabaseBase { return $retval; } - function tableName( $name ) { - global $wgSharedDB, $wgSharedPrefix, $wgSharedTables; + function tableName( $name, $quoted = true ) { /* Replace reserved words with better ones Using uppercase because that's the only way Oracle can handle @@ -668,53 +669,13 @@ class DatabaseOracle extends DatabaseBase { break; } - /* - The rest of procedure is equal to generic Databse class - except for the quoting style - */ - if ( $name[0] == '"' && substr( $name, - 1, 1 ) == '"' ) { - return $name; - } - if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) { - return $name; - } - $dbDetails = array_reverse( explode( '.', $name, 2 ) ); - if ( isset( $dbDetails[1] ) ) { - @list( $table, $database ) = $dbDetails; - } else { - @list( $table ) = $dbDetails; - } - - $prefix = $this->mTablePrefix; - - if ( isset( $database ) ) { - $table = ( $table[0] == '`' ? $table : "`{$table}`" ); - } - - if ( !isset( $database ) && isset( $wgSharedDB ) && $table[0] != '"' - && isset( $wgSharedTables ) - && is_array( $wgSharedTables ) - && in_array( $table, $wgSharedTables ) - ) { - $database = $wgSharedDB; - $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix; - } - - if ( isset( $database ) ) { - $database = ( $database[0] == '"' ? $database : "\"{$database}\"" ); - } - $table = ( $table[0] == '"') ? $table : "\"{$prefix}{$table}\"" ; - - $tableName = ( isset( $database ) ? "{$database}.{$table}" : "{$table}" ); - - return strtoupper( $tableName ); + return parent::tableName( strtoupper( $name ), $quoted ); } function tableNameInternal( $name ) { $name = $this->tableName( $name ); - return preg_replace( '/.*\."(.*)"/', '$1', $name); + return preg_replace( '/.*\.(.*)/', '$1', $name); } - /** * Return the next in a sequence, save the value for retrieval via insertId() */ @@ -730,96 +691,26 @@ class DatabaseOracle extends DatabaseBase { */ private function getSequenceData( $table ) { if ( $this->sequenceData == null ) { - $result = $this->doQuery( 'SELECT lower(us.sequence_name), lower(utc.table_name), lower(utc.column_name) from user_sequences us, user_tab_columns utc where us.sequence_name = utc.table_name||\'_\'||utc.column_name||\'_SEQ\'' ); + $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}')" ); while ( ( $row = $result->fetchRow() ) !== false ) { - $this->sequenceData[$this->tableName( $row[1] )] = array( + $this->sequenceData[$row[1]] = array( 'sequence' => $row[0], 'column' => $row[2] ); } } - + $table = strtolower( $this->removeIdentifierQuotes( $this->tableName( $table ) ) ); return ( isset( $this->sequenceData[$table] ) ) ? $this->sequenceData[$table] : false; } - /** - * REPLACE query wrapper - * Oracle simulates this with a DELETE followed by INSERT - * $row is the row to insert, an associative array - * $uniqueIndexes is an array of indexes. Each element may be either a - * field name or an array of field names - * - * It may be more efficient to leave off unique indexes which are unlikely to collide. - * However if you do this, you run the risk of encountering errors which wouldn't have - * occurred in MySQL. - * - * @param $table String: table name - * @param $uniqueIndexes Array: array of indexes. Each element may be - * either a field name or an array of field names - * @param $rows Array: rows to insert to $table - * @param $fname String: function name, you can use __METHOD__ here - */ - function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseOracle::replace' ) { - $table = $this->tableName( $table ); - - if ( count( $rows ) == 0 ) { - return; - } - - # Single row case - if ( !is_array( reset( $rows ) ) ) { - $rows = array( $rows ); - } - - $sequenceData = $this->getSequenceData( $table ); - - foreach ( $rows as $row ) { - # Delete rows which collide - if ( $uniqueIndexes ) { - $deleteConds = array(); - foreach ( $uniqueIndexes as $key=>$index ) { - if ( is_array( $index ) ) { - $deleteConds2 = array(); - foreach ( $index as $col ) { - $deleteConds2[$col] = $row[$col]; - } - $deleteConds[$key] = $this->makeList( $deleteConds2, LIST_AND ); - } else { - $deleteConds[$index] = $row[$index]; - } - } - $deleteConds = array( $this->makeList( $deleteConds, LIST_OR ) ); - $this->delete( $table, $deleteConds, $fname ); - } - - - if ( $sequenceData !== false && !isset( $row[$sequenceData['column']] ) ) { - $row[$sequenceData['column']] = $this->nextSequenceValue( $sequenceData['sequence'] ); - } - - # Now insert the row - $this->insert( $table, $row, $fname ); - } - } - - # DELETE where the condition is a join - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseOracle::deleteJoin' ) { - if ( !$conds ) { - throw new DBUnexpectedError( $this, 'DatabaseOracle::deleteJoin() called with empty $conds' ); - } - - $delTable = $this->tableName( $delTable ); - $joinTable = $this->tableName( $joinTable ); - $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; - if ( $conds != '*' ) { - $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); - } - $sql .= ')'; - - $this->query( $sql, $fname ); - } - # Returns the size of a text field, or -1 for "unlimited" function textFieldSize( $table, $field ) { $fieldInfoData = $this->fieldInfo( $table, $field ); @@ -849,26 +740,21 @@ class DatabaseOracle extends DatabaseBase { return 'SELECT * ' . ( $all ? '':'/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')' ; } - public function unixTimestamp( $field ) { - return "((trunc($field) - to_date('19700101','YYYYMMDD')) * 86400)"; - } - function wasDeadlock() { return $this->lastErrno() == 'OCI-00060'; } function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseOracle::duplicateTableStructure' ) { - global $wgDBprefix; - $temporary = $temporary ? 'TRUE' : 'FALSE'; - $newName = trim( strtoupper( $newName ), '"'); - $oldName = trim( strtoupper( $oldName ), '"'); + $newName = strtoupper( $newName ); + $oldName = strtoupper( $oldName ); - $tabName = substr( $newName, strlen( $wgDBprefix ) ); + $tabName = substr( $newName, strlen( $this->mTablePrefix ) ); $oldPrefix = substr( $oldName, 0, strlen( $oldName ) - strlen( $tabName ) ); + $newPrefix = strtoupper( $this->mTablePrefix ); - return $this->doQuery( 'BEGIN DUPLICATE_TABLE(\'' . $tabName . '\', \'' . $oldPrefix . '\', \'' . strtoupper( $wgDBprefix ) . '\', ' . $temporary . '); END;' ); + return $this->doQuery( "BEGIN DUPLICATE_TABLE( '$tabName', '$oldPrefix', '$newPrefix', $temporary ); END;" ); } function listTables( $prefix = null, $fname = 'DatabaseOracle::listTables' ) { @@ -876,8 +762,9 @@ class DatabaseOracle extends DatabaseBase { if (!empty($prefix)) { $listWhere = ' AND table_name LIKE \''.strtoupper($prefix).'%\''; } - - $result = $this->doQuery( "SELECT table_name FROM user_tables WHERE table_name NOT LIKE '%!_IDX$_' ESCAPE '!' $listWhere" ); + + $owner = strtoupper( $this->mDBname ); + $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(); @@ -898,7 +785,7 @@ class DatabaseOracle extends DatabaseBase { if( !$this->tableExists( $tableName ) ) { return false; } - + return $this->doQuery( "DROP TABLE $tableName CASCADE CONSTRAINTS PURGE" ); } @@ -942,15 +829,35 @@ class DatabaseOracle extends DatabaseBase { $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 + */ + function indexExists( $table, $index, $fname = 'DatabaseOracle::indexExists' ) { + $table = $this->tableName( $table ); + $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 ); + 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) */ function tableExists( $table ) { - $table = $this->addQuotes( trim( $this->tableName($table), '"' ) ); + $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 ); @@ -960,7 +867,7 @@ class DatabaseOracle extends DatabaseBase { } else { $count = 0; } - return $count!=0; + return $count; } /** @@ -971,6 +878,7 @@ class DatabaseOracle extends DatabaseBase { * * @param $table Array * @param $field String + * @return ORAField|ORAResult */ private function fieldInfoMulti( $table, $field ) { $field = strtoupper( $field ); @@ -978,7 +886,7 @@ class DatabaseOracle extends DatabaseBase { $table = array_map( array( &$this, 'tableNameInternal' ), $table ); $tableWhere = 'IN ('; foreach( $table as &$singleTable ) { - $singleTable = strtoupper( trim( $singleTable, '"' ) ); + $singleTable = $this->removeIdentifierQuotes($singleTable); if ( isset( $this->mFieldInfoCache["$singleTable.$field"] ) ) { return $this->mFieldInfoCache["$singleTable.$field"]; } @@ -986,7 +894,7 @@ class DatabaseOracle extends DatabaseBase { } $tableWhere = rtrim( $tableWhere, ',' ) . ')'; } else { - $table = strtoupper( trim( $this->tableNameInternal( $table ), '"' ) ); + $table = $this->removeIdentifierQuotes( $this->tableNameInternal( $table ) ); if ( isset( $this->mFieldInfoCache["$table.$field"] ) ) { return $this->mFieldInfoCache["$table.$field"]; } @@ -1018,6 +926,12 @@ class DatabaseOracle extends DatabaseBase { return $fieldInfoTemp; } + /** + * @throws DBUnexpectedError + * @param $table + * @param $field + * @return ORAField + */ function fieldInfo( $table, $field ) { if ( is_array( $table ) ) { throw new DBUnexpectedError( $this, 'DatabaseOracle::fieldInfo called with table array!' ); @@ -1027,12 +941,17 @@ class DatabaseOracle extends DatabaseBase { function begin( $fname = 'DatabaseOracle::begin' ) { $this->mTrxLevel = 1; + $this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' ); } function commit( $fname = 'DatabaseOracle::commit' ) { if ( $this->mTrxLevel ) { - oci_commit( $this->mConn ); + $ret = oci_commit( $this->mConn ); + if ( !$ret ) { + throw new DBUnexpectedError( $this, $this->lastError() ); + } $this->mTrxLevel = 0; + $this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' ); } } @@ -1040,6 +959,7 @@ class DatabaseOracle extends DatabaseBase { if ( $this->mTrxLevel ) { oci_rollback( $this->mConn ); $this->mTrxLevel = 0; + $this->doQuery( 'SET CONSTRAINTS ALL IMMEDIATE' ); } } @@ -1154,15 +1074,23 @@ class DatabaseOracle extends DatabaseBase { } public function addIdentifierQuotes( $s ) { - if ( !$this->mFlags & DBO_DDLMODE ) { - $s = '"' . str_replace( '"', '""', $s ) . '"'; + if ( !$this->getFlag( DBO_DDLMODE ) ) { + $s = '/*Q*/' . $s; } return $s; } + public function removeIdentifierQuotes( $s ) { + return strpos($s, '/*Q*/') === FALSE ? $s : substr($s, 5); + } + + public function isQuotedIdentifier( $s ) { + return strpos($s, '/*Q*/') !== FALSE; + } + private function wrapFieldForWhere( $table, &$col, &$val ) { global $wgContLang; - + $col_info = $this->fieldInfoMulti( $table, $col ); $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; if ( $col_type == 'CLOB' ) { @@ -1244,20 +1172,38 @@ class DatabaseOracle extends DatabaseBase { if ( is_array($conds) ) { $conds = $this->wrapConditionsForWhere( $table, $conds ); } + // a hack for deleting pages, users and images (which have non-nullable FKs) + // all deletions on these tables have transactions so final failure rollbacks these updates + $table = $this->tableName( $table ); + if ( $table == $this->tableName( 'page' ) ) { + $this->update( 'recentchanges', array( 'rc_cur_id' => 0 ), array( 'rc_cur_id' => $conds['page_id'] ), $fname ); + } elseif ( $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 ); + } elseif ( $table == $this->tableName( 'image' ) ) { + $this->update( 'oldimage', array( 'oi_name' => 0 ), array( 'oi_name' => $conds['img_name'] ), $fname ); + } return parent::delete( $table, $conds, $fname ); } function update( $table, $values, $conds, $fname = 'DatabaseOracle::update', $options = array() ) { global $wgContLang; - + $table = $this->tableName( $table ); $opts = $this->makeUpdateOptions( $options ); $sql = "UPDATE $opts $table SET "; - + $first = true; foreach ( $values as $col => &$val ) { $sqlSet = $this->fieldBindStatement( $table, $col, $val, true ); - + if ( !$first ) { $sqlSet = ', ' . $sqlSet; } else { @@ -1303,8 +1249,8 @@ class DatabaseOracle extends DatabaseBase { throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] ); } - if ( $col_type == 'BLOB' ) { - $lob[$col]->writeTemporary( $val ); + if ( $col_type == 'BLOB' ) { + $lob[$col]->writeTemporary( $val ); oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, SQLT_BLOB ); } else { $lob[$col]->writeTemporary( $val ); diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index bc71a9a5..742a8b51 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -9,8 +9,14 @@ class PostgresField implements Field { private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname; - static function fromText($db, $table, $field) { - global $wgDBmwschema; + /** + * @param $db DatabaseBase + * @param $table + * @param $field + * @return null|PostgresField + */ + static function fromText( $db, $table, $field ) { + global $wgDBmwschema; $q = <<<SQL SELECT @@ -33,7 +39,7 @@ AND relname=%s AND attname=%s; SQL; - $table = $db->tableName( $table ); + $table = $db->tableName( $table, false ); $res = $db->query( sprintf( $q, $db->addQuotes( $wgDBmwschema ), @@ -137,10 +143,6 @@ class DatabasePostgres extends DatabaseBase { return $this->numRows( $res ); } - static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { - return new DatabasePostgres( $server, $user, $password, $dbName, $flags ); - } - /** * Usually aborts on failure */ @@ -155,9 +157,10 @@ class DatabasePostgres extends DatabaseBase { if ( !strlen( $user ) ) { # e.g. the class is being loaded return; } + $this->close(); $this->mServer = $server; - $this->mPort = $port = $wgDBport; + $port = $wgDBport; $this->mUser = $user; $this->mPassword = $password; $this->mDBname = $dbName; @@ -194,21 +197,34 @@ class DatabasePostgres extends DatabaseBase { $this->doQuery( "SET client_min_messages = 'ERROR'" ); } - $this->doQuery( "SET client_encoding='UTF8'" ); + $this->query( "SET client_encoding='UTF8'", __METHOD__ ); + $this->query( "SET datestyle = 'ISO, YMD'", __METHOD__ ); + $this->query( "SET timezone = 'GMT'", __METHOD__ ); - global $wgDBmwschema, $wgDBts2schema; - if ( isset( $wgDBmwschema ) && isset( $wgDBts2schema ) - && $wgDBmwschema !== 'mediawiki' - && preg_match( '/^\w+$/', $wgDBmwschema ) - && preg_match( '/^\w+$/', $wgDBts2schema ) - ) { + global $wgDBmwschema; + if ( $this->schemaExists( $wgDBmwschema ) ) { $safeschema = $this->addIdentifierQuotes( $wgDBmwschema ); - $this->doQuery( "SET search_path = $safeschema, $wgDBts2schema, public" ); + $this->doQuery( "SET search_path = $safeschema" ); + } else { + $this->doQuery( "SET search_path = public" ); } return $this->mConn; } + /** + * 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 + */ + function selectDB( $db ) { + if ( $this->mDBname !== $db ) { + return (bool)$this->open( $this->mServer, $this->mUser, $this->mPassword, $db ); + } else { + return true; + } + } + function makeConnectionString( $vars ) { $s = ''; foreach ( $vars as $name => $value ) { @@ -230,7 +246,7 @@ class DatabasePostgres extends DatabaseBase { } } - function doQuery( $sql ) { + protected function doQuery( $sql ) { if ( function_exists( 'mb_convert_encoding' ) ) { $sql = mb_convert_encoding( $sql, 'UTF-8' ); } @@ -247,7 +263,10 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - if ( !@pg_free_result( $res ) ) { + wfSuppressWarnings(); + $ok = pg_free_result( $res ); + wfRestoreWarnings(); + if ( !$ok ) { throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" ); } } @@ -256,11 +275,12 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @$row = pg_fetch_object( $res ); - # FIXME: HACK HACK HACK HACK debug + wfSuppressWarnings(); + $row = pg_fetch_object( $res ); + wfRestoreWarnings(); + # @todo FIXME: HACK HACK HACK HACK debug - # TODO: - # hashar : not sure if the following test really trigger if the object + # @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 ) ) ); @@ -272,7 +292,9 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @$row = pg_fetch_array( $res ); + wfSuppressWarnings(); + $row = pg_fetch_array( $res ); + wfRestoreWarnings(); if( pg_last_error( $this->mConn ) ) { throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); } @@ -283,7 +305,9 @@ class DatabasePostgres extends DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @$n = pg_num_rows( $res ); + wfSuppressWarnings(); + $n = pg_num_rows( $res ); + wfRestoreWarnings(); if( pg_last_error( $this->mConn ) ) { throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); } @@ -529,7 +553,7 @@ class DatabasePostgres extends DatabaseBase { * 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)? + * @todo FIXME: Implement this a little better (seperate select/insert)? */ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect', $insertOptions = array(), $selectOptions = array() ) @@ -598,7 +622,7 @@ class DatabasePostgres extends DatabaseBase { return $res; } - function tableName( $name ) { + function tableName( $name, $quoted = true ) { # Replace reserved words with better ones switch( $name ) { case 'user': @@ -606,7 +630,7 @@ class DatabasePostgres extends DatabaseBase { case 'text': return 'pagecontent'; default: - return $name; + return parent::tableName( $name, $quoted ); } } @@ -632,83 +656,6 @@ class DatabasePostgres extends DatabaseBase { return $currval; } - /** - * REPLACE query wrapper - * Postgres simulates this with a DELETE followed by INSERT - * $row is the row to insert, an associative array - * $uniqueIndexes is an array of indexes. Each element may be either a - * field name or an array of field names - * - * It may be more efficient to leave off unique indexes which are unlikely to collide. - * However if you do this, you run the risk of encountering errors which wouldn't have - * occurred in MySQL - */ - function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabasePostgres::replace' ) { - $table = $this->tableName( $table ); - - if ( count( $rows ) == 0 ) { - return; - } - - # Single row case - if ( !is_array( reset( $rows ) ) ) { - $rows = array( $rows ); - } - - foreach( $rows as $row ) { - # Delete rows which collide - if ( $uniqueIndexes ) { - $sql = "DELETE FROM $table WHERE "; - $first = true; - foreach ( $uniqueIndexes as $index ) { - if ( $first ) { - $first = false; - $sql .= '('; - } else { - $sql .= ') OR ('; - } - if ( is_array( $index ) ) { - $first2 = true; - foreach ( $index as $col ) { - if ( $first2 ) { - $first2 = false; - } else { - $sql .= ' AND '; - } - $sql .= $col.'=' . $this->addQuotes( $row[$col] ); - } - } else { - $sql .= $index.'=' . $this->addQuotes( $row[$index] ); - } - } - $sql .= ')'; - $this->query( $sql, $fname ); - } - - # Now insert the row - $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' . - $this->makeList( $row, LIST_COMMA ) . ')'; - $this->query( $sql, $fname ); - } - } - - # DELETE where the condition is a join - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabasePostgres::deleteJoin' ) { - if ( !$conds ) { - throw new DBUnexpectedError( $this, 'DatabasePostgres::deleteJoin() called with empty $conds' ); - } - - $delTable = $this->tableName( $delTable ); - $joinTable = $this->tableName( $joinTable ); - $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; - if ( $conds != '*' ) { - $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); - } - $sql .= ')'; - - $this->query( $sql, $fname ); - } - # Returns the size of a text field, or -1 for "unlimited" function textFieldSize( $table, $field ) { $table = $this->tableName( $table ); @@ -735,6 +682,8 @@ class DatabasePostgres extends DatabaseBase { } function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabasePostgres::duplicateTableStructure' ) { + $newName = $this->addIdentifierQuotes( $newName ); + $oldName = $this->addIdentifierQuotes( $oldName ); return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname ); } @@ -788,6 +737,7 @@ class DatabasePostgres extends DatabaseBase { if ( !$schema ) { $schema = $wgDBmwschema; } + $table = $this->tableName( $table, false ); $etable = $this->addQuotes( $table ); $eschema = $this->addQuotes( $schema ); $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " @@ -899,6 +849,10 @@ SQL; return $sql; } + /** + * @param $b + * @return Blob + */ function encodeBlob( $b ) { return new Blob( pg_escape_bytea( $this->mConn, $b ) ); } @@ -914,6 +868,10 @@ SQL; return pg_escape_string( $this->mConn, $s ); } + /** + * @param $s null|bool|Blob + * @return int|string + */ function addQuotes( $s ) { if ( is_null( $s ) ) { return 'NULL'; @@ -971,13 +929,21 @@ SQL; } if ( isset( $options['GROUP BY'] ) ) { - $preLimitTail .= ' GROUP BY ' . $options['GROUP BY']; + $gb = is_array( $options['GROUP BY'] ) + ? implode( ',', $options['GROUP BY'] ) + : $options['GROUP BY']; + $preLimitTail .= " GROUP BY {$gb}"; } + if ( isset( $options['HAVING'] ) ) { $preLimitTail .= " HAVING {$options['HAVING']}"; } + if ( isset( $options['ORDER BY'] ) ) { - $preLimitTail .= ' ORDER BY ' . $options['ORDER BY']; + $ob = is_array( $options['ORDER BY'] ) + ? implode( ',', $options['ORDER BY'] ) + : $options['ORDER BY']; + $preLimitTail .= " ORDER BY {$ob}"; } //if ( isset( $options['LIMIT'] ) ) { diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index 503ebdf6..e298175d 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -20,8 +20,18 @@ class DatabaseSqlite extends DatabaseBase { var $mName; /** + * @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; @@ -35,21 +45,31 @@ class DatabaseSqlite extends DatabaseBase { } } + /** + * @return string + */ function getType() { return 'sqlite'; } /** * @todo: check if it should be true like parent class + * + * @return bool */ - function implicitGroupby() { return false; } - - static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { - return new DatabaseSqlite( $server, $user, $password, $dbName, $flags ); + function implicitGroupby() { + return false; } /** Open an SQLite database and return a resource handle to it * NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases + * + * @param $server + * @param $user + * @param $pass + * @param $dbName + * + * @return PDO */ function open( $server, $user, $pass, $dbName ) { global $wgSQLiteDataDir; @@ -65,7 +85,10 @@ class DatabaseSqlite extends DatabaseBase { /** * Opens a database file - * @return SQL connection or false if failed + * + * @param $fileName string + * + * @return PDO|false SQL connection or false if failed */ function openFile( $fileName ) { $this->mDatabaseFile = $fileName; @@ -93,6 +116,8 @@ class DatabaseSqlite extends DatabaseBase { /** * Close an SQLite database + * + * @return bool */ function close() { $this->mOpened = false; @@ -115,7 +140,7 @@ class DatabaseSqlite extends DatabaseBase { /** * Check if the searchindext table is FTS enabled. - * @returns false if not enabled. + * @return false if not enabled. */ function checkForEnabledSearch() { if ( self::$fulltextEnabled === null ) { @@ -154,9 +179,12 @@ class DatabaseSqlite extends DatabaseBase { /** * Attaches external database to our connection, see http://sqlite.org/lang_attach.html * for details. + * * @param $name String: database name to be used in queries like SELECT foo FROM dbname.table * @param $file String: database file name. If omitted, will be generated using $name and $wgSQLiteDataDir * @param $fname String: calling function name + * + * @return ResultWrapper */ function attachDatabase( $name, $file = false, $fname = 'DatabaseSqlite::attachDatabase' ) { global $wgSQLiteDataDir; @@ -169,6 +197,10 @@ class DatabaseSqlite extends DatabaseBase { /** * @see DatabaseBase::isWriteQuery() + * + * @param $sql string + * + * @return bool */ function isWriteQuery( $sql ) { return parent::isWriteQuery( $sql ) && !preg_match( '/^ATTACH\b/i', $sql ); @@ -176,8 +208,12 @@ 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 */ - function doQuery( $sql ) { + protected function doQuery( $sql ) { $res = $this->mConn->query( $sql ); if ( $res === false ) { return false; @@ -189,6 +225,9 @@ class DatabaseSqlite extends DatabaseBase { return $res; } + /** + * @param $res ResultWrapper + */ function freeResult( $res ) { if ( $res instanceof ResultWrapper ) { $res->result = null; @@ -197,6 +236,10 @@ class DatabaseSqlite extends DatabaseBase { } } + /** + * @param $res ResultWrapper + * @return + */ function fetchObject( $res ) { if ( $res instanceof ResultWrapper ) { $r =& $res->result; @@ -219,6 +262,10 @@ class DatabaseSqlite extends DatabaseBase { return false; } + /** + * @param $res ResultWrapper + * @return bool|mixed + */ function fetchRow( $res ) { if ( $res instanceof ResultWrapper ) { $r =& $res->result; @@ -235,37 +282,60 @@ class DatabaseSqlite extends DatabaseBase { /** * The PDO::Statement class implements the array interface so count() will work + * + * @param $res ResultWrapper + * + * @return int */ function numRows( $res ) { $r = $res instanceof ResultWrapper ? $res->result : $res; return count( $r ); } + /** + * @param $res ResultWrapper + * @return int + */ function numFields( $res ) { $r = $res instanceof ResultWrapper ? $res->result : $res; return is_array( $r ) ? count( $r[0] ) : 0; } + /** + * @param $res ResultWrapper + * @param $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; + return false; } /** * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks + * + * @param $name + * @param bool $quoted + * @return string */ - function tableName( $name ) { + function tableName( $name, $quoted = true ) { // table names starting with sqlite_ are reserved - if ( strpos( $name, 'sqlite_' ) === 0 ) return $name; - return str_replace( '`', '', parent::tableName( $name ) ); + if ( strpos( $name, 'sqlite_' ) === 0 ) { + return $name; + } + return str_replace( '"', '', parent::tableName( $name, $quoted ) ); } /** * Index names have DB scope + * + * @param $index string + * + * @return string */ function indexName( $index ) { return $index; @@ -273,11 +343,17 @@ class DatabaseSqlite extends DatabaseBase { /** * This must be called after nextSequenceVal + * + * @return int */ function insertId() { return $this->mConn->lastInsertId(); } + /** + * @param $res ResultWrapper + * @param $row + */ function dataSeek( $res, $row ) { if ( $res instanceof ResultWrapper ) { $r =& $res->result; @@ -292,6 +368,9 @@ class DatabaseSqlite extends DatabaseBase { } } + /** + * @return string + */ function lastError() { if ( !is_object( $this->mConn ) ) { return "Cannot return last error, no db connection"; @@ -300,6 +379,9 @@ class DatabaseSqlite extends DatabaseBase { return isset( $e[2] ) ? $e[2] : ''; } + /** + * @return string + */ function lastErrno() { if ( !is_object( $this->mConn ) ) { return "Cannot return last error, no db connection"; @@ -309,6 +391,9 @@ class DatabaseSqlite extends DatabaseBase { } } + /** + * @return int + */ function affectedRows() { return $this->mAffectedRows; } @@ -317,6 +402,8 @@ class DatabaseSqlite extends DatabaseBase { * Returns information about an index * Returns false if the index does not exist * - if errors are explicitly ignored, returns NULL on failure + * + * @return array */ function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) { $sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')'; @@ -334,6 +421,12 @@ class DatabaseSqlite extends DatabaseBase { return $info; } + /** + * @param $table + * @param $index + * @param $fname string + * @return bool|null + */ function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) { $row = $this->selectRow( 'sqlite_master', '*', array( @@ -356,6 +449,10 @@ class DatabaseSqlite extends DatabaseBase { /** * Filter the options used in SELECT statements + * + * @param $options array + * + * @return array */ function makeSelectOptions( $options ) { foreach ( $options as $k => $v ) { @@ -367,22 +464,44 @@ class DatabaseSqlite extends DatabaseBase { } /** - * Based on generic method (parent) with some prior SQLite-sepcific adjustments + * @param $options array + * @return string */ - function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) { - if ( !count( $a ) ) { - return true; - } - if ( !is_array( $options ) ) { - $options = array( $options ); - } + function makeUpdateOptions( $options ) { + $options = self::fixIgnore( $options ); + return parent::makeUpdateOptions( $options ); + } + /** + * @param $options array + * @return array + */ + static function fixIgnore( $options ) { # SQLite uses OR IGNORE not just IGNORE foreach ( $options as $k => $v ) { if ( $v == 'IGNORE' ) { $options[$k] = 'OR IGNORE'; } } + return $options; + } + + /** + * @param $options array + * @return string + */ + function makeInsertOptions( $options ) { + $options = self::fixIgnore( $options ); + return parent::makeInsertOptions( $options ); + } + + /** + * Based on generic method (parent) with some prior SQLite-sepcific adjustments + */ + function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) { + if ( !count( $a ) ) { + return true; + } # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts if ( isset( $a[0] ) && is_array( $a[0] ) ) { @@ -399,6 +518,13 @@ class DatabaseSqlite extends DatabaseBase { return $ret; } + /** + * @param $table + * @param $uniqueIndexes + * @param $rows + * @param $fname string + * @return bool|ResultWrapper + */ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) { if ( !count( $rows ) ) return true; @@ -406,12 +532,12 @@ class DatabaseSqlite extends DatabaseBase { if ( isset( $rows[0] ) && is_array( $rows[0] ) ) { $ret = true; foreach ( $rows as $v ) { - if ( !parent::replace( $table, $uniqueIndexes, $v, "$fname/multi-row" ) ) { + if ( !$this->nativeReplace( $table, $v, "$fname/multi-row" ) ) { $ret = false; } } } else { - $ret = parent::replace( $table, $uniqueIndexes, $rows, "$fname/single-row" ); + $ret = $this->nativeReplace( $table, $rows, "$fname/single-row" ); } return $ret; @@ -420,32 +546,47 @@ 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. + * + * @return int */ function textFieldSize( $table, $field ) { - return - 1; + return -1; } + /** + * @return bool + */ function unionSupportsOrderAndLimit() { return false; } + /** + * @param $sqls + * @param $all + * @return string + */ function unionQueries( $sqls, $all ) { $glue = $all ? ' UNION ALL ' : ' UNION '; return implode( $glue, $sqls ); } - public function unixTimestamp( $field ) { - return $field; - } - + /** + * @return bool + */ function wasDeadlock() { return $this->lastErrno() == 5; // SQLITE_BUSY } + /** + * @return bool + */ function wasErrorReissuable() { return $this->lastErrno() == 17; // SQLITE_SCHEMA; } + /** + * @return bool + */ function wasReadOnlyError() { return $this->lastErrno() == 8; // SQLITE_READONLY; } @@ -475,6 +616,8 @@ class DatabaseSqlite extends DatabaseBase { /** * Get information about a given field * Returns false if the field does not exist. + * + * @return SQLiteField|false */ function fieldInfo( $table, $field ) { $tableName = $this->tableName( $table ); @@ -489,35 +632,58 @@ class DatabaseSqlite extends DatabaseBase { } function begin( $fname = '' ) { - if ( $this->mTrxLevel == 1 ) $this->commit(); + if ( $this->mTrxLevel == 1 ) { + $this->commit(); + } $this->mConn->beginTransaction(); $this->mTrxLevel = 1; } function commit( $fname = '' ) { - if ( $this->mTrxLevel == 0 ) return; + if ( $this->mTrxLevel == 0 ) { + return; + } $this->mConn->commit(); $this->mTrxLevel = 0; } function rollback( $fname = '' ) { - if ( $this->mTrxLevel == 0 ) return; + if ( $this->mTrxLevel == 0 ) { + return; + } $this->mConn->rollBack(); $this->mTrxLevel = 0; } + /** + * @param $sql + * @param $num + * @return string + */ function limitResultForUpdate( $sql, $num ) { return $this->limitResult( $sql, $num ); } + /** + * @param $s string + * @return string + */ function strencode( $s ) { return substr( $this->addQuotes( $s ), 1, - 1 ); } + /** + * @param $b + * @return Blob + */ function encodeBlob( $b ) { return new Blob( $b ); } + /** + * @param $b Blob|string + * @return string + */ function decodeBlob( $b ) { if ( $b instanceof Blob ) { $b = $b->fetch(); @@ -525,6 +691,10 @@ class DatabaseSqlite extends DatabaseBase { return $b; } + /** + * @param $s Blob|string + * @return string + */ function addQuotes( $s ) { if ( $s instanceof Blob ) { return "x'" . bin2hex( $s->fetch() ) . "'"; @@ -533,6 +703,9 @@ class DatabaseSqlite extends DatabaseBase { } } + /** + * @return string + */ function buildLike() { $params = func_get_args(); if ( count( $params ) > 0 && is_array( $params[0] ) ) { @@ -541,6 +714,9 @@ class DatabaseSqlite extends DatabaseBase { return parent::buildLike( $params ) . "ESCAPE '\' "; } + /** + * @return string + */ public function getSearchEngine() { return "SearchSqlite"; } @@ -554,6 +730,10 @@ class DatabaseSqlite extends DatabaseBase { return call_user_func_array( $function, $args ); } + /** + * @param $s string + * @return string + */ protected function replaceVars( $s ) { $s = parent::replaceVars( $s ); if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) { @@ -596,23 +776,75 @@ class DatabaseSqlite extends DatabaseBase { return $s; } - /* + /** * Build a concatenation list to feed into a SQL query + * + * @param $stringList array + * + * @return string */ function buildConcat( $stringList ) { return '(' . implode( ') || (', $stringList ) . ')'; } + /** + * @throws MWException + * @param $oldName + * @param $newName + * @param $temporary bool + * @param $fname string + * @return bool|ResultWrapper + */ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) { - $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name='$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( '/\b' . preg_quote( $oldName ) . '\b/', $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" ); + } else { + $sql = str_replace( 'CREATE TABLE', 'CREATE TEMPORARY TABLE', $sql ); + } + } return $this->query( $sql, $fname ); } + + + /** + * List all tables on the database + * + * @param $prefix Only show tables with this prefix, e.g. mw_ + * @param $fname String: calling function name + * + * @return array + */ + function listTables( $prefix = null, $fname = 'DatabaseSqlite::listTables' ) { + $result = $this->select( + 'sqlite_master', + 'name', + "type='table'" + ); + + $endArray = array(); + + foreach( $result as $table ) { + $vars = get_object_vars($table); + $table = array_pop( $vars ); + + if( !$prefix || strpos( $table, $prefix ) === 0 ) { + if ( strpos( $table, 'sqlite_' ) !== 0 ) { + $endArray[] = $table; + } + + } + } + + return $endArray; + } } // end DatabaseSqlite class @@ -656,6 +888,9 @@ class SQLiteField implements Field { return $this->info->dflt_value; } + /** + * @return bool + */ function isNullable() { return !$this->info->notnull; } diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php new file mode 100644 index 00000000..d1bced6b --- /dev/null +++ b/includes/db/DatabaseUtility.php @@ -0,0 +1,268 @@ +<?php +/** + * Utility class. + * @ingroup Database + */ +class DBObject { + public $mData; + + function __construct( $data ) { + $this->mData = $data; + } + + function isLOB() { + return false; + } + + function data() { + return $this->mData; + } +} + +/** + * Utility class + * @ingroup Database + * + * This allows us to distinguish a blob from a normal string and an array of strings + */ +class Blob { + private $mData; + + function __construct( $data ) { + $this->mData = $data; + } + + function fetch() { + return $this->mData; + } +} + +/** + * Base for all database-specific classes representing information about database fields + * @ingroup Database + */ +interface Field { + /** + * Field name + * @return string + */ + function name(); + + /** + * Name of table this field belongs to + * @return string + */ + function tableName(); + + /** + * Database type + * @return string + */ + function type(); + + /** + * Whether this field can store NULL values + * @return bool + */ + function isNullable(); +} + +/** + * Result wrapper for grabbing data queried by someone else + * @ingroup Database + */ +class ResultWrapper implements Iterator { + var $db, $result, $pos = 0, $currentRow = null; + + /** + * Create a new result object from a result resource and a Database object + * + * @param DatabaseBase $database + * @param resource $result + */ + function __construct( $database, $result ) { + $this->db = $database; + + if ( $result instanceof ResultWrapper ) { + $this->result = $result->result; + } else { + $this->result = $result; + } + } + + /** + * Get the number of rows in a result object + * + * @return integer + */ + function numRows() { + return $this->db->numRows( $this ); + } + + /** + * Fetch the next row from the given result object, in object form. + * Fields can be retrieved with $row->fieldname, with fields acting like + * member variables. + * + * @return object + * @throws DBUnexpectedError Thrown if the database returns an error + */ + function fetchObject() { + return $this->db->fetchObject( $this ); + } + + /** + * Fetch the next row from the given result object, in associative array + * form. Fields are retrieved with $row['fieldname']. + * + * @return Array + * @throws DBUnexpectedError Thrown if the database returns an error + */ + function fetchRow() { + return $this->db->fetchRow( $this ); + } + + /** + * Free a result object + */ + function free() { + $this->db->freeResult( $this ); + unset( $this->result ); + unset( $this->db ); + } + + /** + * Change the position of the cursor in a result object. + * See mysql_data_seek() + * + * @param $row integer + */ + function seek( $row ) { + $this->db->dataSeek( $this, $row ); + } + + /********************* + * Iterator functions + * Note that using these in combination with the non-iterator functions + * above may cause rows to be skipped or repeated. + */ + + function rewind() { + if ( $this->numRows() ) { + $this->db->dataSeek( $this, 0 ); + } + $this->pos = 0; + $this->currentRow = null; + } + + function current() { + if ( is_null( $this->currentRow ) ) { + $this->next(); + } + return $this->currentRow; + } + + function key() { + return $this->pos; + } + + function next() { + $this->pos++; + $this->currentRow = $this->fetchObject(); + return $this->currentRow; + } + + function valid() { + return $this->current() !== false; + } +} + +/** + * Overloads the relevant methods of the real ResultsWrapper so it + * 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; + + function __construct( $array ) { + $this->result = $array; + } + + /** + * @return int + */ + function numRows() { + return count( $this->result ); + } + + function fetchRow() { + if ( $this->pos < count( $this->result ) ) { + $this->currentRow = $this->result[$this->pos]; + } else { + $this->currentRow = false; + } + $this->pos++; + return $this->currentRow; + } + + function seek( $row ) { + $this->pos = $row; + } + + function free() {} + + // Callers want to be able to access fields with $this->fieldName + function fetchObject() { + $this->fetchRow(); + if ( $this->currentRow ) { + return (object)$this->currentRow; + } else { + return false; + } + } + + function rewind() { + $this->pos = 0; + $this->currentRow = null; + } + + 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. + */ +class LikeMatch { + private $str; + + /** + * Store a string into a LikeMatch marker object. + * + * @param String $s + */ + public function __construct( $s ) { + $this->str = $s; + } + + /** + * Return the original stored string. + * + * @return String + */ + public function toString() { + return $this->str; + } +} + +/** + * An object representing a master or slave position in a replicated setup. + */ +interface DBMasterPos { +} + diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php index f84a70e5..22a84960 100644 --- a/includes/db/LBFactory.php +++ b/includes/db/LBFactory.php @@ -11,6 +11,10 @@ * @ingroup Database */ abstract class LBFactory { + + /** + * @var LBFactory + */ static $instance; /** @@ -23,15 +27,9 @@ abstract class LBFactory { } /** - * Resets the singleton for use if it's been disabled. Does nothing otherwise - */ - public static function enableBackend() { - if( self::$instance instanceof LBFactory_Fake ) - self::$instance = null; - } - - /** * Get an LBFactory instance + * + * @return LBFactory */ static function &singleton() { if ( is_null( self::$instance ) ) { @@ -56,6 +54,8 @@ abstract class LBFactory { /** * Set the instance to be the given object + * + * @param $instance LBFactory */ static function setInstance( $instance ) { self::destroyInstance(); @@ -84,21 +84,25 @@ abstract class LBFactory { */ abstract function getMainLB( $wiki = false ); - /* + /** * Create a new load balancer for external storage. The resulting object will be * untracked, not chronology-protected, and the caller is responsible for * cleaning it up. * * @param $cluster String: external storage cluster, or false for core * @param $wiki String: wiki ID, or false for the current wiki + * + * @return LoadBalancer */ abstract function newExternalLB( $cluster, $wiki = false ); - /* + /** * Get a cached (tracked) load balancer for external storage * * @param $cluster String: external storage cluster, or false for core * @param $wiki String: wiki ID, or false for the current wiki + * + * @return LoadBalancer */ abstract function &getExternalLB( $cluster, $wiki = false ); @@ -141,6 +145,10 @@ abstract class LBFactory { * A simple single-master LBFactory that gets its configuration from the b/c globals */ class LBFactory_Simple extends LBFactory { + + /** + * @var LoadBalancer + */ var $mainLB; var $extLBs = array(); @@ -151,6 +159,10 @@ class LBFactory_Simple extends LBFactory { $this->chronProt = new ChronologyProtector; } + /** + * @param $wiki + * @return LoadBalancer + */ function newMainLB( $wiki = false ) { global $wgDBservers, $wgMasterWaitTimeout; if ( $wgDBservers ) { @@ -174,6 +186,10 @@ class LBFactory_Simple extends LBFactory { )); } + /** + * @param $wiki + * @return LoadBalancer + */ function getMainLB( $wiki = false ) { if ( !isset( $this->mainLB ) ) { $this->mainLB = $this->newMainLB( $wiki ); @@ -183,6 +199,12 @@ class LBFactory_Simple extends LBFactory { return $this->mainLB; } + /** + * @throws MWException + * @param $cluster + * @param $wiki + * @return LoadBalancer + */ function newExternalLB( $cluster, $wiki = false ) { global $wgExternalServers; if ( !isset( $wgExternalServers[$cluster] ) ) { @@ -193,6 +215,11 @@ class LBFactory_Simple extends LBFactory { )); } + /** + * @param $cluster + * @param $wiki + * @return array + */ function &getExternalLB( $cluster, $wiki = false ) { if ( !isset( $this->extLBs[$cluster] ) ) { $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki ); diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php index 0d411ec6..61e56e78 100644 --- a/includes/db/LBFactory_Multi.php +++ b/includes/db/LBFactory_Multi.php @@ -98,6 +98,10 @@ class LBFactory_Multi extends LBFactory { return $section; } + /** + * @param $wiki string + * @return LoadBalancer + */ function newMainLB( $wiki = false ) { list( $dbName, ) = $this->getDBNameAndPrefix( $wiki ); $section = $this->getSectionForWiki( $wiki ); @@ -111,6 +115,10 @@ class LBFactory_Multi extends LBFactory { return $this->newLoadBalancer( $this->serverTemplate, $this->sectionLoads[$section], $groupLoads ); } + /** + * @param $wiki + * @return LoadBalancer + */ function getMainLB( $wiki = false ) { $section = $this->getSectionForWiki( $wiki ); if ( !isset( $this->mainLBs[$section] ) ) { @@ -122,6 +130,11 @@ class LBFactory_Multi extends LBFactory { return $this->mainLBs[$section]; } + /** + * @param $cluster + * @param $wiki + * @return LoadBalancer + */ function newExternalLB( $cluster, $wiki = false ) { if ( !isset( $this->externalLoads[$cluster] ) ) { throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" ); @@ -136,6 +149,11 @@ class LBFactory_Multi extends LBFactory { return $this->newLoadBalancer( $template, $this->externalLoads[$cluster], array() ); } + /** + * @param $cluster + * @param $wiki + * @return LoadBalancer + */ function &getExternalLB( $cluster, $wiki = false ) { if ( !isset( $this->extLBs[$cluster] ) ) { $this->extLBs[$cluster] = $this->newExternalLB( $cluster, $wiki ); @@ -146,6 +164,8 @@ class LBFactory_Multi extends LBFactory { /** * Make a new load balancer object based on template and load array + * + * @return LoadBalancer */ function newLoadBalancer( $template, $loads, $groupLoads ) { global $wgMasterWaitTimeout; @@ -159,6 +179,8 @@ class LBFactory_Multi extends LBFactory { /** * Make a server array as expected by LoadBalancer::__construct, using a template and load array + * + * @return array */ function makeServerArray( $template, $loads, $groupLoads ) { $servers = array(); diff --git a/includes/db/LBFactory_Single.php b/includes/db/LBFactory_Single.php index 25acdc5b..89b41321 100644 --- a/includes/db/LBFactory_Single.php +++ b/includes/db/LBFactory_Single.php @@ -7,29 +7,55 @@ class LBFactory_Single extends LBFactory { protected $lb; /** - * @param $conf An associative array with one member: + * @param $conf array An associative array with one member: * - connection: The DatabaseBase connection object */ function __construct( $conf ) { $this->lb = new LoadBalancer_Single( $conf ); } + /** + * @param $wiki + * + * @return LoadBalancer_Single + */ function newMainLB( $wiki = false ) { return $this->lb; } + /** + * @param $wiki + * + * @return LoadBalancer_Single + */ function getMainLB( $wiki = false ) { return $this->lb; } + /** + * @param $cluster + * @param $wiki + * + * @return LoadBalancer_Single + */ function newExternalLB( $cluster, $wiki = false ) { return $this->lb; } + /** + * @param $cluster + * @param $wiki + * + * @return LoadBalancer_Single + */ function &getExternalLB( $cluster, $wiki = false ) { return $this->lb; } + /** + * @param $callback string|array + * @param $params array + */ function forEachLB( $callback, $params = array() ) { call_user_func_array( $callback, array_merge( array( $this->lb ), $params ) ); } @@ -39,8 +65,15 @@ class LBFactory_Single extends LBFactory { * Helper class for LBFactory_Single. */ class LoadBalancer_Single extends LoadBalancer { + + /** + * @var DatabaseBase + */ var $db; + /** + * @param $params array + */ function __construct( $params ) { $this->db = $params['connection']; parent::__construct( array( 'servers' => array( array( @@ -51,6 +84,13 @@ class LoadBalancer_Single extends LoadBalancer { ) ) ) ); } + /** + * + * @param $server string + * @param $dbNameOverride bool + * + * @return DatabaseBase + */ function reallyOpenConnection( $server, $dbNameOverride = false ) { return $this->db; } diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index d899ce07..c7210c4c 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -13,13 +13,13 @@ * @ingroup Database */ class LoadBalancer { - /* private */ var $mServers, $mConns, $mLoads, $mGroupLoads; - /* private */ var $mErrorConnection; - /* private */ var $mReadIndex, $mAllowLagged; - /* private */ var $mWaitForPos, $mWaitTimeout; - /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error'; - /* private */ var $mParentInfo, $mLagTimes; - /* private */ var $mLoadMonitorClass, $mLoadMonitor; + private $mServers, $mConns, $mLoads, $mGroupLoads; + private $mErrorConnection; + private $mReadIndex, $mAllowLagged; + private $mWaitForPos, $mWaitTimeout; + private $mLaggedSlaveMode, $mLastError = 'Unknown error'; + private $mParentInfo, $mLagTimes; + private $mLoadMonitorClass, $mLoadMonitor; /** * @param $params Array with keys: @@ -27,8 +27,7 @@ class LoadBalancer { * masterWaitTimeout Replication lag wait timeout * loadMonitor Name of a class used to fetch server lag and load. */ - function __construct( $params ) - { + function __construct( $params ) { if ( !isset( $params['servers'] ) ) { throw new MWException( __CLASS__.': missing servers parameter' ); } @@ -51,8 +50,17 @@ class LoadBalancer { $this->mLaggedSlaveMode = false; $this->mErrorConnection = false; $this->mAllowLagged = false; - $this->mLoadMonitorClass = isset( $params['loadMonitor'] ) - ? $params['loadMonitor'] : 'LoadMonitor_MySQL'; + + if ( isset( $params['loadMonitor'] ) ) { + $this->mLoadMonitorClass = $params['loadMonitor']; + } else { + $master = reset( $params['servers'] ); + if ( isset( $master['type'] ) && $master['type'] === 'mysql' ) { + $this->mLoadMonitorClass = 'LoadMonitor_MySQL'; + } else { + $this->mLoadMonitorClass = 'LoadMonitor_Null'; + } + } foreach( $params['servers'] as $i => $server ) { $this->mLoads[$i] = $server['load']; @@ -69,6 +77,8 @@ class LoadBalancer { /** * Get a LoadMonitor instance + * + * @return LoadMonitor */ function getLoadMonitor() { if ( !isset( $this->mLoadMonitor ) ) { @@ -88,9 +98,12 @@ class LoadBalancer { /** * Given an array of non-normalised probabilities, this function will select * an element and return the appropriate key + * + * @param $weights + * + * @return int */ - function pickRandom( $weights ) - { + function pickRandom( $weights ) { if ( !is_array( $weights ) || count( $weights ) == 0 ) { return false; } @@ -116,6 +129,11 @@ class LoadBalancer { return $i; } + /** + * @param $loads + * @param $wiki bool + * @return bool|int|string + */ function getRandomNonLagged( $loads, $wiki = false ) { # Unset excessively lagged servers $lags = $this->getLagTimes( $wiki ); @@ -160,11 +178,14 @@ class LoadBalancer { * always return a consistent index during a given invocation * * Side effect: opens connections to databases + * @param $group bool + * @param $wiki bool + * @return bool|int|string */ function getReaderIndex( $group = false, $wiki = false ) { global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype; - # FIXME: For now, only go through all this for mysql databases + # @todo FIXME: For now, only go through all this for mysql databases if ($wgDBtype != 'mysql') { return $this->getWriterIndex(); } @@ -220,7 +241,7 @@ class LoadBalancer { $i = $this->getRandomNonLagged( $currentLoads, $wiki ); if ( $i === false && count( $currentLoads ) != 0 ) { # All slaves lagged. Switch to read-only mode - $wgReadOnly = 'The database has been automatically locked ' . + $wgReadOnly = 'The database has been automatically locked ' . 'while the slave database servers catch up to the master'; $i = $this->pickRandom( $currentLoads ); $laggedSlaveMode = true; @@ -229,7 +250,7 @@ class LoadBalancer { if ( $i === false ) { # pickRandom() returned false - # This is permanent and means the configuration or the load monitor + # 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__ ); @@ -247,7 +268,7 @@ class LoadBalancer { } // Perform post-connection backoff - $threshold = isset( $this->mServers[$i]['max threads'] ) + $threshold = isset( $this->mServers[$i]['max threads'] ) ? $this->mServers[$i]['max threads'] : false; $backoff = $this->getLoadMonitor()->postConnectionBackoff( $conn, $threshold ); @@ -256,7 +277,7 @@ class LoadBalancer { if ( $wiki !== false ) { $this->reuseConnection( $conn ); } - + if ( $backoff ) { # Post-connection overload, don't use this server for now $totalThreadsConnected += $backoff; @@ -339,7 +360,7 @@ class LoadBalancer { } wfProfileOut( __METHOD__ ); } - + /** * Set the master wait position and wait for ALL slaves to catch up to it */ @@ -347,7 +368,7 @@ class LoadBalancer { wfProfileIn( __METHOD__ ); $this->mWaitForPos = $pos; for ( $i = 1; $i < count( $this->mServers ); $i++ ) { - $this->doWait( $i ); + $this->doWait( $i , true ); } wfProfileOut( __METHOD__ ); } @@ -355,6 +376,8 @@ class LoadBalancer { /** * Get any open connection to a given server index, local or foreign * Returns false if there is no connection open + * + * @return DatabaseBase */ function getAnyOpenConnection( $i ) { foreach ( $this->mConns as $conns ) { @@ -368,12 +391,20 @@ class LoadBalancer { /** * Wait for a given slave to catch up to the master pos stored in $this */ - function doWait( $index ) { + function doWait( $index, $open = false ) { # Find a connection to wait on $conn = $this->getAnyOpenConnection( $index ); if ( !$conn ) { - wfDebug( __METHOD__ . ": no connection open\n" ); - return false; + 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" ); @@ -392,11 +423,11 @@ class LoadBalancer { /** * Get a connection by index * This is the main entry point for this class. - * + * * @param $i Integer: server index * @param $groups Array: query groups * @param $wiki String: wiki ID - * + * * @return DatabaseBase */ public function &getConnection( $i, $groups = array(), $wiki = false ) { @@ -460,6 +491,8 @@ class LoadBalancer { * Mark a foreign connection as being available for reuse under a different * DB name or prefix. This mechanism is reference-counted, and must be called * the same number of times as getConnection() to work. + * + * @param DatabaseBase $conn */ public function reuseConnection( $conn ) { $serverIndex = $conn->getLBInfo('serverIndex'); @@ -506,8 +539,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 $wiki String: wiki ID to open + * @param $i Integer server index + * @param $wiki String wiki ID to open * @return DatabaseBase * * @access private @@ -615,6 +648,7 @@ class LoadBalancer { * * @param $index Integer: server index * @access private + * @return bool */ function isOpen( $index ) { if( !is_integer( $index ) ) { @@ -627,10 +661,13 @@ class LoadBalancer { * Really opens a connection. Uncached. * Returns a Database object whether or not the connection was successful. * @access private + * + * @return DatabaseBase */ function reallyOpenConnection( $server, $dbNameOverride = false ) { if( !is_array( $server ) ) { - throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' ); + throw new MWException( 'You must update your load-balancing configuration. ' . + 'See DefaultSettings.php entry for $wgDBservers.' ); } $host = $server['host']; @@ -643,12 +680,13 @@ class LoadBalancer { # Create object wfDebug( "Connecting to $host $dbname...\n" ); try { - $db = DatabaseBase::newFromType( $server['type'], $server ); + $db = DatabaseBase::factory( $server['type'], $server ); } catch ( DBConnectionError $e ) { - // FIXME: This is probably the ugliest thing I have ever done to + // FIXME: This is probably the ugliest thing I have ever done to // PHP. I'm half-expecting it to segfault, just out of disgust. -- TS $db = $e->db; } + if ( $db->isOpen() ) { wfDebug( "Connected to $host $dbname.\n" ); } else { @@ -669,7 +707,7 @@ class LoadBalancer { if ( !is_object( $conn ) ) { // No last connection, probably due to all servers being too busy - wfLogDBError( "LB failure with no last connection\n" ); + wfLogDBError( "LB failure with no last connection. Connection error: {$this->mLastError}\n" ); $conn = new Database; // If all servers were busy, mLastError will contain something sensible throw new DBConnectionError( $conn, $this->mLastError ); @@ -687,6 +725,8 @@ class LoadBalancer { /** * Returns true if the specified index is a valid server index + * + * @return bool */ function haveIndex( $i ) { return array_key_exists( $i, $this->mServers ); @@ -694,6 +734,8 @@ class LoadBalancer { /** * Returns true if the specified index is valid and has non-zero load + * + * @return bool */ function isNonZeroLoad( $i ) { return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0; @@ -701,6 +743,8 @@ class LoadBalancer { /** * Get the number of defined servers (not the number of open connections) + * + * @return int */ function getServerCount() { return count( $this->mServers ); @@ -732,6 +776,13 @@ class LoadBalancer { } /** + * Sets the server info structure for the given index. Entry at index $i is created if it doesn't exist + */ + function setServerInfo( $i, $serverInfo ) { + $this->mServers[$i] = $serverInfo; + } + + /** * Get the current master position for chronology control purposes * @return mixed */ @@ -774,6 +825,8 @@ class LoadBalancer { /** * Deprecated function, typo in function name + * + * @deprecated in 1.18 */ function closeConnecton( $conn ) { $this->closeConnection( $conn ); @@ -783,7 +836,7 @@ class LoadBalancer { * 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 + * @param $conn * @return void */ function closeConnection( $conn ) { @@ -818,7 +871,9 @@ class LoadBalancer { } } - /* Issue COMMIT only on master, only if queries were done on connection */ + /** + * Issue COMMIT only on master, only if queries were done on connection + */ function commitMasterChanges() { // Always 0, but who knows.. :) $masterIndex = $this->getWriterIndex(); @@ -843,10 +898,11 @@ class LoadBalancer { } /* Disables/enables lag checks */ - function allowLagged($mode=null) { - if ($mode===null) + function allowLagged( $mode = null ) { + if ( $mode === null) { return $this->mAllowLagged; - $this->mAllowLagged=$mode; + } + $this->mAllowLagged = $mode; } function pingAll() { @@ -880,47 +936,84 @@ class LoadBalancer { /** * Get the hostname and lag time of the most-lagged slave. * This is useful for maintenance scripts that need to throttle their updates. - * May attempt to open connections to slaves on the default DB. + * 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 $wiki string 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 = ''; - foreach ( $this->mServers as $i => $conn ) { - $conn = false; - if ( $wiki === false ) { - $conn = $this->getAnyOpenConnection( $i ); - } - if ( !$conn ) { - $conn = $this->openConnection( $i, $wiki ); - } - if ( !$conn ) { - continue; - } - $lag = $conn->getLag(); - if ( $lag > $maxLag ) { - $maxLag = $lag; - $host = $this->mServers[$i]['host']; + $maxIndex = 0; + if ( $this->getServerCount() > 1 ) { // no replication = no lag + foreach ( $this->mServers as $i => $conn ) { + $conn = false; + if ( $wiki === false ) { + $conn = $this->getAnyOpenConnection( $i ); + } + if ( !$conn ) { + $conn = $this->openConnection( $i, $wiki ); + } + if ( !$conn ) { + continue; + } + $lag = $conn->getLag(); + if ( $lag > $maxLag ) { + $maxLag = $lag; + $host = $this->mServers[$i]['host']; + $maxIndex = $i; + } } } - return array( $host, $maxLag ); + return array( $host, $maxLag, $maxIndex ); } /** * Get lag time for each server * Results are cached for a short time in memcached, and indefinitely in the process cache + * + * @param $wiki + * + * @return array */ function getLagTimes( $wiki = false ) { # Try process cache if ( isset( $this->mLagTimes ) ) { return $this->mLagTimes; } - # No, send the request to the load monitor - $this->mLagTimes = $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki ); + if ( $this->getServerCount() == 1 ) { + # No replication + $this->mLagTimes = array( 0 => 0 ); + } else { + # Send the request to the load monitor + $this->mLagTimes = $this->getLoadMonitor()->getLagTimes( + array_keys( $this->mServers ), $wiki ); + } return $this->mLagTimes; } /** + * Get the lag in seconds for a given connection, or zero if this load + * balancer does not have replication enabled. + * + * This should be used in preference to Database::getLag() in cases where + * replication may not be in use, since there is no way to determine if + * replication is in use at the connection level without running + * potentially restricted queries such as SHOW SLAVE STATUS. Using this + * function instead of Database::getLag() avoids a fatal error in this + * case on many installations. + */ + function safeGetLag( $conn ) { + if ( $this->getServerCount() == 1 ) { + return 0; + } else { + return $conn->getLag(); + } + } + + /** * Clear the cache for getLagTimes */ function clearLagTimeCache() { diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index 9b959728..a6370c9e 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -14,12 +14,14 @@ interface LoadMonitor { /** * Construct a new LoadMonitor with a given LoadBalancer parent + * + * @param LoadBalancer $parent */ function __construct( $parent ); - + /** * Perform pre-connection load ratio adjustment. - * @param $loads Array + * @param $loads array * @param $group String: the selected query group * @param $wiki String */ @@ -28,13 +30,13 @@ interface LoadMonitor { /** * 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 + * 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 + * 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. * @@ -45,10 +47,30 @@ interface LoadMonitor { /** * Return an estimate of replication lag for each server + * + * @param $serverIndexes + * @param $wiki + * + * @return array */ function getLagTimes( $serverIndexes, $wiki ); } +class LoadMonitor_Null implements LoadMonitor { + function __construct( $parent ) { + } + + function scaleLoads( &$loads, $group = false, $wiki = false ) { + } + + function postConnectionBackoff( $conn, $threshold ) { + } + + function getLagTimes( $serverIndexes, $wiki ) { + return array_fill_keys( $serverIndexes, 0 ); + } +} + /** * Basic MySQL load monitor with no external dependencies @@ -57,16 +79,38 @@ interface LoadMonitor { * @ingroup Database */ class LoadMonitor_MySQL implements LoadMonitor { - var $parent; // LoadBalancer + /** + * @var LoadBalancer + */ + var $parent; + + /** + * @param LoadBalancer $parent + */ function __construct( $parent ) { $this->parent = $parent; } + /** + * @param $loads + * @param $group bool + * @param $wiki bool + */ function scaleLoads( &$loads, $group = false, $wiki = false ) { } + /** + * @param $serverIndexes + * @param $wiki + * @return array + */ 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__ ); $expiry = 5; $requestRate = 10; @@ -74,7 +118,7 @@ class LoadMonitor_MySQL implements LoadMonitor { global $wgMemc; if ( empty( $wgMemc ) ) $wgMemc = wfGetMainCache(); - + $masterName = $this->parent->getServerName( 0 ); $memcKey = wfMemcKey( 'lag_times', $masterName ); $times = $wgMemc->get( $memcKey ); @@ -117,12 +161,19 @@ class LoadMonitor_MySQL implements LoadMonitor { return $lagTimes; } + /** + * @param $conn DatabaseBase + * @param $threshold + * @return int + */ function postConnectionBackoff( $conn, $threshold ) { if ( !$threshold ) { return 0; } - $status = $conn->getStatus("Thread%"); + $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; |