From 63601400e476c6cf43d985f3e7b9864681695ed4 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 18 Jan 2013 16:46:04 +0100 Subject: Update to MediaWiki 1.20.2 this update includes: * adjusted Arch Linux skin * updated FluxBBAuthPlugin * patch for https://bugzilla.wikimedia.org/show_bug.cgi?id=44024 --- includes/db/CloneDatabase.php | 1 + includes/db/Database.php | 701 +++++++++++++++++++++------------------ includes/db/DatabaseError.php | 27 +- includes/db/DatabaseIbm_db2.php | 165 +++++---- includes/db/DatabaseMssql.php | 53 ++- includes/db/DatabaseMysql.php | 159 +++++---- includes/db/DatabaseOracle.php | 48 +-- includes/db/DatabasePostgres.php | 622 ++++++++++++++++++++++++++++------ includes/db/DatabaseSqlite.php | 80 ++--- includes/db/DatabaseUtility.php | 28 +- includes/db/IORMRow.php | 275 +++++++++++++++ includes/db/IORMTable.php | 448 +++++++++++++++++++++++++ includes/db/LBFactory.php | 29 +- includes/db/LBFactory_Multi.php | 17 +- includes/db/LBFactory_Single.php | 21 ++ includes/db/LoadBalancer.php | 25 +- includes/db/LoadMonitor.php | 17 +- includes/db/ORMIterator.php | 31 ++ includes/db/ORMResult.php | 123 +++++++ includes/db/ORMRow.php | 663 ++++++++++++++++++++++++++++++++++++ includes/db/ORMTable.php | 675 +++++++++++++++++++++++++++++++++++++ 21 files changed, 3558 insertions(+), 650 deletions(-) create mode 100644 includes/db/IORMRow.php create mode 100644 includes/db/IORMTable.php create mode 100644 includes/db/ORMIterator.php create mode 100644 includes/db/ORMResult.php create mode 100644 includes/db/ORMRow.php create mode 100644 includes/db/ORMTable.php (limited to 'includes/db') diff --git a/includes/db/CloneDatabase.php b/includes/db/CloneDatabase.php index bd0895cf..4e43642f 100644 --- a/includes/db/CloneDatabase.php +++ b/includes/db/CloneDatabase.php @@ -20,6 +20,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * + * @file * @ingroup Database */ diff --git a/includes/db/Database.php b/includes/db/Database.php index f3e84675..5f10b97d 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -2,10 +2,26 @@ /** * @defgroup Database Database * + * This file deals with database interface functions + * and query specifics/optimisations. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file * @ingroup Database - * This file deals with database interface functions - * and query specifics/optimisations */ /** Number of times to re-try an operation in case of deadlock */ @@ -209,12 +225,15 @@ abstract class DatabaseBase implements DatabaseType { protected $mServer, $mUser, $mPassword, $mDBname; - /** - * @var DatabaseBase - */ protected $mConn = null; protected $mOpened = false; + /** + * @since 1.20 + * @var array of Closure + */ + protected $mTrxIdleCallbacks = array(); + protected $mTablePrefix; protected $mFlags; protected $mTrxLevel = 0; @@ -253,9 +272,9 @@ abstract class DatabaseBase implements DatabaseType { * - false to disable debugging * - omitted or null to do nothing * - * @return The previous value of the flag + * @return bool|null previous value of the flag */ - function debug( $debug = null ) { + public function debug( $debug = null ) { return wfSetBit( $this->mFlags, DBO_DEBUG, $debug ); } @@ -279,9 +298,9 @@ abstract class DatabaseBase implements DatabaseType { * * @param $buffer null|bool * - * @return The previous value of the flag + * @return null|bool The previous value of the flag */ - function bufferResults( $buffer = null ) { + public function bufferResults( $buffer = null ) { if ( is_null( $buffer ) ) { return !(bool)( $this->mFlags & DBO_NOBUFFER ); } else { @@ -300,7 +319,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool The previous value of the flag. */ - function ignoreErrors( $ignoreErrors = null ) { + public function ignoreErrors( $ignoreErrors = null ) { return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors ); } @@ -310,28 +329,28 @@ abstract class DatabaseBase implements DatabaseType { * 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 + * @param $level int An integer (0 or 1), or omitted to leave it unchanged. + * @return int The previous value */ - function trxLevel( $level = null ) { + public function trxLevel( $level = null ) { return wfSetVar( $this->mTrxLevel, $level ); } /** * 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 + * @param $count int The count to set, or omitted to leave it unchanged. + * @return int The error count */ - function errorCount( $count = null ) { + public 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. + * @param $prefix string The table prefix to set, or omitted to leave it unchanged. + * @return string The previous table prefix. */ - function tablePrefix( $prefix = null ) { + public function tablePrefix( $prefix = null ) { return wfSetVar( $this->mTablePrefix, $prefix ); } @@ -344,7 +363,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return LoadBalancer|null */ - function getLBInfo( $name = null ) { + public function getLBInfo( $name = null ) { if ( is_null( $name ) ) { return $this->mLBInfo; } else { @@ -364,7 +383,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $name * @param $value */ - function setLBInfo( $name, $value = null ) { + public function setLBInfo( $name, $value = null ) { if ( is_null( $value ) ) { $this->mLBInfo = $name; } else { @@ -377,7 +396,7 @@ abstract class DatabaseBase implements DatabaseType { * * @param $lag int */ - function setFakeSlaveLag( $lag ) { + public function setFakeSlaveLag( $lag ) { $this->mFakeSlaveLag = $lag; } @@ -386,7 +405,7 @@ abstract class DatabaseBase implements DatabaseType { * * @param $enabled bool */ - function setFakeMaster( $enabled = true ) { + public function setFakeMaster( $enabled = true ) { $this->mFakeMaster = $enabled; } @@ -395,7 +414,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function cascadingDeletes() { + public function cascadingDeletes() { return false; } @@ -404,7 +423,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function cleanupTriggers() { + public function cleanupTriggers() { return false; } @@ -414,7 +433,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function strictIPs() { + public function strictIPs() { return false; } @@ -423,7 +442,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function realTimestamps() { + public function realTimestamps() { return false; } @@ -432,7 +451,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function implicitGroupby() { + public function implicitGroupby() { return true; } @@ -442,17 +461,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function implicitOrderby() { - return true; - } - - /** - * Returns true if this database requires that SELECT DISTINCT queries require that all - ORDER BY expressions occur in the SELECT list per the SQL92 standard - * - * @return bool - */ - function standardSelectDistinct() { + public function implicitOrderby() { return true; } @@ -462,7 +471,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function searchableIPs() { + public function searchableIPs() { return false; } @@ -471,7 +480,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function functionalIndexes() { + public function functionalIndexes() { return false; } @@ -479,7 +488,7 @@ abstract class DatabaseBase implements DatabaseType { * Return the last query that went through DatabaseBase::query() * @return String */ - function lastQuery() { + public function lastQuery() { return $this->mLastQuery; } @@ -489,15 +498,25 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function doneWrites() { + public function doneWrites() { return $this->mDoneWrites; } + /** + * Returns true if there is a transaction open with possible write + * queries or transaction idle callbacks waiting on it to finish. + * + * @return bool + */ + public function writesOrCallbacksPending() { + return $this->mTrxLevel && ( $this->mDoneWrites || $this->mTrxIdleCallbacks ); + } + /** * Is a connection to the database open? * @return Boolean */ - function isOpen() { + public function isOpen() { return $this->mOpened; } @@ -513,8 +532,12 @@ abstract class DatabaseBase implements DatabaseType { * and removes it in command line mode * - DBO_PERSISTENT: use persistant database connection */ - function setFlag( $flag ) { + public function setFlag( $flag ) { + global $wgDebugDBTransactions; $this->mFlags |= $flag; + if ( ( $flag & DBO_TRX) & $wgDebugDBTransactions ) { + wfDebug("Implicit transactions are now disabled.\n"); + } } /** @@ -522,8 +545,12 @@ abstract class DatabaseBase implements DatabaseType { * * @param $flag: same as setFlag()'s $flag param */ - function clearFlag( $flag ) { + public function clearFlag( $flag ) { + global $wgDebugDBTransactions; $this->mFlags &= ~$flag; + if ( ( $flag & DBO_TRX ) && $wgDebugDBTransactions ) { + wfDebug("Implicit transactions are now disabled.\n"); + } } /** @@ -532,7 +559,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $flag: same as setFlag()'s $flag param * @return Boolean */ - function getFlag( $flag ) { + public function getFlag( $flag ) { return !!( $this->mFlags & $flag ); } @@ -543,14 +570,14 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function getProperty( $name ) { + public function getProperty( $name ) { return $this->$name; } /** * @return string */ - function getWikiID() { + public function getWikiID() { if ( $this->mTablePrefix ) { return "{$this->mDBname}-{$this->mTablePrefix}"; } else { @@ -588,15 +615,21 @@ abstract class DatabaseBase implements DatabaseType { function __construct( $server = false, $user = false, $password = false, $dbName = false, $flags = 0, $tablePrefix = 'get from global' ) { - global $wgDBprefix, $wgCommandLineMode; + global $wgDBprefix, $wgCommandLineMode, $wgDebugDBTransactions; $this->mFlags = $flags; if ( $this->mFlags & DBO_DEFAULT ) { if ( $wgCommandLineMode ) { $this->mFlags &= ~DBO_TRX; + if ( $wgDebugDBTransactions ) { + wfDebug("Implicit transaction open disabled.\n"); + } } else { $this->mFlags |= DBO_TRX; + if ( $wgDebugDBTransactions ) { + wfDebug("Implicit transaction open enabled.\n"); + } } } @@ -621,35 +654,6 @@ abstract class DatabaseBase implements DatabaseType { throw new MWException( 'Database serialization may cause problems, since the connection is not restored on wakeup.' ); } - /** - * Same as new DatabaseMysql( ... ), kept for backward compatibility - * @deprecated since 1.17 - * - * @param $server - * @param $user - * @param $password - * @param $dbName - * @param $flags int - * @return DatabaseMysql - */ - static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { - wfDeprecated( __METHOD__, '1.17' ); - return new DatabaseMysql( $server, $user, $password, $dbName, $flags ); - } - - /** - * Same as new factory( ... ), kept for backward compatibility - * @deprecated since 1.18 - * @see Database::factory() - */ - public final static function newFromType( $dbType, $p = array() ) { - wfDeprecated( __METHOD__, '1.18' ); - if ( isset( $p['tableprefix'] ) ) { - $p['tablePrefix'] = $p['tableprefix']; - } - return self::factory( $dbType, $p ); - } - /** * Given a DB type, construct the name of the appropriate child class of * DatabaseBase. This is designed to replace all of the manual stuff like: @@ -665,6 +669,8 @@ abstract class DatabaseBase implements DatabaseType { * @see ForeignDBRepo::getMasterDB() * @see WebInstaller_DBConnect::execute() * + * @since 1.18 + * * @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 @@ -707,7 +713,7 @@ abstract class DatabaseBase implements DatabaseType { } if ( $this->mPHPError ) { $error = preg_replace( '!\[\]!', '', $this->mPHPError ); - $error = preg_replace( '!^.*?:(.*)$!', '$1', $error ); + $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error ); return $error; } else { return false; @@ -728,11 +734,30 @@ abstract class DatabaseBase implements DatabaseType { * * @return Bool operation success. true if already closed. */ - function close() { - # Stub, should probably be overridden - return true; + public function close() { + if ( count( $this->mTrxIdleCallbacks ) ) { // sanity + throw new MWException( "Transaction idle callbacks still pending." ); + } + $this->mOpened = false; + if ( $this->mConn ) { + if ( $this->trxLevel() ) { + $this->commit( __METHOD__ ); + } + $ret = $this->closeConnection(); + $this->mConn = false; + return $ret; + } else { + return true; + } } + /** + * Closes underlying database connection + * @since 1.20 + * @return bool: Whether connection was closed successfully + */ + protected abstract function closeConnection(); + /** * @param $error String: fallback error message, used if none is given by DB */ @@ -762,8 +787,8 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function isWriteQuery( $sql ) { - return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW|\(SELECT)\b/i', $sql ); + public function isWriteQuery( $sql ) { + return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql ); } /** @@ -833,8 +858,13 @@ abstract class DatabaseBase implements DatabaseType { # 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 && strpos( $sqlstart, "SET " ) !== 0 ) + if ( strpos( $sqlstart, "SHOW " ) !== 0 && strpos( $sqlstart, "SET " ) !== 0 ) { + global $wgDebugDBTransactions; + if ( $wgDebugDBTransactions ) { + wfDebug("Implicit transaction start.\n"); + } $this->begin( __METHOD__ . " ($fname)" ); + } } if ( $this->debug() ) { @@ -863,6 +893,7 @@ abstract class DatabaseBase implements DatabaseType { if ( false === $ret && $this->wasErrorReissuable() ) { # Transaction is gone, like it or not $this->mTrxLevel = 0; + $this->mTrxIdleCallbacks = array(); // cancel wfDebug( "Connection lost, reconnecting...\n" ); if ( $this->ping() ) { @@ -903,7 +934,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String * @param $tempIgnore Boolean */ - function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { + public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { # Ignore errors during error handling to avoid infinite recursion $ignore = $this->ignoreErrors( true ); ++$this->mErrorCount; @@ -928,16 +959,12 @@ abstract class DatabaseBase implements DatabaseType { * & = 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. - * * @param $sql string * @param $func string * * @return array */ - function prepare( $sql, $func = 'DatabaseBase::prepare' ) { + protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) { /* MySQL doesn't support prepared statements (yet), so just pack up the query for reference. We'll manually replace the bits later. */ @@ -948,7 +975,7 @@ abstract class DatabaseBase implements DatabaseType { * Free a prepared query, generated by prepare(). * @param $prepared */ - function freePrepared( $prepared ) { + protected function freePrepared( $prepared ) { /* No-op by default */ } @@ -959,7 +986,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return ResultWrapper */ - function execute( $prepared, $args = null ) { + public function execute( $prepared, $args = null ) { if ( !is_array( $args ) ) { # Pull the var args $args = func_get_args(); @@ -972,41 +999,13 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Prepare & execute an SQL statement, quoting and inserting arguments - * in the appropriate places. + * For faking prepared SQL statements on DBs that don't support it directly. * - * 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' ); - - if ( !is_array( $args ) ) { - # Pull the var args - $args = func_get_args(); - array_shift( $args ); - } - - $retval = $this->execute( $prepared, $args ); - $this->freePrepared( $prepared ); - - return $retval; - } - - /** - * For faking prepared SQL statements on DBs that don't support - * it directly. * @param $preparedQuery String: a 'preparable' SQL statement * @param $args Array of arguments to fill it with * @return string executable SQL */ - function fillPrepared( $preparedQuery, $args ) { + public function fillPrepared( $preparedQuery, $args ) { reset( $args ); $this->preparedArgs =& $args; @@ -1022,7 +1021,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $matches Array * @return String */ - function fillPreparedArg( $matches ) { + protected function fillPreparedArg( $matches ) { switch( $matches[1] ) { case '\\?': return '?'; case '\\!': return '!'; @@ -1049,32 +1048,7 @@ abstract class DatabaseBase implements DatabaseType { * * @param $res Mixed: A SQL result */ - function freeResult( $res ) { - } - - /** - * 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 - * - * @param $table string - * @param $var - * @param $value - * @param $cond - * @param $fname string - * - * @return bool - */ - function set( $table, $var, $value, $cond, $fname = 'DatabaseBase::set' ) { - $table = $this->tableName( $table ); - $sql = "UPDATE $table SET $var = '" . - $this->strencode( $value ) . "' WHERE ($cond)"; - - return (bool)$this->query( $sql, $fname ); - } + public function freeResult( $res ) {} /** * A SELECT wrapper which returns a single field from a single result row. @@ -1091,9 +1065,9 @@ abstract class DatabaseBase implements DatabaseType { * @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. + * @return bool|mixed The value from the field, or false on failure. */ - function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField', + public function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField', $options = array() ) { if ( !is_array( $options ) ) { @@ -1126,7 +1100,7 @@ abstract class DatabaseBase implements DatabaseType { * @return Array * @see DatabaseBase::select() */ - function makeSelectOptions( $options ) { + public function makeSelectOptions( $options ) { $preLimitTail = $postLimitTail = ''; $startOpts = ''; @@ -1146,7 +1120,10 @@ abstract class DatabaseBase implements DatabaseType { } if ( isset( $options['HAVING'] ) ) { - $preLimitTail .= " HAVING {$options['HAVING']}"; + $having = is_array( $options['HAVING'] ) + ? $this->makeList( $options['HAVING'], LIST_AND ) + : $options['HAVING']; + $preLimitTail .= " HAVING {$having}"; } if ( isset( $options['ORDER BY'] ) ) { @@ -1245,10 +1222,12 @@ abstract class DatabaseBase implements DatabaseType { * @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: + * can be complete fragments of SQL, for direct inclusion into the SELECT + * query. If an array is given, field aliases can be specified, for example: + * + * array( 'maxrev' => 'MAX(rev_id)' ) * - * array( 'MAX(rev_id) AS maxrev' ) + * This includes an expression with the alias "maxrev" in the query. * * If an expression is given, care must be taken to ensure that it is * DBMS-independent. @@ -1305,7 +1284,9 @@ abstract class DatabaseBase implements DatabaseType { * - 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. + * - HAVING: May be either an string containing a HAVING clause or an array of + * conditions building the HAVING clause. If an array is given, the conditions + * constructed from each element are combined with AND. * * - ORDER BY: May be either an SQL fragment giving a field name or * expression to order by, or an array of such SQL fragments. @@ -1351,7 +1332,7 @@ abstract class DatabaseBase implements DatabaseType { * DBQueryError exception will be thrown, except if the "ignore errors" * option was set, in which case false will be returned. */ - function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', + public function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) { $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); @@ -1360,7 +1341,9 @@ abstract class DatabaseBase implements DatabaseType { /** * The equivalent of DatabaseBase::select() except that the constructed SQL - * is returned, instead of being immediately executed. + * is returned, instead of being immediately executed. This can be useful for + * doing UNION queries, where the SQL text of each query is needed. In general, + * however, callers outside of Database classes should just use select(). * * @param $table string|array Table name * @param $vars string|array Field names @@ -1369,12 +1352,14 @@ abstract class DatabaseBase implements DatabaseType { * @param $options string|array Query options * @param $join_conds string|array Join conditions * - * @return SQL query string. + * @return string SQL query string. * @see DatabaseBase::select() */ - function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) { + public function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', + $options = array(), $join_conds = array() ) + { if ( is_array( $vars ) ) { - $vars = implode( ',', $vars ); + $vars = implode( ',', $this->fieldNamesWithAlias( $vars ) ); } $options = (array)$options; @@ -1435,9 +1420,9 @@ abstract class DatabaseBase implements DatabaseType { * @param $options string|array Query options * @param $join_conds array|string Join conditions * - * @return ResultWrapper|bool + * @return object|bool */ - function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow', + public function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow', $options = array(), $join_conds = array() ) { $options = (array)$options; @@ -1481,7 +1466,7 @@ abstract class DatabaseBase implements DatabaseType { $fname = 'DatabaseBase::estimateRowCount', $options = array() ) { $rows = 0; - $res = $this->select ( $table, 'COUNT(*) AS rowcount', $conds, $fname, $options ); + $res = $this->select( $table, array( 'rowcount' => 'COUNT(*)' ), $conds, $fname, $options ); if ( $res ) { $row = $this->fetchRow( $res ); @@ -1527,7 +1512,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String: calling function name (optional) * @return Boolean: whether $table has filed $field */ - function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) { + public function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) { $info = $this->fieldInfo( $table, $field ); return (bool)$info; @@ -1544,7 +1529,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool|null */ - function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) { + public function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) { $info = $this->indexInfo( $table, $index, $fname ); if ( is_null( $info ) ) { return null; @@ -1561,7 +1546,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function tableExists( $table, $fname = __METHOD__ ) { + public function tableExists( $table, $fname = __METHOD__ ) { $table = $this->tableName( $table ); $old = $this->ignoreErrors( true ); $res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname ); @@ -1576,7 +1561,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $index * @return string */ - function fieldType( $res, $index ) { + public function fieldType( $res, $index ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } @@ -1592,7 +1577,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function indexUnique( $table, $index ) { + public function indexUnique( $table, $index ) { $indexInfo = $this->indexInfo( $table, $index ); if ( !$indexInfo ) { @@ -1608,7 +1593,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $options array * @return string */ - function makeInsertOptions( $options ) { + protected function makeInsertOptions( $options ) { return implode( ' ', $options ); } @@ -1645,7 +1630,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) { + public function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) { # No rows to insert, easy just return now if ( !count( $a ) ) { return true; @@ -1693,7 +1678,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $options Array: The options passed to DatabaseBase::update * @return string */ - function makeUpdateOptions( $options ) { + protected function makeUpdateOptions( $options ) { if ( !is_array( $options ) ) { $options = array( $options ); } @@ -1759,7 +1744,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function makeList( $a, $mode = LIST_COMMA ) { + public function makeList( $a, $mode = LIST_COMMA ) { if ( !is_array( $a ) ) { throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' ); } @@ -1819,12 +1804,12 @@ abstract class DatabaseBase implements DatabaseType { * The keys on each level may be either integers or strings. * * @param $data Array: organized as 2-d - * array(baseKeyVal => array(subKeyVal => , ...), ...) + * 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. */ - function makeWhereFrom2d( $data, $baseKey, $subKey ) { + public function makeWhereFrom2d( $data, $baseKey, $subKey ) { $conds = array(); foreach ( $data as $base => $sub ) { @@ -1844,14 +1829,22 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Bitwise operations + * Return aggregated value alias + * + * @param $valuedata + * @param $valuename string + * + * @return string */ + public function aggregateValue( $valuedata, $valuename = 'value' ) { + return $valuename; + } /** * @param $field * @return string */ - function bitNot( $field ) { + public function bitNot( $field ) { return "(~$field)"; } @@ -1860,7 +1853,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fieldRight * @return string */ - function bitAnd( $fieldLeft, $fieldRight ) { + public function bitAnd( $fieldLeft, $fieldRight ) { return "($fieldLeft & $fieldRight)"; } @@ -1869,10 +1862,19 @@ abstract class DatabaseBase implements DatabaseType { * @param $fieldRight * @return string */ - function bitOr( $fieldLeft, $fieldRight ) { + public function bitOr( $fieldLeft, $fieldRight ) { return "($fieldLeft | $fieldRight)"; } + /** + * Build a concatenation list to feed into a SQL query + * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting + * @return String + */ + public function buildConcat( $stringList ) { + return 'CONCAT(' . implode( ',', $stringList ) . ')'; + } + /** * Change the current database * @@ -1882,7 +1884,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool Success or failure */ - function selectDB( $db ) { + public function selectDB( $db ) { # 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. @@ -1893,14 +1895,14 @@ abstract class DatabaseBase implements DatabaseType { /** * Get the current DB name */ - function getDBname() { + public function getDBname() { return $this->mDBname; } /** * Get the server hostname or IP address */ - function getServer() { + public function getServer() { return $this->mServer; } @@ -1921,7 +1923,7 @@ abstract class DatabaseBase implements DatabaseType { * raw - Do not add identifier quotes to the table name * @return String: full database name */ - function tableName( $name, $format = 'quoted' ) { + public function tableName( $name, $format = 'quoted' ) { 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 @@ -2066,6 +2068,39 @@ abstract class DatabaseBase implements DatabaseType { return $retval; } + /** + * Get an aliased field name + * e.g. fieldName AS newFieldName + * + * @param $name string Field name + * @param $alias string|bool Alias (optional) + * @return string SQL name for aliased field. Will not alias a field to its own name + */ + public function fieldNameWithAlias( $name, $alias = false ) { + if ( !$alias || (string)$alias === (string)$name ) { + return $name; + } else { + return $name . ' AS ' . $alias; //PostgreSQL needs AS + } + } + + /** + * Gets an array of aliased field names + * + * @param $fields array( [alias] => field ) + * @return array of strings, see fieldNameWithAlias() + */ + public function fieldNamesWithAlias( $fields ) { + $retval = array(); + foreach ( $fields as $alias => $field ) { + if ( is_numeric( $alias ) ) { + $alias = $field; + } + $retval[] = $this->fieldNameWithAlias( $field, $alias ); + } + return $retval; + } + /** * Get the aliased table name clause for a FROM clause * which might have a JOIN and/or USE INDEX clause @@ -2134,7 +2169,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function indexName( $index ) { + protected function indexName( $index ) { // Backwards-compatibility hack $renamed = array( 'ar_usertext_timestamp' => 'usertext_timestamp', @@ -2157,7 +2192,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function addQuotes( $s ) { + public function addQuotes( $s ) { if ( $s === null ) { return 'NULL'; } else { @@ -2195,36 +2230,6 @@ abstract class DatabaseBase implements DatabaseType { 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 since 1.18 use addIdentifierQuotes - * - * @param $s string - * - * @return string - */ - function quote_ident( $s ) { - wfDeprecated( __METHOD__, '1.18' ); - return $this->addIdentifierQuotes( $s ); - } - - /** - * Escape string for safe LIKE usage. - * WARNING: you should almost never use this function directly, - * instead use buildLike() that escapes everything automatically - * @deprecated since 1.17, warnings in 1.17, removed in ??? - * - * @param $s string - * - * @return string - */ - public function escapeLike( $s ) { - wfDeprecated( __METHOD__, '1.17' ); - return $this->escapeLikeInternal( $s ); - } - /** * @param $s string * @return string @@ -2249,7 +2254,7 @@ abstract class DatabaseBase implements DatabaseType { * @since 1.16 * @return String: fully built LIKE statement */ - function buildLike() { + public function buildLike() { $params = func_get_args(); if ( count( $params ) > 0 && is_array( $params[0] ) ) { @@ -2274,7 +2279,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return LikeMatch */ - function anyChar() { + public function anyChar() { return new LikeMatch( '_' ); } @@ -2283,7 +2288,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return LikeMatch */ - function anyString() { + public function anyString() { return new LikeMatch( '%' ); } @@ -2298,7 +2303,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $seqName string * @return null */ - function nextSequenceValue( $seqName ) { + public function nextSequenceValue( $seqName ) { return null; } @@ -2312,7 +2317,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $index * @return string */ - function useIndexClause( $index ) { + public function useIndexClause( $index ) { return ''; } @@ -2338,7 +2343,7 @@ abstract class DatabaseBase implements DatabaseType { * a field name or an array of field names * @param $fname String: Calling function name (use __METHOD__) for logs/profiling */ - function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) { + public function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) { $quotedTable = $this->tableName( $table ); if ( count( $rows ) == 0 ) { @@ -2439,7 +2444,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String: Calling function name (use __METHOD__) for * logs/profiling */ - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, + public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) { if ( !$conds ) { @@ -2466,7 +2471,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return int */ - function textFieldSize( $table, $field ) { + public function textFieldSize( $table, $field ) { $table = $this->tableName( $table ); $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";"; $res = $this->query( $sql, 'DatabaseBase::textFieldSize' ); @@ -2491,7 +2496,7 @@ abstract class DatabaseBase implements DatabaseType { * @return string Returns the text of the low priority option if it is * supported, or a blank string otherwise */ - function lowPriorityOption() { + public function lowPriorityOption() { return ''; } @@ -2505,7 +2510,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) { + public function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) { if ( !$conds ) { throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' ); } @@ -2546,7 +2551,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return ResultWrapper */ - function insertSelect( $destTable, $srcTable, $varMap, $conds, + public function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseBase::insertSelect', $insertOptions = array(), $selectOptions = array() ) { @@ -2592,35 +2597,24 @@ abstract class DatabaseBase implements DatabaseType { * If the result of the query is not ordered, then the rows to be returned * are theoretically arbitrary. * - * $sql is expected to be a SELECT, if that makes a difference. For - * UPDATE, limitResultForUpdate should be used. + * $sql is expected to be a SELECT, if that makes a difference. * * The version provided by default works in MySQL and SQLite. It will very * likely need to be overridden for most other DBMSes. * * @param $sql String SQL query we will append the limit too * @param $limit Integer the SQL limit - * @param $offset Integer|false the SQL offset (default false) + * @param $offset Integer|bool the SQL offset (default false) * * @return string */ - function limitResult( $sql, $limit, $offset = false ) { + public function limitResult( $sql, $limit, $offset = false ) { if ( !is_numeric( $limit ) ) { throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" ); } - return "$sql LIMIT " - . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" ) - . "{$limit} "; - } - - /** - * @param $sql - * @param $num - * @return string - */ - function limitResultForUpdate( $sql, $num ) { - return $this->limitResult( $sql, $num, 0 ); + . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" ) + . "{$limit} "; } /** @@ -2628,7 +2622,7 @@ abstract class DatabaseBase implements DatabaseType { * within the UNION construct. * @return Boolean */ - function unionSupportsOrderAndLimit() { + public function unionSupportsOrderAndLimit() { return true; // True for almost every DB supported } @@ -2640,7 +2634,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $all Boolean: use UNION ALL * @return String: SQL fragment */ - function unionQueries( $sqls, $all ) { + public function unionQueries( $sqls, $all ) { $glue = $all ? ') UNION ALL (' : ') UNION ('; return '(' . implode( $glue, $sqls ) . ')'; } @@ -2649,12 +2643,15 @@ abstract class DatabaseBase implements DatabaseType { * Returns an SQL expression for a simple conditional. This doesn't need * to be overridden unless CASE isn't supported in your DBMS. * - * @param $cond String: SQL expression which will result in a boolean value + * @param $cond string|array SQL expression which will result in a boolean value * @param $trueVal String: SQL expression to return if true * @param $falseVal String: SQL expression to return if false * @return String: SQL fragment */ - function conditional( $cond, $trueVal, $falseVal ) { + public function conditional( $cond, $trueVal, $falseVal ) { + if ( is_array( $cond ) ) { + $cond = $this->makeList( $cond, LIST_AND ); + } return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) "; } @@ -2668,7 +2665,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function strreplace( $orig, $old, $new ) { + public function strreplace( $orig, $old, $new ) { return "REPLACE({$orig}, {$old}, {$new})"; } @@ -2678,7 +2675,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return int */ - function getServerUptime() { + public function getServerUptime() { return 0; } @@ -2688,7 +2685,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function wasDeadlock() { + public function wasDeadlock() { return false; } @@ -2698,7 +2695,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function wasLockTimeout() { + public function wasLockTimeout() { return false; } @@ -2709,7 +2706,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function wasErrorReissuable() { + public function wasErrorReissuable() { return false; } @@ -2719,7 +2716,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function wasReadOnlyError() { + public function wasReadOnlyError() { return false; } @@ -2741,8 +2738,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool */ - function deadlockLoop() { - + public function deadlockLoop() { $this->begin( __METHOD__ ); $args = func_get_args(); $function = array_shift( $args ); @@ -2790,11 +2786,11 @@ abstract class DatabaseBase implements DatabaseType { * @param $timeout Integer: the maximum number of seconds to wait for * synchronisation * - * @return An integer: zero if the slave was past that position already, + * @return 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( DBMasterPos $pos, $timeout ) { + public function masterPosWait( DBMasterPos $pos, $timeout ) { wfProfileIn( __METHOD__ ); if ( !is_null( $this->mFakeSlaveLag ) ) { @@ -2827,7 +2823,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return DBMasterPos, or false if this is not a slave. */ - function getSlavePos() { + public function getSlavePos() { if ( !is_null( $this->mFakeSlaveLag ) ) { $pos = new MySQLMasterPos( 'fake', microtime( true ) - $this->mFakeSlaveLag ); wfDebug( __METHOD__ . ": fake slave pos = $pos\n" ); @@ -2843,7 +2839,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return DBMasterPos, or false if this is not a master */ - function getMasterPos() { + public function getMasterPos() { if ( $this->mFakeMaster ) { return new MySQLMasterPos( 'fake', microtime( true ) ); } else { @@ -2852,11 +2848,65 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Begin a transaction, committing any previously open transaction + * Run an anonymous function as soon as there is no transaction pending. + * If there is a transaction and it is rolled back, then the callback is cancelled. + * Callbacks must commit any transactions that they begin. + * + * This is useful for updates to different systems or separate transactions are needed. + * + * @param Closure $callback + * @return void + */ + final public function onTransactionIdle( Closure $callback ) { + if ( $this->mTrxLevel ) { + $this->mTrxIdleCallbacks[] = $callback; + } else { + $callback(); + } + } + + /** + * Actually run the "on transaction idle" callbacks + */ + protected function runOnTransactionIdleCallbacks() { + $autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled? + + $e = null; // last exception + do { // callbacks may add callbacks :) + $callbacks = $this->mTrxIdleCallbacks; + $this->mTrxIdleCallbacks = array(); // recursion guard + foreach ( $callbacks as $callback ) { + try { + $this->clearFlag( DBO_TRX ); // make each query its own transaction + $callback(); + $this->setFlag( $autoTrx ? DBO_TRX : 0 ); // restore automatic begin() + } catch ( Exception $e ) {} + } + } while ( count( $this->mTrxIdleCallbacks ) ); + + if ( $e instanceof Exception ) { + throw $e; // re-throw any last exception + } + } + + /** + * Begin a transaction * * @param $fname string */ - function begin( $fname = 'DatabaseBase::begin' ) { + final public function begin( $fname = 'DatabaseBase::begin' ) { + if ( $this->mTrxLevel ) { // implicit commit + $this->doCommit( $fname ); + $this->runOnTransactionIdleCallbacks(); + } + $this->doBegin( $fname ); + } + + /** + * @see DatabaseBase::begin() + * @param type $fname + */ + protected function doBegin( $fname ) { $this->query( 'BEGIN', $fname ); $this->mTrxLevel = 1; } @@ -2866,7 +2916,16 @@ abstract class DatabaseBase implements DatabaseType { * * @param $fname string */ - function commit( $fname = 'DatabaseBase::commit' ) { + final public function commit( $fname = 'DatabaseBase::commit' ) { + $this->doCommit( $fname ); + $this->runOnTransactionIdleCallbacks(); + } + + /** + * @see DatabaseBase::commit() + * @param type $fname + */ + protected function doCommit( $fname ) { if ( $this->mTrxLevel ) { $this->query( 'COMMIT', $fname ); $this->mTrxLevel = 0; @@ -2879,7 +2938,16 @@ abstract class DatabaseBase implements DatabaseType { * * @param $fname string */ - function rollback( $fname = 'DatabaseBase::rollback' ) { + final public function rollback( $fname = 'DatabaseBase::rollback' ) { + $this->doRollback( $fname ); + $this->mTrxIdleCallbacks = array(); // cancel + } + + /** + * @see DatabaseBase::rollback() + * @param type $fname + */ + protected function doRollback( $fname ) { if ( $this->mTrxLevel ) { $this->query( 'ROLLBACK', $fname, true ); $this->mTrxLevel = 0; @@ -2900,7 +2968,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $fname String: calling function name * @return Boolean: true if operation was successful */ - function duplicateTableStructure( $oldName, $newName, $temporary = false, + public function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseBase::duplicateTableStructure' ) { throw new MWException( @@ -2910,7 +2978,7 @@ abstract class DatabaseBase implements DatabaseType { /** * List all tables on the database * - * @param $prefix Only show tables with this prefix, e.g. mw_ + * @param $prefix string Only show tables with this prefix, e.g. mw_ * @param $fname String: calling function name */ function listTables( $prefix = null, $fname = 'DatabaseBase::listTables' ) { @@ -2928,7 +2996,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function timestamp( $ts = 0 ) { + public function timestamp( $ts = 0 ) { return wfTimestamp( TS_MW, $ts ); } @@ -2945,7 +3013,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return string */ - function timestampOrNull( $ts = null ) { + public function timestampOrNull( $ts = null ) { if ( is_null( $ts ) ) { return null; } else { @@ -2968,7 +3036,7 @@ abstract class DatabaseBase implements DatabaseType { * * @return bool|ResultWrapper */ - function resultObject( $result ) { + public function resultObject( $result ) { if ( empty( $result ) ) { return false; } elseif ( $result instanceof ResultWrapper ) { @@ -2981,24 +3049,12 @@ abstract class DatabaseBase implements DatabaseType { } } - /** - * Return aggregated value alias - * - * @param $valuedata - * @param $valuename string - * - * @return string - */ - function aggregateValue ( $valuedata, $valuename = 'value' ) { - return $valuename; - } - /** * Ping the server and try to reconnect if it there is no connection * * @return bool Success or failure */ - function ping() { + public function ping() { # Stub. Not essential to override. return true; } @@ -3010,9 +3066,9 @@ abstract class DatabaseBase implements DatabaseType { * installations. Most callers should use LoadBalancer::safeGetLag() * instead. * - * @return Database replication lag in seconds + * @return int Database replication lag in seconds */ - function getLag() { + public function getLag() { return intval( $this->mFakeSlaveLag ); } @@ -3033,7 +3089,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $b string * @return string */ - function encodeBlob( $b ) { + public function encodeBlob( $b ) { return $b; } @@ -3044,22 +3100,10 @@ abstract class DatabaseBase implements DatabaseType { * @param $b string * @return string */ - function decodeBlob( $b ) { + public function decodeBlob( $b ) { return $b; } - /** - * Override database's default connection timeout - * - * @param $timeout Integer in seconds - * @return void - * @deprecated since 1.19; use setSessionOptions() - */ - public function setTimeout( $timeout ) { - wfDeprecated( __METHOD__, '1.19' ); - $this->setSessionOptions( array( 'connTimeout' => $timeout ) ); - } - /** * Override database's default behavior. $options include: * 'connTimeout' : Set the connection timeout value in seconds. @@ -3085,7 +3129,9 @@ abstract class DatabaseBase implements DatabaseType { * generated dynamically using $filename * @return bool|string */ - function sourceFile( $filename, $lineCallback = false, $resultCallback = false, $fname = false ) { + public function sourceFile( + $filename, $lineCallback = false, $resultCallback = false, $fname = false + ) { wfSuppressWarnings(); $fp = fopen( $filename, 'r' ); wfRestoreWarnings(); @@ -3135,9 +3181,9 @@ abstract class DatabaseBase implements DatabaseType { * ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at * all. If it's set to false, $GLOBALS will be used. * - * @param $vars False, or array mapping variable name to value. + * @param $vars bool|array mapping variable name to value. */ - function setSchemaVars( $vars ) { + public function setSchemaVars( $vars ) { $this->mSchemaVars = $vars; } @@ -3323,12 +3369,15 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Build a concatenation list to feed into a SQL query - * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting - * @return String + * Check to see if a named lock is available. This is non-blocking. + * + * @param $lockName String: name of lock to poll + * @param $method String: name of method calling us + * @return Boolean + * @since 1.20 */ - function buildConcat( $stringList ) { - return 'CONCAT(' . implode( ',', $stringList ) . ')'; + public function lockIsFree( $lockName, $method ) { + return true; } /** @@ -3352,7 +3401,7 @@ abstract class DatabaseBase implements DatabaseType { * @param $lockName String: Name of lock to release * @param $method String: Name of method calling us * - * @return Returns 1 if the lock was released, 0 if the lock was not established + * @return int Returns 1 if the lock was released, 0 if the lock was not established * by this thread (in which case the lock is not released), and NULL if the named * lock did not exist */ @@ -3425,17 +3474,28 @@ abstract class DatabaseBase implements DatabaseType { } /** - * Encode an expiry time + * Encode an expiry time into the DBMS dependent format * * @param $expiry String: timestamp for expiry, or the 'infinity' string * @return String */ public function encodeExpiry( $expiry ) { - if ( $expiry == '' || $expiry == $this->getInfinity() ) { - return $this->getInfinity(); - } else { - return $this->timestamp( $expiry ); - } + return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) + ? $this->getInfinity() + : $this->timestamp( $expiry ); + } + + /** + * Decode an expiry time into a DBMS independent format + * + * @param $expiry String: DB timestamp field value for expiry + * @param $format integer: TS_* constant, defaults to TS_MW + * @return String + */ + public function decodeExpiry( $expiry, $format = TS_MW ) { + return ( $expiry == '' || $expiry == $this->getInfinity() ) + ? 'infinity' + : wfTimestamp( $format, $expiry ); } /** @@ -3450,4 +3510,17 @@ abstract class DatabaseBase implements DatabaseType { public function setBigSelects( $value = true ) { // no-op } + + /** + * @since 1.19 + */ + public function __toString() { + return (string)$this->mConn; + } + + public function __destruct() { + if ( count( $this->mTrxIdleCallbacks ) ) { // sanity + trigger_error( "Transaction idle callbacks still pending." ); + } + } } diff --git a/includes/db/DatabaseError.php b/includes/db/DatabaseError.php index 836d7814..a53a6747 100644 --- a/includes/db/DatabaseError.php +++ b/includes/db/DatabaseError.php @@ -1,4 +1,25 @@ 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.' ) ); @@ -197,7 +218,7 @@ class DBConnectionError extends DBError { $search = htmlspecialchars( $wgRequest->getVal( 'search' ) ); - $server = htmlspecialchars( $wgServer ); + $server = htmlspecialchars( $wgCanonicalServer ); $sitename = htmlspecialchars( $wgSitename ); $trygoogle = <<fname; $error = $this->error; } - return wfMsg( $msg, $sql, $fname, $this->errno, $error ); + return wfMessage( $msg )->rawParams( $sql, $fname, $this->errno, $error )->text(); } else { return parent::getContentMessage( $html ); } diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php index fed3b12e..f1f6dfca 100644 --- a/includes/db/DatabaseIbm_db2.php +++ b/includes/db/DatabaseIbm_db2.php @@ -2,7 +2,22 @@ /** * This is the IBM DB2 database abstraction layer. * See maintenance/ibm_db2/README for development notes - * and other specific information + * and other specific information. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html * * @file * @ingroup Database @@ -122,7 +137,7 @@ class IBM_DB2Result{ /** * Construct and initialize a wrapper for DB2 query results - * @param $db Database + * @param $db DatabaseBase * @param $result Object * @param $num_rows Integer * @param $sql String @@ -130,21 +145,21 @@ class IBM_DB2Result{ */ public function __construct( $db, $result, $num_rows, $sql, $columns ){ $this->db = $db; - + if( $result instanceof ResultWrapper ){ $this->result = $result->result; } else{ $this->result = $result; } - + $this->num_rows = $num_rows; $this->current_pos = 0; if ( $this->num_rows > 0 ) { // Make a lower-case list of the column names // By default, DB2 column names are capitalized // while MySQL column names are lowercase - + // Is there a reasonable maximum value for $i? // Setting to 2048 to prevent an infinite loop for( $i = 0; $i < 2048; $i++ ) { @@ -155,11 +170,11 @@ class IBM_DB2Result{ else { return false; } - + $this->columns[$i] = strtolower( $name ); } } - + $this->sql = $sql; } @@ -187,14 +202,14 @@ class IBM_DB2Result{ * @return mixed Object on success, false on failure. */ public function fetchObject() { - if ( $this->result - && $this->num_rows > 0 - && $this->current_pos >= 0 - && $this->current_pos < $this->num_rows ) + if ( $this->result + && $this->num_rows > 0 + && $this->current_pos >= 0 + && $this->current_pos < $this->num_rows ) { $row = $this->fetchRow(); $ret = new stdClass(); - + foreach ( $row as $k => $v ) { $lc = $this->columns[$k]; $ret->$lc = $v; @@ -210,9 +225,9 @@ class IBM_DB2Result{ * @throws DBUnexpectedError */ public function fetchRow(){ - if ( $this->result - && $this->num_rows > 0 - && $this->current_pos >= 0 + if ( $this->result + && $this->num_rows > 0 + && $this->current_pos >= 0 && $this->current_pos < $this->num_rows ) { if ( $this->loadedLines <= $this->current_pos ) { @@ -227,7 +242,7 @@ class IBM_DB2Result{ if ( $this->loadedLines > $this->current_pos ){ return $this->resultSet[$this->current_pos++]; } - + } return false; } @@ -313,6 +328,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database supports (and uses) cascading deletes + * @return bool */ function cascadingDeletes() { return true; @@ -321,6 +337,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database supports (and uses) triggers (e.g. on the * page table) + * @return bool */ function cleanupTriggers() { return true; @@ -330,6 +347,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Returns true if this database is strict about what can be put into an * IP field. * Specifically, it uses a NULL value instead of an empty string. + * @return bool */ function strictIPs() { return true; @@ -337,13 +355,15 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database uses timestamps rather than integers - */ + * @return bool + */ function realTimestamps() { return true; } /** * Returns true if this database does an implicit sort when doing GROUP BY + * @return bool */ function implicitGroupby() { return false; @@ -353,6 +373,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Returns true if this database does an implicit order by when the column * has an index * For example: SELECT page_title FROM page LIMIT 1 + * @return bool */ function implicitOrderby() { return false; @@ -361,6 +382,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database can do a native search on IP columns * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32'; + * @return bool */ function searchableIPs() { return true; @@ -368,6 +390,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns true if this database can use functional indexes + * @return bool */ function functionalIndexes() { return true; @@ -375,6 +398,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Returns a unique string representing the wiki on the server + * @return string */ public function getWikiID() { if( $this->mSchema ) { @@ -392,7 +416,7 @@ class DatabaseIbm_db2 extends DatabaseBase { return 'ibm_db2'; } - /** + /** * Returns the database connection object * @return Object */ @@ -472,7 +496,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * @param $user String * @param $password String * @param $dbName String: database name - * @return a fresh connection + * @return DatabaseBase a fresh connection */ public function open( $server, $user, $password, $dbName ) { wfProfileIn( __METHOD__ ); @@ -546,32 +570,26 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Closes a database connection, if it is open * Returns success, true if already closed + * @return bool */ - public function close() { - $this->mOpened = false; - if ( $this->mConn ) { - if ( $this->trxLevel() > 0 ) { - $this->commit(); - } - return db2_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return db2_close( $this->mConn ); } /** * Retrieves the most current database error * Forces a database rollback + * @return bool|string */ public function lastError() { $connerr = db2_conn_errormsg(); if ( $connerr ) { - //$this->rollback(); + //$this->rollback( __METHOD__ ); return $connerr; } $stmterr = db2_stmt_errormsg(); if ( $stmterr ) { - //$this->rollback(); + //$this->rollback( __METHOD__ ); return $stmterr; } @@ -667,7 +685,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Fields can be retrieved with $row->fieldname, with fields acting like * member variables. * - * @param $res SQL result object as returned from Database::query(), etc. + * @param $res array|ResultWrapper SQL result object as returned from Database::query(), etc. * @return DB2 row object * @throws DBUnexpectedError Thrown if the database returns an error */ @@ -689,8 +707,8 @@ class DatabaseIbm_db2 extends DatabaseBase { * 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 Database::query(), etc. - * @return DB2 row object + * @param $res array|ResultWrapper SQL result object as returned from Database::query(), etc. + * @return ResultWrapper row object * @throws DBUnexpectedError Thrown if the database returns an error */ public function fetchRow( $res ) { @@ -715,7 +733,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Doesn't escape numbers * * @param $s String: string to escape - * @return escaped string + * @return string escaped string */ public function addQuotes( $s ) { //$this->installPrint( "DB2::addQuotes( $s )\n" ); @@ -758,7 +776,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Alias for addQuotes() * @param $s String: string to escape - * @return escaped string + * @return string escaped string */ public function strencode( $s ) { // Bloody useless function @@ -780,16 +798,16 @@ class DatabaseIbm_db2 extends DatabaseBase { protected function applySchema() { if ( !( $this->mSchemaSet ) ) { $this->mSchemaSet = true; - $this->begin(); + $this->begin( __METHOD__ ); $this->doQuery( "SET SCHEMA = $this->mSchema" ); - $this->commit(); + $this->commit( __METHOD__ ); } } /** * Start a transaction (mandatory) */ - public function begin( $fname = 'DatabaseIbm_db2::begin' ) { + protected function doBegin( $fname = 'DatabaseIbm_db2::begin' ) { // BEGIN is implicit for DB2 // However, it requires that AutoCommit be off. @@ -805,7 +823,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * End a transaction * Must have a preceding begin() */ - public function commit( $fname = 'DatabaseIbm_db2::commit' ) { + protected function doCommit( $fname = 'DatabaseIbm_db2::commit' ) { db2_commit( $this->mConn ); // Some MediaWiki code is still transaction-less (?). @@ -819,7 +837,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Cancel a transaction */ - public function rollback( $fname = 'DatabaseIbm_db2::rollback' ) { + protected function doRollback( $fname = 'DatabaseIbm_db2::rollback' ) { db2_rollback( $this->mConn ); // turn auto-commit back on // not sure if this is appropriate @@ -836,6 +854,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * LIST_SET - comma separated with field names, like a SET clause * LIST_NAMES - comma separated field names * LIST_SET_PREPARED - like LIST_SET, except with ? tokens as values + * @return string */ function makeList( $a, $mode = LIST_COMMA ) { if ( !is_array( $a ) ) { @@ -873,6 +892,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * @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 */ public function limitResult( $sql, $limit, $offset=false ) { if( !is_numeric( $limit ) ) { @@ -904,7 +924,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Generates a timestamp in an insertable format * - * @param $ts timestamp + * @param $ts string timestamp * @return String: timestamp value */ public function timestamp( $ts = 0 ) { @@ -915,7 +935,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * Return the next in a sequence, save the value for retrieval via insertId() * @param $seqName String: name of a defined sequence in the database - * @return next value in that sequence + * @return int next value in that sequence */ public function nextSequenceValue( $seqName ) { // Not using sequences in the primary schema to allow for easier migration @@ -934,7 +954,7 @@ class DatabaseIbm_db2 extends DatabaseBase { /** * This must be called after nextSequenceVal - * @return Last sequence value used as a primary key + * @return int Last sequence value used as a primary key */ public function insertId() { return $this->mInsertId; @@ -1003,7 +1023,7 @@ class DatabaseIbm_db2 extends DatabaseBase { $res = true; // If we are not in a transaction, we need to be for savepoint trickery if ( !$this->mTrxLevel ) { - $this->begin(); + $this->begin( __METHOD__ ); } $sql = "INSERT INTO $table ( " . implode( ',', $keys ) . ' ) VALUES '; @@ -1018,7 +1038,7 @@ class DatabaseIbm_db2 extends DatabaseBase { $stmt = $this->prepare( $sql ); // start a transaction/enter transaction mode - $this->begin(); + $this->begin( __METHOD__ ); if ( !$ignore ) { //$first = true; @@ -1071,7 +1091,7 @@ class DatabaseIbm_db2 extends DatabaseBase { $this->mAffectedRows = $numrowsinserted; } // commit either way - $this->commit(); + $this->commit( __METHOD__ ); $this->freePrepared( $stmt ); return $res; @@ -1121,11 +1141,11 @@ class DatabaseIbm_db2 extends DatabaseBase { * UPDATE wrapper, takes a condition array and a SET array * * @param $table String: The table to UPDATE - * @param $values An array of values to SET - * @param $conds An array of conditions ( WHERE ). Use '*' to update all rows. + * @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 An array of UPDATE options, can be one or + * @param $options array An array of UPDATE options, can be one or * more of IGNORE, LOW_PRIORITY * @return Boolean */ @@ -1153,6 +1173,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * DELETE query wrapper * * Use $conds == "*" to delete all rows + * @return bool|\ResultWrapper */ public function delete( $table, $conds, $fname = 'DatabaseIbm_db2::delete' ) { if ( !$conds ) { @@ -1206,7 +1227,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * Moves the row pointer of the result set * @param $res Object: result set * @param $row Integer: row number - * @return success or failure + * @return bool success or failure */ public function dataSeek( $res, $row ) { if ( $res instanceof ResultWrapper ) { @@ -1279,11 +1300,11 @@ class DatabaseIbm_db2 extends DatabaseBase { * @param $conds Array or string, condition(s) for WHERE * @param $fname String: calling function name (use __METHOD__) * for logs/profiling - * @param $options Associative array of options + * @param $options array Associative array of options * (e.g. array( 'GROUP BY' => 'page_title' )), * see Database::makeSelectOptions code for list of * supported stuff - * @param $join_conds Associative array of table join conditions (optional) + * @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 for fetch functions or false @@ -1320,10 +1341,10 @@ class DatabaseIbm_db2 extends DatabaseBase { $res2 = parent::select( $table, $vars2, $conds, $fname, $options2, $join_conds ); - + $obj = $this->fetchObject( $res2 ); $this->mNumRows = $obj->num_rows; - + return new ResultWrapper( $this, new IBM_DB2Result( $this, $res, $obj->num_rows, $vars, $sql ) ); } @@ -1333,7 +1354,7 @@ class DatabaseIbm_db2 extends DatabaseBase { * * @private * - * @param $options Associative array of options to be turned into + * @param $options array Associative array of options to be turned into * an SQL query, valid keys are listed in the function. * @return Array */ @@ -1412,7 +1433,7 @@ class DatabaseIbm_db2 extends DatabaseBase { // db2_ping() doesn't exist // Emulate $this->close(); - $this->mConn = $this->openUncataloged( $this->mDBName, $this->mUser, + $this->openUncataloged( $this->mDBName, $this->mUser, $this->mPassword, $this->mServer, $this->mPort ); return false; @@ -1420,14 +1441,6 @@ class DatabaseIbm_db2 extends DatabaseBase { ###################################### # Unimplemented and not applicable ###################################### - /** - * Not implemented - * @return string $sql - */ - public function limitResultForUpdate( $sql, $num ) { - $this->installPrint( 'Not implemented for DB2: limitResultForUpdate()' ); - return $sql; - } /** * Only useful with fake prepare like in base Database class @@ -1502,7 +1515,7 @@ SQL; * Verifies that an index was created as unique * @param $table String: table name * @param $index String: index name - * @param $fname function name for profiling + * @param $fname string function name for profiling * @return Bool */ public function indexUnique ( $table, $index, @@ -1635,25 +1648,6 @@ SQL; return $res; } - /** - * Prepare & execute an SQL statement, quoting and inserting arguments - * in the appropriate places. - * @param $query String - * @param $args ... - */ - public function safeQuery( $query, $args = null ) { - // copied verbatim from Database.php - $prepared = $this->prepare( $query, 'DB2::safeQuery' ); - if( !is_array( $args ) ) { - # Pull the var args - $args = func_get_args(); - array_shift( $args ); - } - $retval = $this->execute( $prepared, $args ); - $this->freePrepared( $prepared ); - return $retval; - } - /** * For faking prepared SQL statements on DBs that don't support * it directly. @@ -1674,6 +1668,7 @@ SQL; /** * Switches module between regular and install modes + * @return string */ public function setMode( $mode ) { $old = $this->mMode; diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 38be4cbb..914ab408 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -2,6 +2,21 @@ /** * This is the MS SQL Server Native database abstraction layer. * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file * @ingroup Database * @author Joel Penner @@ -46,6 +61,7 @@ class DatabaseMssql extends DatabaseBase { /** * Usually aborts on failure + * @return bool|DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { # Test for driver support, to avoid suppressed fatal error @@ -107,14 +123,10 @@ class DatabaseMssql extends DatabaseBase { /** * Closes a database connection, if it is open * Returns success, true if already closed + * @return bool */ - function close() { - $this->mOpened = false; - if ( $this->mConn ) { - return sqlsrv_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return sqlsrv_close( $this->mConn ); } protected function doQuery( $sql ) { @@ -226,6 +238,7 @@ class DatabaseMssql extends DatabaseBase { /** * This must be called after nextSequenceVal + * @return null */ function insertId() { return $this->mInsertId; @@ -310,6 +323,7 @@ class DatabaseMssql extends DatabaseBase { * This is not necessarily an accurate estimate, so use sparingly * Returns -1 if count cannot be found * Takes same arguments as Database::select() + * @return int */ function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) { $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx @@ -326,6 +340,7 @@ class DatabaseMssql extends DatabaseBase { /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure + * @return array|bool|null */ function indexInfo( $table, $index, $fname = 'DatabaseMssql::indexExists' ) { # This does not return the same info as MYSQL would, but that's OK because MediaWiki never uses the @@ -365,6 +380,7 @@ class DatabaseMssql extends DatabaseBase { * * Usually aborts on failure * If errors are explicitly ignored, returns success + * @return bool */ function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) { # No rows to insert, easy just return now @@ -494,6 +510,7 @@ class DatabaseMssql extends DatabaseBase { * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() * $conds may be "*" to copy the whole table * srcTable may be an array of tables. + * @return null|\ResultWrapper */ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect', $insertOptions = array(), $selectOptions = array() ) { @@ -511,6 +528,7 @@ class DatabaseMssql extends DatabaseBase { /** * Return the next in a sequence, save the value for retrieval via insertId() + * @return */ function nextSequenceValue( $seqName ) { if ( !$this->tableExists( 'sequence_' . $seqName ) ) { @@ -527,6 +545,7 @@ class DatabaseMssql extends DatabaseBase { /** * Return the current value of a sequence. Assumes it has ben nextval'ed in this session. + * @return */ function currentSequenceValue( $seqName ) { $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" ); @@ -559,6 +578,7 @@ class DatabaseMssql extends DatabaseBase { * $sql string SQL query we will append the limit too * $limit integer the SQL limit * $offset integer the SQL offset (default false) + * @return mixed|string */ function limitResult( $sql, $limit, $offset = false ) { if ( $offset === false || $offset == 0 ) { @@ -600,14 +620,6 @@ class DatabaseMssql extends DatabaseBase { return $sql; } - // MSSQL does support this, but documentation is too thin to make a generalized - // function for this. Apparently UPDATE TOP (N) works, but the sort order - // may not be what we're expecting so the top n results may be a random selection. - // TODO: Implement properly. - function limitResultForUpdate( $sql, $num ) { - return $sql; - } - function timestamp( $ts = 0 ) { return wfTimestamp( TS_ISO_8601, $ts ); } @@ -647,6 +659,7 @@ class DatabaseMssql extends DatabaseBase { /** * Query whether a given column exists in the mediawiki schema + * @return bool */ function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) { $table = $this->tableName( $table ); @@ -681,7 +694,7 @@ class DatabaseMssql extends DatabaseBase { /** * Begin a transaction, committing any previously open transaction */ - function begin( $fname = 'DatabaseMssql::begin' ) { + protected function doBegin( $fname = 'DatabaseMssql::begin' ) { sqlsrv_begin_transaction( $this->mConn ); $this->mTrxLevel = 1; } @@ -689,7 +702,7 @@ class DatabaseMssql extends DatabaseBase { /** * End a transaction */ - function commit( $fname = 'DatabaseMssql::commit' ) { + protected function doCommit( $fname = 'DatabaseMssql::commit' ) { sqlsrv_commit( $this->mConn ); $this->mTrxLevel = 0; } @@ -698,7 +711,7 @@ class DatabaseMssql extends DatabaseBase { * Rollback a transaction. * No-op on non-transactional databases. */ - function rollback( $fname = 'DatabaseMssql::rollback' ) { + protected function doRollback( $fname = 'DatabaseMssql::rollback' ) { sqlsrv_rollback( $this->mConn ); $this->mTrxLevel = 0; } @@ -707,6 +720,7 @@ class DatabaseMssql extends DatabaseBase { * Escapes a identifier for use inm SQL. * Throws an exception if it is invalid. * Reference: http://msdn.microsoft.com/en-us/library/aa224033%28v=SQL.80%29.aspx + * @return string */ private function escapeIdentifier( $identifier ) { if ( strlen( $identifier ) == 0 ) { @@ -795,6 +809,7 @@ class DatabaseMssql extends DatabaseBase { /** * @private + * @return string */ function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) { $ret = array(); @@ -893,6 +908,7 @@ class DatabaseMssql extends DatabaseBase { /** * Get the type of the DBMS, as it appears in $wgDBtype. + * @return string */ function getType(){ return 'mssql'; @@ -909,6 +925,7 @@ class DatabaseMssql extends DatabaseBase { /** * Since MSSQL doesn't recognize the infinity keyword, set date manually. * @todo Remove magic date + * @return string */ public function getInfinity() { return '3000-01-31 00:00:00.000'; diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php index c179b724..7f389da9 100644 --- a/includes/db/DatabaseMysql.php +++ b/includes/db/DatabaseMysql.php @@ -2,6 +2,21 @@ /** * This is the MySQL database abstraction layer. * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file * @ingroup Database */ @@ -44,7 +59,7 @@ class DatabaseMysql extends DatabaseBase { * @throws DBConnectionError */ function open( $server, $user, $password, $dbName ) { - global $wgAllDBsAreLocalhost; + global $wgAllDBsAreLocalhost, $wgDBmysql5, $wgSQLMode; wfProfileIn( __METHOD__ ); # Load mysql.so if we don't have it @@ -68,7 +83,15 @@ class DatabaseMysql extends DatabaseBase { $this->mPassword = $password; $this->mDBname = $dbName; - wfProfileIn("dbconnect-$server"); + $connFlags = 0; + if ( $this->mFlags & DBO_SSL ) { + $connFlags |= MYSQL_CLIENT_SSL; + } + if ( $this->mFlags & DBO_COMPRESS ) { + $connFlags |= MYSQL_CLIENT_COMPRESS; + } + + wfProfileIn( "dbconnect-$server" ); # The kernel's default SYN retransmission period is far too slow for us, # so we use a short timeout plus a manual retry. Retrying means that a small @@ -85,85 +108,71 @@ class DatabaseMysql extends DatabaseBase { usleep( 1000 ); } if ( $this->mFlags & DBO_PERSISTENT ) { - $this->mConn = mysql_pconnect( $realServer, $user, $password ); + $this->mConn = mysql_pconnect( $realServer, $user, $password, $connFlags ); } else { # Create a new connection... - $this->mConn = mysql_connect( $realServer, $user, $password, true ); + $this->mConn = mysql_connect( $realServer, $user, $password, true, $connFlags ); } #if ( $this->mConn === false ) { #$iplus = $i + 1; #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); #} } - $phpError = $this->restoreErrorHandler(); + $error = $this->restoreErrorHandler(); + + wfProfileOut( "dbconnect-$server" ); + # Always log connection errors if ( !$this->mConn ) { - $error = $this->lastError(); if ( !$error ) { - $error = $phpError; + $error = $this->lastError(); } wfLogDBError( "Error connecting to {$this->mServer}: $error\n" ); - wfDebug( "DB connection error\n" ); - wfDebug( "Server: $server, User: $user, Password: " . - substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" ); - } + wfDebug( "DB connection error\n" . + "Server: $server, User: $user, Password: " . + substr( $password, 0, 3 ) . "..., error: " . $error . "\n" ); - wfProfileOut("dbconnect-$server"); + wfProfileOut( __METHOD__ ); + $this->reportConnectionError( $error ); + } - if ( $dbName != '' && $this->mConn !== false ) { + if ( $dbName != '' ) { 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"; - wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n"); - wfDebug( $error ); - } - } else { - # Delay USE query - $success = (bool)$this->mConn; - } + wfLogDBError( "Error selecting database $dbName on server {$this->mServer}\n" ); + wfDebug( "Error selecting database $dbName on server {$this->mServer} " . + "from client host " . wfHostname() . "\n" ); - if ( $success ) { - // Tell the server we're communicating with it in UTF-8. - // This may engage various charset conversions. - global $wgDBmysql5; - if( $wgDBmysql5 ) { - $this->query( 'SET NAMES utf8', __METHOD__ ); - } else { - $this->query( 'SET NAMES binary', __METHOD__ ); - } - // Set SQL mode, default is turning them all off, can be overridden or skipped with null - global $wgSQLMode; - if ( is_string( $wgSQLMode ) ) { - $mode = $this->addQuotes( $wgSQLMode ); - $this->query( "SET sql_mode = $mode", __METHOD__ ); + wfProfileOut( __METHOD__ ); + $this->reportConnectionError( "Error selecting database $dbName" ); } + } - // Turn off strict mode if it is on + // Tell the server we're communicating with it in UTF-8. + // This may engage various charset conversions. + if( $wgDBmysql5 ) { + $this->query( 'SET NAMES utf8', __METHOD__ ); } else { - $this->reportConnectionError( $phpError ); + $this->query( 'SET NAMES binary', __METHOD__ ); + } + // Set SQL mode, default is turning them all off, can be overridden or skipped with null + if ( is_string( $wgSQLMode ) ) { + $mode = $this->addQuotes( $wgSQLMode ); + $this->query( "SET sql_mode = $mode", __METHOD__ ); } - $this->mOpened = $success; + $this->mOpened = true; wfProfileOut( __METHOD__ ); - return $success; + return true; } /** * @return bool */ - function close() { - $this->mOpened = false; - if ( $this->mConn ) { - if ( $this->trxLevel() ) { - $this->commit(); - } - return mysql_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return mysql_close( $this->mConn ); } /** @@ -194,7 +203,13 @@ class DatabaseMysql extends DatabaseBase { wfSuppressWarnings(); $row = mysql_fetch_object( $res ); wfRestoreWarnings(); - if( $this->lastErrno() ) { + + $errno = $this->lastErrno(); + // Unfortunately, mysql_fetch_object does not reset the last errno. + // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as + // these are the only errors mysql_fetch_object can cause. + // See http://dev.mysql.com/doc/refman/5.0/es/mysql-fetch-row.html. + if( $errno == 2000 || $errno == 2013 ) { throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); } return $row; @@ -212,7 +227,13 @@ class DatabaseMysql extends DatabaseBase { wfSuppressWarnings(); $row = mysql_fetch_array( $res ); wfRestoreWarnings(); - if ( $this->lastErrno() ) { + + $errno = $this->lastErrno(); + // Unfortunately, mysql_fetch_array does not reset the last errno. + // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as + // these are the only errors mysql_fetch_object can cause. + // See http://dev.mysql.com/doc/refman/5.0/es/mysql-fetch-row.html. + if( $errno == 2000 || $errno == 2013 ) { throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); } return $row; @@ -385,7 +406,7 @@ class DatabaseMysql extends DatabaseBase { * @param $table string * @param $index string * @param $fname string - * @return false|array + * @return bool|array|null False or null on failure */ function indexInfo( $table, $index, $fname = 'DatabaseMysql::indexInfo' ) { # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not. @@ -558,7 +579,7 @@ class DatabaseMysql extends DatabaseBase { # Commit any open transactions if ( $this->mTrxLevel ) { - $this->commit(); + $this->commit( __METHOD__ ); } if ( !is_null( $this->mFakeSlaveLag ) ) { @@ -585,7 +606,7 @@ class DatabaseMysql extends DatabaseBase { /** * Get the position of the master from SHOW SLAVE STATUS * - * @return MySQLMasterPos|false + * @return MySQLMasterPos|bool */ function getSlavePos() { if ( !is_null( $this->mFakeSlaveLag ) ) { @@ -606,7 +627,7 @@ class DatabaseMysql extends DatabaseBase { /** * Get the position of the master from SHOW MASTER STATUS * - * @return MySQLMasterPos|false + * @return MySQLMasterPos|bool */ function getMasterPos() { if ( $this->mFakeMaster ) { @@ -652,13 +673,6 @@ class DatabaseMysql extends DatabaseBase { return '[http://www.mysql.com/ MySQL]'; } - /** - * @return bool - */ - function standardSelectDistinct() { - return false; - } - /** * @param $options array */ @@ -679,6 +693,21 @@ class DatabaseMysql extends DatabaseBase { return parent::streamStatementEnd( $sql, $newLine ); } + /** + * Check to see if a named lock is available. This is non-blocking. + * + * @param $lockName String: name of lock to poll + * @param $method String: name of method calling us + * @return Boolean + * @since 1.20 + */ + public function lockIsFree( $lockName, $method ) { + $lockName = $this->addQuotes( $lockName ); + $result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method ); + $row = $this->fetchObject( $result ); + return ( $row->lockstatus == 1 ); + } + /** * @param $lockName string * @param $method string @@ -708,7 +737,7 @@ class DatabaseMysql extends DatabaseBase { $lockName = $this->addQuotes( $lockName ); $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method ); $row = $this->fetchObject( $result ); - return $row->lockstatus; + return ( $row->lockstatus == 1 ); } /** @@ -860,7 +889,7 @@ class DatabaseMysql extends DatabaseBase { /** * List all tables on the database * - * @param $prefix Only show tables with this prefix, e.g. mw_ + * @param $prefix string Only show tables with this prefix, e.g. mw_ * @param $fname String: calling function name * @return array */ diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index 855fc831..7d8884fb 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -2,6 +2,21 @@ /** * This is the Oracle database abstraction layer. * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file * @ingroup Database */ @@ -226,6 +241,7 @@ class DatabaseOracle extends DatabaseBase { /** * Usually aborts on failure + * @return DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { if ( !function_exists( 'oci_connect' ) ) { @@ -285,17 +301,10 @@ class DatabaseOracle extends DatabaseBase { /** * Closes a database connection, if it is open * Returns success, true if already closed + * @return bool */ - function close() { - $this->mOpened = false; - if ( $this->mConn ) { - if ( $this->mTrxLevel ) { - $this->commit(); - } - return oci_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return oci_close( $this->mConn ); } function execFlags() { @@ -401,6 +410,7 @@ class DatabaseOracle extends DatabaseBase { /** * This must be called after nextSequenceVal + * @return null */ function insertId() { return $this->mInsertId; @@ -439,6 +449,7 @@ class DatabaseOracle extends DatabaseBase { /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure + * @return bool */ function indexInfo( $table, $index, $fname = 'DatabaseOracle::indexExists' ) { return false; @@ -679,6 +690,7 @@ class DatabaseOracle extends DatabaseBase { } /** * Return the next in a sequence, save the value for retrieval via insertId() + * @return null */ function nextSequenceValue( $seqName ) { $res = $this->query( "SELECT $seqName.nextval FROM dual" ); @@ -689,6 +701,7 @@ class DatabaseOracle extends DatabaseBase { /** * Return sequence_name if table has a sequence + * @return bool */ private function getSequenceData( $table ) { if ( $this->sequenceData == null ) { @@ -797,7 +810,7 @@ class DatabaseOracle extends DatabaseBase { /** * Return aggregated value function call */ - function aggregateValue ( $valuedata, $valuename = 'value' ) { + public function aggregateValue( $valuedata, $valuename = 'value' ) { return $valuedata; } @@ -836,6 +849,7 @@ class DatabaseOracle extends DatabaseBase { /** * Query whether a given index exists + * @return bool */ function indexExists( $table, $index, $fname = 'DatabaseOracle::indexExists' ) { $table = $this->tableName( $table ); @@ -855,6 +869,7 @@ class DatabaseOracle extends DatabaseBase { /** * Query whether a given table exists (in the given schema, or the default mw one if not given) + * @return int */ function tableExists( $table, $fname = __METHOD__ ) { $table = $this->tableName( $table ); @@ -940,12 +955,12 @@ class DatabaseOracle extends DatabaseBase { return $this->fieldInfoMulti ($table, $field); } - function begin( $fname = 'DatabaseOracle::begin' ) { + protected function doBegin( $fname = 'DatabaseOracle::begin' ) { $this->mTrxLevel = 1; $this->doQuery( 'SET CONSTRAINTS ALL DEFERRED' ); } - function commit( $fname = 'DatabaseOracle::commit' ) { + protected function doCommit( $fname = 'DatabaseOracle::commit' ) { if ( $this->mTrxLevel ) { $ret = oci_commit( $this->mConn ); if ( !$ret ) { @@ -956,7 +971,7 @@ class DatabaseOracle extends DatabaseBase { } } - function rollback( $fname = 'DatabaseOracle::rollback' ) { + protected function doRollback( $fname = 'DatabaseOracle::rollback' ) { if ( $this->mTrxLevel ) { oci_rollback( $this->mConn ); $this->mTrxLevel = 0; @@ -964,11 +979,6 @@ class DatabaseOracle extends DatabaseBase { } } - /* Not even sure why this is used in the main codebase... */ - function limitResultForUpdate( $sql, $num ) { - return $sql; - } - /* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */ function sourceStream( $fp, $lineCallback = false, $resultCallback = false, $fname = 'DatabaseOracle::sourceStream', $inputCallback = false ) { diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 98cf3c75..457bf384 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -2,12 +2,28 @@ /** * This is the Postgres database abstraction layer. * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file * @ingroup Database */ class PostgresField implements Field { - private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname; + private $name, $tablename, $type, $nullable, $max_length, $deferred, $deferrable, $conname, + $has_default, $default; /** * @param $db DatabaseBase @@ -16,11 +32,11 @@ class PostgresField implements Field { * @return null|PostgresField */ static function fromText( $db, $table, $field ) { - global $wgDBmwschema; - $q = <<tableName( $table, 'raw' ); $res = $db->query( sprintf( $q, - $db->addQuotes( $wgDBmwschema ), + $db->addQuotes( $db->getCoreSchema() ), $db->addQuotes( $table ), $db->addQuotes( $field ) ) @@ -60,6 +77,8 @@ SQL; $n->deferrable = ( $row->deferrable == 't' ); $n->deferred = ( $row->deferred == 't' ); $n->conname = $row->conname; + $n->has_default = ( $row->atthasdef === 't' ); + $n->default = $row->adsrc; return $n; } @@ -94,7 +113,169 @@ SQL; function conname() { return $this->conname; } + /** + * @since 1.19 + */ + function defaultValue() { + if( $this->has_default ) { + return $this->default; + } else { + return false; + } + } + +} + +/** + * Used to debug transaction processing + * Only used if $wgDebugDBTransactions is true + * + * @since 1.19 + * @ingroup Database + */ +class PostgresTransactionState { + + static $WATCHED = array( + array( + "desc" => "%s: Connection state changed from %s -> %s\n", + "states" => array( + PGSQL_CONNECTION_OK => "OK", + PGSQL_CONNECTION_BAD => "BAD" + ) + ), + array( + "desc" => "%s: Transaction state changed from %s -> %s\n", + "states" => array( + PGSQL_TRANSACTION_IDLE => "IDLE", + PGSQL_TRANSACTION_ACTIVE => "ACTIVE", + PGSQL_TRANSACTION_INTRANS => "TRANS", + PGSQL_TRANSACTION_INERROR => "ERROR", + PGSQL_TRANSACTION_UNKNOWN => "UNKNOWN" + ) + ) + ); + + public function __construct( $conn ) { + $this->mConn = $conn; + $this->update(); + $this->mCurrentState = $this->mNewState; + } + + public function update() { + $this->mNewState = array( + pg_connection_status( $this->mConn ), + pg_transaction_status( $this->mConn ) + ); + } + + public function check() { + global $wgDebugDBTransactions; + $this->update(); + if ( $wgDebugDBTransactions ) { + if ( $this->mCurrentState !== $this->mNewState ) { + $old = reset( $this->mCurrentState ); + $new = reset( $this->mNewState ); + foreach ( self::$WATCHED as $watched ) { + if ($old !== $new) { + $this->log_changed($old, $new, $watched); + } + $old = next( $this->mCurrentState ); + $new = next( $this->mNewState ); + + } + } + } + $this->mCurrentState = $this->mNewState; + } + + protected function describe_changed( $status, $desc_table ) { + if( isset( $desc_table[$status] ) ) { + return $desc_table[$status]; + } else { + return "STATUS " . $status; + } + } + protected function log_changed( $old, $new, $watched ) { + wfDebug(sprintf($watched["desc"], + $this->mConn, + $this->describe_changed( $old, $watched["states"] ), + $this->describe_changed( $new, $watched["states"] )) + ); + } +} + +/** + * Manage savepoints within a transaction + * @ingroup Database + * @since 1.19 + */ +class SavepointPostgres { + /** + * Establish a savepoint within a transaction + */ + protected $dbw; + protected $id; + protected $didbegin; + + public function __construct ($dbw, $id) { + $this->dbw = $dbw; + $this->id = $id; + $this->didbegin = false; + /* If we are not in a transaction, we need to be for savepoint trickery */ + if ( !$dbw->trxLevel() ) { + $dbw->begin( "FOR SAVEPOINT" ); + $this->didbegin = true; + } + } + + public function __destruct() { + if ( $this->didbegin ) { + $this->dbw->rollback(); + } + } + + public function commit() { + if ( $this->didbegin ) { + $this->dbw->commit(); + } + } + + protected function query( $keyword, $msg_ok, $msg_failed ) { + global $wgDebugDBTransactions; + if ( $this->dbw->doQuery( $keyword . " " . $this->id ) !== false ) { + if ( $wgDebugDBTransactions ) { + wfDebug( sprintf ($msg_ok, $this->id ) ); + } + } else { + wfDebug( sprintf ($msg_failed, $this->id ) ); + } + } + + public function savepoint() { + $this->query("SAVEPOINT", + "Transaction state: savepoint \"%s\" established.\n", + "Transaction state: establishment of savepoint \"%s\" FAILED.\n" + ); + } + + public function release() { + $this->query("RELEASE", + "Transaction state: savepoint \"%s\" released.\n", + "Transaction state: release of savepoint \"%s\" FAILED.\n" + ); + } + + public function rollback() { + $this->query("ROLLBACK TO", + "Transaction state: savepoint \"%s\" rolled back.\n", + "Transaction state: rollback of savepoint \"%s\" FAILED.\n" + ); + } + + public function __toString() { + return (string)$this->id; + } } /** @@ -136,15 +317,15 @@ class DatabasePostgres extends DatabaseBase { } function hasConstraint( $name ) { - global $wgDBmwschema; $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . - pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $wgDBmwschema ) ."'"; + pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $this->getCoreSchema() ) ."'"; $res = $this->doQuery( $SQL ); return $this->numRows( $res ); } /** * Usually aborts on failure + * @return DatabaseBase|null */ function open( $server, $user, $password, $dbName ) { # Test for Postgres support, to avoid suppressed fatal error @@ -158,7 +339,6 @@ class DatabasePostgres extends DatabaseBase { return; } - $this->close(); $this->mServer = $server; $port = $wgDBport; $this->mUser = $user; @@ -176,10 +356,14 @@ class DatabasePostgres extends DatabaseBase { if ( $port != false && $port != '' ) { $connectVars['port'] = $port; } - $connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW ); + if ( $this->mFlags & DBO_SSL ) { + $connectVars['sslmode'] = 1; + } + $this->connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW ); + $this->close(); $this->installErrorHandler(); - $this->mConn = pg_connect( $connectString ); + $this->mConn = pg_connect( $this->connectString ); $phpError = $this->restoreErrorHandler(); if ( !$this->mConn ) { @@ -190,6 +374,7 @@ class DatabasePostgres extends DatabaseBase { } $this->mOpened = true; + $this->mTransactionState = new PostgresTransactionState( $this->mConn ); global $wgCommandLineMode; # If called from the command-line (e.g. importDump), only show errors @@ -203,12 +388,7 @@ class DatabasePostgres extends DatabaseBase { $this->query( "SET standard_conforming_strings = on", __METHOD__ ); global $wgDBmwschema; - if ( $this->schemaExists( $wgDBmwschema ) ) { - $safeschema = $this->addIdentifierQuotes( $wgDBmwschema ); - $this->doQuery( "SET search_path = $safeschema" ); - } else { - $this->doQuery( "SET search_path = public" ); - } + $this->determineCoreSchema( $wgDBmwschema ); return $this->mConn; } @@ -237,25 +417,62 @@ class DatabasePostgres extends DatabaseBase { /** * Closes a database connection, if it is open * Returns success, true if already closed + * @return bool */ - function close() { - $this->mOpened = false; - if ( $this->mConn ) { - return pg_close( $this->mConn ); - } else { - return true; - } + protected function closeConnection() { + return pg_close( $this->mConn ); } - protected function doQuery( $sql ) { + public function doQuery( $sql ) { if ( function_exists( 'mb_convert_encoding' ) ) { $sql = mb_convert_encoding( $sql, 'UTF-8' ); } - $this->mLastResult = pg_query( $this->mConn, $sql ); - $this->mAffectedRows = null; // use pg_affected_rows(mLastResult) + $this->mTransactionState->check(); + if( pg_send_query( $this->mConn, $sql ) === false ) { + throw new DBUnexpectedError( $this, "Unable to post new query to PostgreSQL\n" ); + } + $this->mLastResult = pg_get_result( $this->mConn ); + $this->mTransactionState->check(); + $this->mAffectedRows = null; + if ( pg_result_error( $this->mLastResult ) ) { + return false; + } return $this->mLastResult; } + protected function dumpError () { + $diags = array( PGSQL_DIAG_SEVERITY, + PGSQL_DIAG_SQLSTATE, + PGSQL_DIAG_MESSAGE_PRIMARY, + PGSQL_DIAG_MESSAGE_DETAIL, + PGSQL_DIAG_MESSAGE_HINT, + PGSQL_DIAG_STATEMENT_POSITION, + PGSQL_DIAG_INTERNAL_POSITION, + PGSQL_DIAG_INTERNAL_QUERY, + PGSQL_DIAG_CONTEXT, + PGSQL_DIAG_SOURCE_FILE, + PGSQL_DIAG_SOURCE_LINE, + PGSQL_DIAG_SOURCE_FUNCTION ); + foreach ( $diags as $d ) { + wfDebug( sprintf("PgSQL ERROR(%d): %s\n", $d, pg_result_error_field( $this->mLastResult, $d ) ) ); + } + } + + function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { + /* Transaction stays in the ERROR state until rolledback */ + if ( $tempIgnore ) { + /* Check for constraint violation */ + if ( $errno === '23505' ) { + parent::reportQueryError( $error, $errno, $sql, $fname, $tempIgnore ); + return; + } + } + /* Don't ignore serious errors */ + $this->rollback( __METHOD__ ); + parent::reportQueryError( $error, $errno, $sql, $fname, false ); + } + + function queryIgnore( $sql, $fname = 'DatabasePostgres::queryIgnore' ) { return $this->query( $sql, $fname, true ); } @@ -331,6 +548,7 @@ class DatabasePostgres extends DatabaseBase { /** * This must be called after nextSequenceVal + * @return null */ function insertId() { return $this->mInsertId; @@ -345,13 +563,21 @@ class DatabasePostgres extends DatabaseBase { function lastError() { if ( $this->mConn ) { - return pg_last_error(); + if ( $this->mLastResult ) { + return pg_result_error( $this->mLastResult ); + } else { + return pg_last_error(); + } } else { return 'No database connection'; } } function lastErrno() { - return pg_last_error() ? 1 : 0; + if ( $this->mLastResult ) { + return pg_result_error_field( $this->mLastResult, PGSQL_DIAG_SQLSTATE ); + } else { + return false; + } } function affectedRows() { @@ -371,6 +597,7 @@ class DatabasePostgres extends DatabaseBase { * This is not necessarily an accurate estimate, so use sparingly * Returns -1 if count cannot be found * Takes same arguments as Database::select() + * @return int */ function estimateRowCount( $table, $vars = '*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) { $options['EXPLAIN'] = true; @@ -389,6 +616,7 @@ class DatabasePostgres extends DatabaseBase { /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure + * @return bool|null */ function indexInfo( $table, $index, $fname = 'DatabasePostgres::indexInfo' ) { $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'"; @@ -404,6 +632,68 @@ class DatabasePostgres extends DatabaseBase { return false; } + /** + * Returns is of attributes used in index + * + * @since 1.19 + * @return Array + */ + function indexAttributes ( $index, $schema = false ) { + if ( $schema === false ) + $schema = $this->getCoreSchema(); + /* + * A subquery would be not needed if we didn't care about the order + * of attributes, but we do + */ + $sql = <<<__INDEXATTR__ + + SELECT opcname, + attname, + i.indoption[s.g] as option, + pg_am.amname + FROM + (SELECT generate_series(array_lower(isub.indkey,1), array_upper(isub.indkey,1)) AS g + FROM + pg_index isub + JOIN pg_class cis + ON cis.oid=isub.indexrelid + JOIN pg_namespace ns + ON cis.relnamespace = ns.oid + WHERE cis.relname='$index' AND ns.nspname='$schema') AS s, + pg_attribute, + pg_opclass opcls, + pg_am, + pg_class ci + JOIN pg_index i + ON ci.oid=i.indexrelid + JOIN pg_class ct + ON ct.oid = i.indrelid + JOIN pg_namespace n + ON ci.relnamespace = n.oid + WHERE + ci.relname='$index' AND n.nspname='$schema' + AND attrelid = ct.oid + AND i.indkey[s.g] = attnum + AND i.indclass[s.g] = opcls.oid + AND pg_am.oid = opcls.opcmethod +__INDEXATTR__; + $res = $this->query($sql, __METHOD__); + $a = array(); + if ( $res ) { + foreach ( $res as $row ) { + $a[] = array( + $row->attname, + $row->opcname, + $row->amname, + $row->option); + } + } else { + return null; + } + return $a; + } + + function indexUnique( $table, $index, $fname = 'DatabasePostgres::indexUnique' ) { $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'". " AND indexdef LIKE 'CREATE UNIQUE%(" . @@ -455,15 +745,9 @@ class DatabasePostgres extends DatabaseBase { } // If IGNORE is set, we use savepoints to emulate mysql's behavior - $ignore = in_array( 'IGNORE', $options ) ? 'mw' : ''; - - // If we are not in a transaction, we need to be for savepoint trickery - $didbegin = 0; - if ( $ignore ) { - if ( !$this->mTrxLevel ) { - $this->begin(); - $didbegin = 1; - } + $savepoint = null; + if ( in_array( 'IGNORE', $options ) ) { + $savepoint = new SavepointPostgres( $this, 'mw' ); $olde = error_reporting( 0 ); // For future use, we may want to track the number of actual inserts // Right now, insert (all writes) simply return true/false @@ -473,7 +757,7 @@ class DatabasePostgres extends DatabaseBase { $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES '; if ( $multi ) { - if ( $this->numeric_version >= 8.2 && !$ignore ) { + if ( $this->numeric_version >= 8.2 && !$savepoint ) { $first = true; foreach ( $args as $row ) { if ( $first ) { @@ -483,7 +767,7 @@ class DatabasePostgres extends DatabaseBase { } $sql .= '(' . $this->makeList( $row ) . ')'; } - $res = (bool)$this->query( $sql, $fname, $ignore ); + $res = (bool)$this->query( $sql, $fname, $savepoint ); } else { $res = true; $origsql = $sql; @@ -491,18 +775,18 @@ class DatabasePostgres extends DatabaseBase { $tempsql = $origsql; $tempsql .= '(' . $this->makeList( $row ) . ')'; - if ( $ignore ) { - pg_query( $this->mConn, "SAVEPOINT $ignore" ); + if ( $savepoint ) { + $savepoint->savepoint(); } - $tempres = (bool)$this->query( $tempsql, $fname, $ignore ); + $tempres = (bool)$this->query( $tempsql, $fname, $savepoint ); - if ( $ignore ) { + if ( $savepoint ) { $bar = pg_last_error(); if ( $bar != false ) { - pg_query( $this->mConn, "ROLLBACK TO $ignore" ); + $savepoint->rollback(); } else { - pg_query( $this->mConn, "RELEASE $ignore" ); + $savepoint->release(); $numrowsinserted++; } } @@ -516,27 +800,25 @@ class DatabasePostgres extends DatabaseBase { } } else { // Not multi, just a lone insert - if ( $ignore ) { - pg_query($this->mConn, "SAVEPOINT $ignore"); + if ( $savepoint ) { + $savepoint->savepoint(); } $sql .= '(' . $this->makeList( $args ) . ')'; - $res = (bool)$this->query( $sql, $fname, $ignore ); - if ( $ignore ) { + $res = (bool)$this->query( $sql, $fname, $savepoint ); + if ( $savepoint ) { $bar = pg_last_error(); if ( $bar != false ) { - pg_query( $this->mConn, "ROLLBACK TO $ignore" ); + $savepoint->rollback(); } else { - pg_query( $this->mConn, "RELEASE $ignore" ); + $savepoint->release(); $numrowsinserted++; } } } - if ( $ignore ) { + if ( $savepoint ) { $olde = error_reporting( $olde ); - if ( $didbegin ) { - $this->commit(); - } + $savepoint->commit(); // Set the affected row count for the whole operation $this->mAffectedRows = $numrowsinserted; @@ -555,18 +837,29 @@ class DatabasePostgres extends DatabaseBase { * $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)? + * @return bool */ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect', $insertOptions = array(), $selectOptions = array() ) { $destTable = $this->tableName( $destTable ); - // If IGNORE is set, we use savepoints to emulate mysql's behavior - $ignore = in_array( 'IGNORE', $insertOptions ) ? 'mw' : ''; + if( !is_array( $insertOptions ) ) { + $insertOptions = array( $insertOptions ); + } - if( is_array( $insertOptions ) ) { - $insertOptions = implode( ' ', $insertOptions ); // FIXME: This is unused + /* + * If IGNORE is set, we use savepoints to emulate mysql's behavior + * Ignore LOW PRIORITY option, since it is MySQL-specific + */ + $savepoint = null; + if ( in_array( 'IGNORE', $insertOptions ) ) { + $savepoint = new SavepointPostgres( $this, 'mw' ); + $olde = error_reporting( 0 ); + $numrowsinserted = 0; + $savepoint->savepoint(); } + if( !is_array( $selectOptions ) ) { $selectOptions = array( $selectOptions ); } @@ -577,18 +870,6 @@ class DatabasePostgres extends DatabaseBase { $srcTable = $this->tableName( $srcTable ); } - // If we are not in a transaction, we need to be for savepoint trickery - $didbegin = 0; - if ( $ignore ) { - if( !$this->mTrxLevel ) { - $this->begin(); - $didbegin = 1; - } - $olde = error_reporting( 0 ); - $numrowsinserted = 0; - pg_query( $this->mConn, "SAVEPOINT $ignore"); - } - $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . " SELECT $startOpts " . implode( ',', $varMap ) . " FROM $srcTable $useIndex"; @@ -599,19 +880,17 @@ class DatabasePostgres extends DatabaseBase { $sql .= " $tailOpts"; - $res = (bool)$this->query( $sql, $fname, $ignore ); - if( $ignore ) { + $res = (bool)$this->query( $sql, $fname, $savepoint ); + if( $savepoint ) { $bar = pg_last_error(); if( $bar != false ) { - pg_query( $this->mConn, "ROLLBACK TO $ignore" ); + $savepoint->rollback(); } else { - pg_query( $this->mConn, "RELEASE $ignore" ); + $savepoint->release(); $numrowsinserted++; } $olde = error_reporting( $olde ); - if( $didbegin ) { - $this->commit(); - } + $savepoint->commit(); // Set the affected row count for the whole operation $this->mAffectedRows = $numrowsinserted; @@ -642,6 +921,7 @@ class DatabasePostgres extends DatabaseBase { /** * Return the next in a sequence, save the value for retrieval via insertId() + * @return null */ function nextSequenceValue( $seqName ) { $safeseq = str_replace( "'", "''", $seqName ); @@ -653,6 +933,7 @@ class DatabasePostgres extends DatabaseBase { /** * Return the current value of a sequence. Assumes it has been nextval'ed in this session. + * @return */ function currentSequenceValue( $seqName ) { $safeseq = str_replace( "'", "''", $seqName ); @@ -694,10 +975,8 @@ class DatabasePostgres extends DatabaseBase { } function listTables( $prefix = null, $fname = 'DatabasePostgres::listTables' ) { - global $wgDBmwschema; - $eschema = $this->addQuotes( $wgDBmwschema ); + $eschema = $this->addQuotes( $this->getCoreSchema() ); $result = $this->query( "SELECT tablename FROM pg_tables WHERE schemaname = $eschema", $fname ); - $endArray = array(); foreach( $result as $table ) { @@ -715,10 +994,54 @@ class DatabasePostgres extends DatabaseBase { return wfTimestamp( TS_POSTGRES, $ts ); } + /* + * Posted by cc[plus]php[at]c2se[dot]com on 25-Mar-2009 09:12 + * to http://www.php.net/manual/en/ref.pgsql.php + * + * Parsing a postgres array can be a tricky problem, he's my + * take on this, it handles multi-dimensional arrays plus + * escaping using a nasty regexp to determine the limits of each + * data-item. + * + * This should really be handled by PHP PostgreSQL module + * + * @since 1.19 + * @param $text string: postgreql array returned in a text form like {a,b} + * @param $output string + * @param $limit int + * @param $offset int + * @return string + */ + function pg_array_parse( $text, &$output, $limit = false, $offset = 1 ) { + if( false === $limit ) { + $limit = strlen( $text )-1; + $output = array(); + } + if( '{}' == $text ) { + return $output; + } + do { + if ( '{' != $text{$offset} ) { + preg_match( "/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/", + $text, $match, 0, $offset ); + $offset += strlen( $match[0] ); + $output[] = ( '"' != $match[1]{0} + ? $match[1] + : stripcslashes( substr( $match[1], 1, -1 ) ) ); + if ( '},' == $match[3] ) { + return $output; + } + } else { + $offset = $this->pg_array_parse( $text, $output, $limit, $offset+1 ); + } + } while ( $limit > $offset ); + return $output; + } + /** * Return aggregated value function call */ - function aggregateValue( $valuedata, $valuename = 'value' ) { + public function aggregateValue( $valuedata, $valuename = 'value' ) { return $valuedata; } @@ -729,6 +1052,115 @@ class DatabasePostgres extends DatabaseBase { return '[http://www.postgresql.org/ PostgreSQL]'; } + + /** + * Return current schema (executes SELECT current_schema()) + * Needs transaction + * + * @since 1.19 + * @return string return default schema for the current session + */ + function getCurrentSchema() { + $res = $this->query( "SELECT current_schema()", __METHOD__); + $row = $this->fetchRow( $res ); + return $row[0]; + } + + /** + * Return list of schemas which are accessible without schema name + * This is list does not contain magic keywords like "$user" + * Needs transaction + * + * @seealso getSearchPath() + * @seealso setSearchPath() + * @since 1.19 + * @return array list of actual schemas for the current sesson + */ + function getSchemas() { + $res = $this->query( "SELECT current_schemas(false)", __METHOD__); + $row = $this->fetchRow( $res ); + $schemas = array(); + /* PHP pgsql support does not support array type, "{a,b}" string is returned */ + return $this->pg_array_parse($row[0], $schemas); + } + + /** + * Return search patch for schemas + * This is different from getSchemas() since it contain magic keywords + * (like "$user"). + * Needs transaction + * + * @since 1.19 + * @return array how to search for table names schemas for the current user + */ + function getSearchPath() { + $res = $this->query( "SHOW search_path", __METHOD__); + $row = $this->fetchRow( $res ); + /* PostgreSQL returns SHOW values as strings */ + return explode(",", $row[0]); + } + + /** + * Update search_path, values should already be sanitized + * Values may contain magic keywords like "$user" + * @since 1.19 + * + * @param $search_path array list of schemas to be searched by default + */ + function setSearchPath( $search_path ) { + $this->query( "SET search_path = " . implode(", ", $search_path) ); + } + + /** + * Determine default schema for MediaWiki core + * Adjust this session schema search path if desired schema exists + * and is not alread there. + * + * We need to have name of the core schema stored to be able + * to query database metadata. + * + * This will be also called by the installer after the schema is created + * + * @since 1.19 + * @param $desired_schema string + */ + function determineCoreSchema( $desired_schema ) { + $this->begin( __METHOD__ ); + if ( $this->schemaExists( $desired_schema ) ) { + if ( in_array( $desired_schema, $this->getSchemas() ) ) { + $this->mCoreSchema = $desired_schema; + wfDebug("Schema \"" . $desired_schema . "\" already in the search path\n"); + } else { + /** + * Prepend our schema (e.g. 'mediawiki') in front + * of the search path + * Fixes bug 15816 + */ + $search_path = $this->getSearchPath(); + array_unshift( $search_path, + $this->addIdentifierQuotes( $desired_schema )); + $this->setSearchPath( $search_path ); + $this->mCoreSchema = $desired_schema; + wfDebug("Schema \"" . $desired_schema . "\" added to the search path\n"); + } + } else { + $this->mCoreSchema = $this->getCurrentSchema(); + wfDebug("Schema \"" . $desired_schema . "\" not found, using current \"". $this->mCoreSchema ."\"\n"); + } + /* Commit SET otherwise it will be rollbacked on error or IGNORE SELECT */ + $this->commit( __METHOD__ ); + } + + /** + * Return schema name fore core MediaWiki tables + * + * @since 1.19 + * @return string core schema name + */ + function getCoreSchema() { + return $this->mCoreSchema; + } + /** * @return string Version information from the database */ @@ -752,14 +1184,14 @@ class DatabasePostgres extends DatabaseBase { /** * Query whether a given relation exists (in the given schema, or the * default mw one if not given) + * @return bool */ function relationExists( $table, $types, $schema = false ) { - global $wgDBmwschema; if ( !is_array( $types ) ) { $types = array( $types ); } if ( !$schema ) { - $schema = $wgDBmwschema; + $schema = $this->getCoreSchema(); } $table = $this->realTableName( $table, 'raw' ); $etable = $this->addQuotes( $table ); @@ -775,6 +1207,7 @@ class DatabasePostgres extends DatabaseBase { /** * For backward compatibility, this function checks both tables and * views. + * @return bool */ function tableExists( $table, $fname = __METHOD__, $schema = false ) { return $this->relationExists( $table, array( 'r', 'v' ), $schema ); @@ -785,8 +1218,6 @@ class DatabasePostgres extends DatabaseBase { } function triggerExists( $table, $trigger ) { - global $wgDBmwschema; - $q = <<query( sprintf( $q, - $this->addQuotes( $wgDBmwschema ), + $this->addQuotes( $this->getCoreSchema() ), $this->addQuotes( $table ), $this->addQuotes( $trigger ) ) @@ -809,22 +1240,20 @@ SQL; } function ruleExists( $table, $rule ) { - global $wgDBmwschema; $exists = $this->selectField( 'pg_rules', 'rulename', array( 'rulename' => $rule, 'tablename' => $table, - 'schemaname' => $wgDBmwschema + 'schemaname' => $this->getCoreSchema() ) ); return $exists === $rule; } function constraintExists( $table, $constraint ) { - global $wgDBmwschema; $SQL = sprintf( "SELECT 1 FROM information_schema.table_constraints ". "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s", - $this->addQuotes( $wgDBmwschema ), + $this->addQuotes( $this->getCoreSchema() ), $this->addQuotes( $table ), $this->addQuotes( $constraint ) ); @@ -838,6 +1267,7 @@ SQL; /** * Query whether a given schema exists. Returns true if it does, false if it doesn't. + * @return bool */ function schemaExists( $schema ) { $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1, @@ -847,6 +1277,7 @@ SQL; /** * Returns true if a given role (i.e. user) exists, false otherwise. + * @return bool */ function roleExists( $roleName ) { $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1, @@ -860,6 +1291,7 @@ SQL; /** * pg_field_type() wrapper + * @return string */ function fieldType( $res, $index ) { if ( $res instanceof ResultWrapper ) { @@ -868,11 +1300,6 @@ SQL; return pg_field_type( $res, $index ); } - /* Not even sure why this is used in the main codebase... */ - function limitResultForUpdate( $sql, $num ) { - return $sql; - } - /** * @param $b * @return Blob @@ -979,9 +1406,6 @@ SQL; if ( isset( $noKeyOptions['FOR UPDATE'] ) ) { $postLimitTail .= ' FOR UPDATE'; } - if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) { - $postLimitTail .= ' LOCK IN SHARE MODE'; - } if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { $startOpts .= 'DISTINCT'; } diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index b2eb1c6b..f1e553d7 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -3,6 +3,21 @@ * This is the SQLite database abstraction layer. * See maintenance/sqlite/README for development notes and other specific information * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * * @file * @ingroup Database */ @@ -88,7 +103,7 @@ class DatabaseSqlite extends DatabaseBase { * * @param $fileName string * - * @return PDO|false SQL connection or false if failed + * @return PDO|bool SQL connection or false if failed */ function openFile( $fileName ) { $this->mDatabaseFile = $fileName; @@ -115,16 +130,11 @@ class DatabaseSqlite extends DatabaseBase { } /** - * Close an SQLite database - * + * Does not actually close the connection, just destroys the reference for GC to do its work * @return bool */ - function close() { - $this->mOpened = false; - if ( is_object( $this->mConn ) ) { - if ( $this->trxLevel() ) $this->commit(); - $this->mConn = null; - } + protected function closeConnection() { + $this->mConn = null; return true; } @@ -140,7 +150,7 @@ class DatabaseSqlite extends DatabaseBase { /** * Check if the searchindext table is FTS enabled. - * @return false if not enabled. + * @return bool False if not enabled. */ function checkForEnabledSearch() { if ( self::$fulltextEnabled === null ) { @@ -166,7 +176,7 @@ class DatabaseSqlite extends DatabaseBase { } $cachedResult = false; $table = 'dummy_search_test'; - + $db = new DatabaseSqliteStandalone( ':memory:' ); if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) { @@ -303,7 +313,7 @@ class DatabaseSqlite extends DatabaseBase { /** * @param $res ResultWrapper - * @param $n + * @param $n * @return bool */ function fieldName( $res, $n ) { @@ -347,7 +357,8 @@ class DatabaseSqlite extends DatabaseBase { * @return int */ function insertId() { - return $this->mConn->lastInsertId(); + // PDO::lastInsertId yields a string :( + return intval( $this->mConn->lastInsertId() ); } /** @@ -497,6 +508,7 @@ class DatabaseSqlite extends DatabaseBase { /** * Based on generic method (parent) with some prior SQLite-sepcific adjustments + * @return bool */ function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) { if ( !count( $a ) ) { @@ -610,14 +622,16 @@ class DatabaseSqlite extends DatabaseBase { * @return string User-friendly database information */ public function getServerInfo() { - return wfMsg( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() ); + return wfMessage( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() )->text(); } /** * Get information about a given field * Returns false if the field does not exist. * - * @return SQLiteField|false + * @param $table string + * @param $field string + * @return SQLiteField|bool False on failure */ function fieldInfo( $table, $field ) { $tableName = $this->tableName( $table ); @@ -631,15 +645,15 @@ class DatabaseSqlite extends DatabaseBase { return false; } - function begin( $fname = '' ) { + protected function doBegin( $fname = '' ) { if ( $this->mTrxLevel == 1 ) { - $this->commit(); + $this->commit( __METHOD__ ); } $this->mConn->beginTransaction(); $this->mTrxLevel = 1; } - function commit( $fname = '' ) { + protected function doCommit( $fname = '' ) { if ( $this->mTrxLevel == 0 ) { return; } @@ -647,7 +661,7 @@ class DatabaseSqlite extends DatabaseBase { $this->mTrxLevel = 0; } - function rollback( $fname = '' ) { + protected function doRollback( $fname = '' ) { if ( $this->mTrxLevel == 0 ) { return; } @@ -655,15 +669,6 @@ class DatabaseSqlite extends DatabaseBase { $this->mTrxLevel = 0; } - /** - * @param $sql - * @param $num - * @return string - */ - function limitResultForUpdate( $sql, $num ) { - return $this->limitResult( $sql, $num ); - } - /** * @param $s string * @return string @@ -723,6 +728,7 @@ class DatabaseSqlite extends DatabaseBase { /** * No-op version of deadlockLoop + * @return mixed */ public function deadlockLoop( /*...*/ ) { $args = func_get_args(); @@ -812,12 +818,12 @@ class DatabaseSqlite extends DatabaseBase { } return $this->query( $sql, $fname ); } - - + + /** * List all tables on the database * - * @param $prefix Only show tables with this prefix, e.g. mw_ + * @param $prefix string Only show tables with this prefix, e.g. mw_ * @param $fname String: calling function name * * @return array @@ -828,21 +834,21 @@ class DatabaseSqlite extends DatabaseBase { 'name', "type='table'" ); - + $endArray = array(); - - foreach( $result as $table ) { + + foreach( $result as $table ) { $vars = get_object_vars($table); $table = array_pop( $vars ); - + if( !$prefix || strpos( $table, $prefix ) === 0 ) { if ( strpos( $table, 'sqlite_' ) !== 0 ) { $endArray[] = $table; } - + } } - + return $endArray; } diff --git a/includes/db/DatabaseUtility.php b/includes/db/DatabaseUtility.php index 0ea713c8..c846788d 100644 --- a/includes/db/DatabaseUtility.php +++ b/includes/db/DatabaseUtility.php @@ -1,4 +1,26 @@ currentRow = false; } $this->pos++; - return $this->currentRow; + if ( is_object( $this->currentRow ) ) { + return get_object_vars( $this->currentRow ); + } else { + return $this->currentRow; + } } function seek( $row ) { diff --git a/includes/db/IORMRow.php b/includes/db/IORMRow.php new file mode 100644 index 00000000..e99ba6cc --- /dev/null +++ b/includes/db/IORMRow.php @@ -0,0 +1,275 @@ + + */ + +interface IORMRow { + + + /** + * Constructor. + * + * @since 1.20 + * + * @param IORMTable $table + * @param array|null $fields + * @param boolean $loadDefaults + */ + public function __construct( IORMTable $table, $fields = null, $loadDefaults = false ); + + /** + * Load the specified fields from the database. + * + * @since 1.20 + * + * @param array|null $fields + * @param boolean $override + * @param boolean $skipLoaded + * + * @return bool Success indicator + */ + public function loadFields( $fields = null, $override = true, $skipLoaded = false ); + + /** + * Gets the value of a field. + * + * @since 1.20 + * + * @param string $name + * @param mixed $default + * + * @throws MWException + * @return mixed + */ + public function getField( $name, $default = null ); + + /** + * Gets the value of a field but first loads it if not done so already. + * + * @since 1.20 + * + * @param string$name + * + * @return mixed + */ + public function loadAndGetField( $name ); + + /** + * Remove a field. + * + * @since 1.20 + * + * @param string $name + */ + public function removeField( $name ); + + /** + * Returns the objects database id. + * + * @since 1.20 + * + * @return integer|null + */ + public function getId(); + + /** + * Sets the objects database id. + * + * @since 1.20 + * + * @param integer|null $id + */ + public function setId( $id ); + + /** + * Gets if a certain field is set. + * + * @since 1.20 + * + * @param string $name + * + * @return boolean + */ + public function hasField( $name ); + + /** + * Gets if the id field is set. + * + * @since 1.20 + * + * @return boolean + */ + public function hasIdField(); + + /** + * Sets multiple fields. + * + * @since 1.20 + * + * @param array $fields The fields to set + * @param boolean $override Override already set fields with the provided values? + */ + public function setFields( array $fields, $override = true ); + + /** + * Serializes the object to an associative array which + * can then easily be converted into JSON or similar. + * + * @since 1.20 + * + * @param null|array $fields + * @param boolean $incNullId + * + * @return array + */ + public function toArray( $fields = null, $incNullId = false ); + + /** + * Load the default values, via getDefaults. + * + * @since 1.20 + * + * @param boolean $override + */ + public function loadDefaults( $override = true ); + + /** + * Writes the answer to the database, either updating it + * when it already exists, or inserting it when it doesn't. + * + * @since 1.20 + * + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function save( $functionName = null ); + + /** + * Removes the object from the database. + * + * @since 1.20 + * + * @return boolean Success indicator + */ + public function remove(); + + /** + * Return the names and values of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFields(); + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getSetFieldNames(); + + /** + * Sets the value of a field. + * Strings can be provided for other types, + * so this method can be called from unserialization handlers. + * + * @since 1.20 + * + * @param string $name + * @param mixed $value + * + * @throws MWException + */ + public function setField( $name, $value ); + + /** + * Add an amount (can be negative) to the specified field (needs to be numeric). + * TODO: most off this stuff makes more sense in the table class + * + * @since 1.20 + * + * @param string $field + * @param integer $amount + * + * @return boolean Success indicator + */ + public function addToField( $field, $amount ); + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFieldNames(); + + /** + * Computes and updates the values of the summary fields. + * + * @since 1.20 + * + * @param array|string|null $summaryFields + */ + public function loadSummaryFields( $summaryFields = null ); + + /** + * Sets the value for the @see $updateSummaries field. + * + * @since 1.20 + * + * @param boolean $update + */ + public function setUpdateSummaries( $update ); + + /** + * Sets the value for the @see $inSummaryMode field. + * + * @since 1.20 + * + * @param boolean $summaryMode + */ + public function setSummaryMode( $summaryMode ); + + /** + * Returns the table this IORMRow is a row in. + * + * @since 1.20 + * + * @return IORMTable + */ + public function getTable(); + +} \ No newline at end of file diff --git a/includes/db/IORMTable.php b/includes/db/IORMTable.php new file mode 100644 index 00000000..99413f99 --- /dev/null +++ b/includes/db/IORMTable.php @@ -0,0 +1,448 @@ + + */ + +interface IORMTable { + + /** + * Returns the name of the database table objects of this type are stored in. + * + * @since 1.20 + * + * @return string + */ + public function getName(); + + /** + * Returns the name of a IORMRow implementing class that + * represents single rows in this table. + * + * @since 1.20 + * + * @return string + */ + public function getRowClass(); + + /** + * Returns an array with the fields and their types this object contains. + * This corresponds directly to the fields in the database, without prefix. + * + * field name => type + * + * Allowed types: + * * id + * * str + * * int + * * float + * * bool + * * array + * * blob + * + * TODO: get rid of the id field. Every row instance needs to have + * one so this is just causing hassle at various locations by requiring an extra check for field name. + * + * @since 1.20 + * + * @return array + */ + public function getFields(); + + /** + * Returns a list of default field values. + * field name => field value + * + * @since 1.20 + * + * @return array + */ + public function getDefaults(); + + /** + * Returns a list of the summary fields. + * These are fields that cache computed values, such as the amount of linked objects of $type. + * This is relevant as one might not want to do actions such as log changes when these get updated. + * + * @since 1.20 + * + * @return array + */ + public function getSummaryFields(); + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as DBDataObject. Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return ORMResult + */ + public function select( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as DBDataObject. Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return array of self + */ + public function selectObjects( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Do the actual select. + * + * @since 1.20 + * + * @param null|string|array $fields + * @param array $conditions + * @param array $options + * @param null|string $functionName + * + * @return ResultWrapper + */ + public function rawSelect( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as associative arrays. + * Provided field names get prefixed. + * Returned field names will not have a prefix. + * + * When $collapse is true: + * If one field is selected, each item in the result array will be this field. + * If two fields are selected, each item in the result array will have as key + * the first field and as value the second field. + * If more then two fields are selected, each item will be an associative array. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param boolean $collapse Set to false to always return each result row as associative array. + * @param string|null $functionName + * + * @return array of array + */ + public function selectFields( $fields = null, array $conditions = array(), + array $options = array(), $collapse = true, $functionName = null ); + + /** + * Selects the the specified fields of the first matching record. + * Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return IORMRow|bool False on failure + */ + public function selectRow( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Selects the the specified fields of the records matching the provided + * conditions. Field names do NOT get prefixed. + * + * @since 1.20 + * + * @param array $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return ResultWrapper + */ + public function rawSelectRow( array $fields, array $conditions = array(), + array $options = array(), $functionName = null ); + + /** + * Selects the the specified fields of the first record matching the provided + * conditions and returns it as an associative array, or false when nothing matches. + * This method makes use of selectFields and expects the same parameters and + * returns the same results (if there are any, if there are none, this method returns false). + * @see IORMTable::selectFields + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param boolean $collapse Set to false to always return each result row as associative array. + * @param string|null $functionName + * + * @return mixed|array|bool False on failure + */ + public function selectFieldsRow( $fields = null, array $conditions = array(), + array $options = array(), $collapse = true, $functionName = null ); + + /** + * Returns if there is at least one record matching the provided conditions. + * Condition field names get prefixed. + * + * @since 1.20 + * + * @param array $conditions + * + * @return boolean + */ + public function has( array $conditions = array() ); + + /** + * Returns the amount of matching records. + * Condition field names get prefixed. + * + * Note that this can be expensive on large tables. + * In such cases you might want to use DatabaseBase::estimateRowCount instead. + * + * @since 1.20 + * + * @param array $conditions + * @param array $options + * + * @return integer + */ + public function count( array $conditions = array(), array $options = array() ); + + /** + * Removes the object from the database. + * + * @since 1.20 + * + * @param array $conditions + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function delete( array $conditions, $functionName = null ); + + /** + * Get API parameters for the fields supported by this object. + * + * @since 1.20 + * + * @param boolean $requireParams + * @param boolean $setDefaults + * + * @return array + */ + public function getAPIParams( $requireParams = false, $setDefaults = false ); + + /** + * Returns an array with the fields and their descriptions. + * + * field name => field description + * + * @since 1.20 + * + * @return array + */ + public function getFieldDescriptions(); + + /** + * Get the database type used for read operations. + * + * @since 1.20 + * + * @return integer DB_ enum + */ + public function getReadDb(); + + /** + * Set the database type to use for read operations. + * + * @param integer $db + * + * @since 1.20 + */ + public function setReadDb( $db ); + + /** + * Update the records matching the provided conditions by + * setting the fields that are keys in the $values param to + * their corresponding values. + * + * @since 1.20 + * + * @param array $values + * @param array $conditions + * + * @return boolean Success indicator + */ + public function update( array $values, array $conditions = array() ); + + /** + * Computes the values of the summary fields of the objects matching the provided conditions. + * + * @since 1.20 + * + * @param array|string|null $summaryFields + * @param array $conditions + */ + public function updateSummaryFields( $summaryFields = null, array $conditions = array() ); + + /** + * Takes in an associative array with field names as keys and + * their values as value. The field names are prefixed with the + * db field prefix. + * + * @since 1.20 + * + * @param array $values + * + * @return array + */ + public function getPrefixedValues( array $values ); + + /** + * Takes in a field or array of fields and returns an + * array with their prefixed versions, ready for db usage. + * + * @since 1.20 + * + * @param array|string $fields + * + * @return array + */ + public function getPrefixedFields( array $fields ); + + /** + * Takes in a field and returns an it's prefixed version, ready for db usage. + * + * @since 1.20 + * + * @param string|array $field + * + * @return string + */ + public function getPrefixedField( $field ); + + /** + * Takes an array of field names with prefix and returns the unprefixed equivalent. + * + * @since 1.20 + * + * @param array $fieldNames + * + * @return array + */ + public function unprefixFieldNames( array $fieldNames ); + + /** + * Takes a field name with prefix and returns the unprefixed equivalent. + * + * @since 1.20 + * + * @param string $fieldName + * + * @return string + */ + public function unprefixFieldName( $fieldName ); + + /** + * Get an instance of this class. + * + * @since 1.20 + * + * @return IORMTable + */ + public static function singleton(); + + /** + * Get an array with fields from a database result, + * that can be fed directly to the constructor or + * to setFields. + * + * @since 1.20 + * + * @param stdClass $result + * + * @return array + */ + public function getFieldsFromDBResult( stdClass $result ); + + /** + * Get a new instance of the class from a database result. + * + * @since 1.20 + * + * @param stdClass $result + * + * @return IORMRow + */ + public function newRowFromDBResult( stdClass $result ); + + /** + * Get a new instance of the class from an array. + * + * @since 1.20 + * + * @param array $data + * @param boolean $loadDefaults + * + * @return IORMRow + */ + public function newRow( array $data, $loadDefaults = false ); + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFieldNames(); + + /** + * Gets if the object can take a certain field. + * + * @since 1.20 + * + * @param string $name + * + * @return boolean + */ + public function canHaveField( $name ); + +} diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php index dec6ae16..e82c54ba 100644 --- a/includes/db/LBFactory.php +++ b/includes/db/LBFactory.php @@ -1,6 +1,21 @@ $wgDBserver, 'user' => $wgDBuser, @@ -183,7 +208,7 @@ class LBFactory_Simple extends LBFactory { 'dbname' => $wgDBname, 'type' => $wgDBtype, 'load' => 1, - 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT + 'flags' => $flags )); } diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php index b7977a21..6008813b 100644 --- a/includes/db/LBFactory_Multi.php +++ b/includes/db/LBFactory_Multi.php @@ -1,6 +1,21 @@ mParentInfo, $x ); @@ -385,7 +400,7 @@ class LoadBalancer { * Returns false if there is no connection open * * @param $i int - * @return DatabaseBase|false + * @return DatabaseBase|bool False on failure */ function getAnyOpenConnection( $i ) { foreach ( $this->mConns as $conns ) { @@ -894,7 +909,7 @@ class LoadBalancer { foreach ( $this->mConns as $conns2 ) { foreach ( $conns2 as $conns3 ) { foreach ( $conns3 as $conn ) { - $conn->commit(); + $conn->commit( __METHOD__ ); } } } @@ -911,7 +926,7 @@ class LoadBalancer { continue; } foreach ( $conns2[$masterIndex] as $conn ) { - if ( $conn->doneWrites() ) { + if ( $conn->writesOrCallbacksPending() ) { $conn->commit( __METHOD__ ); } } diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index 16a0343f..146ac61e 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -1,6 +1,21 @@ + */ +interface ORMIterator extends Iterator { + +} \ No newline at end of file diff --git a/includes/db/ORMResult.php b/includes/db/ORMResult.php new file mode 100644 index 00000000..2a5837a1 --- /dev/null +++ b/includes/db/ORMResult.php @@ -0,0 +1,123 @@ + + */ + +class ORMResult implements ORMIterator { + + /** + * @var ResultWrapper + */ + protected $res; + + /** + * @var integer + */ + protected $key; + + /** + * @var IORMRow + */ + protected $current; + + /** + * @var IORMTable + */ + protected $table; + + /** + * @param IORMTable $table + * @param ResultWrapper $res + */ + public function __construct( IORMTable $table, ResultWrapper $res ) { + $this->table = $table; + $this->res = $res; + $this->key = 0; + $this->setCurrent( $this->res->current() ); + } + + /** + * @param $row + */ + protected function setCurrent( $row ) { + if ( $row === false ) { + $this->current = false; + } else { + $this->current = $this->table->newRowFromDBResult( $row ); + } + } + + /** + * @return integer + */ + public function count() { + return $this->res->numRows(); + } + + /** + * @return boolean + */ + public function isEmpty() { + return $this->res->numRows() === 0; + } + + /** + * @return IORMRow + */ + public function current() { + return $this->current; + } + + /** + * @return integer + */ + public function key() { + return $this->key; + } + + public function next() { + $row = $this->res->next(); + $this->setCurrent( $row ); + $this->key++; + } + + public function rewind() { + $this->res->rewind(); + $this->key = 0; + $this->setCurrent( $this->res->current() ); + } + + /** + * @return boolean + */ + public function valid() { + return $this->current !== false; + } + +} diff --git a/includes/db/ORMRow.php b/includes/db/ORMRow.php new file mode 100644 index 00000000..303f3a20 --- /dev/null +++ b/includes/db/ORMRow.php @@ -0,0 +1,663 @@ + + */ + +abstract class ORMRow implements IORMRow { + + /** + * The fields of the object. + * field name (w/o prefix) => value + * + * @since 1.20 + * @var array + */ + protected $fields = array( 'id' => null ); + + /** + * @since 1.20 + * @var ORMTable + */ + protected $table; + + /** + * If the object should update summaries of linked items when changed. + * For example, update the course_count field in universities when a course in courses is deleted. + * Settings this to false can prevent needless updating work in situations + * such as deleting a university, which will then delete all it's courses. + * + * @since 1.20 + * @var bool + */ + protected $updateSummaries = true; + + /** + * Indicates if the object is in summary mode. + * This mode indicates that only summary fields got updated, + * which allows for optimizations. + * + * @since 1.20 + * @var bool + */ + protected $inSummaryMode = false; + + /** + * Constructor. + * + * @since 1.20 + * + * @param IORMTable $table + * @param array|null $fields + * @param boolean $loadDefaults + */ + public function __construct( IORMTable $table, $fields = null, $loadDefaults = false ) { + $this->table = $table; + + if ( !is_array( $fields ) ) { + $fields = array(); + } + + if ( $loadDefaults ) { + $fields = array_merge( $this->table->getDefaults(), $fields ); + } + + $this->setFields( $fields ); + } + + /** + * Load the specified fields from the database. + * + * @since 1.20 + * + * @param array|null $fields + * @param boolean $override + * @param boolean $skipLoaded + * + * @return bool Success indicator + */ + public function loadFields( $fields = null, $override = true, $skipLoaded = false ) { + if ( is_null( $this->getId() ) ) { + return false; + } + + if ( is_null( $fields ) ) { + $fields = array_keys( $this->table->getFields() ); + } + + if ( $skipLoaded ) { + $fields = array_diff( $fields, array_keys( $this->fields ) ); + } + + if ( !empty( $fields ) ) { + $result = $this->table->rawSelectRow( + $this->table->getPrefixedFields( $fields ), + array( $this->table->getPrefixedField( 'id' ) => $this->getId() ), + array( 'LIMIT' => 1 ) + ); + + if ( $result !== false ) { + $this->setFields( $this->table->getFieldsFromDBResult( $result ), $override ); + return true; + } + return false; + } + + return true; + } + + /** + * Gets the value of a field. + * + * @since 1.20 + * + * @param string $name + * @param mixed $default + * + * @throws MWException + * @return mixed + */ + public function getField( $name, $default = null ) { + if ( $this->hasField( $name ) ) { + return $this->fields[$name]; + } elseif ( !is_null( $default ) ) { + return $default; + } else { + throw new MWException( 'Attempted to get not-set field ' . $name ); + } + } + + /** + * Gets the value of a field but first loads it if not done so already. + * + * @since 1.20 + * + * @param string$name + * + * @return mixed + */ + public function loadAndGetField( $name ) { + if ( !$this->hasField( $name ) ) { + $this->loadFields( array( $name ) ); + } + + return $this->getField( $name ); + } + + /** + * Remove a field. + * + * @since 1.20 + * + * @param string $name + */ + public function removeField( $name ) { + unset( $this->fields[$name] ); + } + + /** + * Returns the objects database id. + * + * @since 1.20 + * + * @return integer|null + */ + public function getId() { + return $this->getField( 'id' ); + } + + /** + * Sets the objects database id. + * + * @since 1.20 + * + * @param integer|null $id + */ + public function setId( $id ) { + $this->setField( 'id', $id ); + } + + /** + * Gets if a certain field is set. + * + * @since 1.20 + * + * @param string $name + * + * @return boolean + */ + public function hasField( $name ) { + return array_key_exists( $name, $this->fields ); + } + + /** + * Gets if the id field is set. + * + * @since 1.20 + * + * @return boolean + */ + public function hasIdField() { + return $this->hasField( 'id' ) + && !is_null( $this->getField( 'id' ) ); + } + + /** + * Sets multiple fields. + * + * @since 1.20 + * + * @param array $fields The fields to set + * @param boolean $override Override already set fields with the provided values? + */ + public function setFields( array $fields, $override = true ) { + foreach ( $fields as $name => $value ) { + if ( $override || !$this->hasField( $name ) ) { + $this->setField( $name, $value ); + } + } + } + + /** + * Gets the fields => values to write to the table. + * + * @since 1.20 + * + * @return array + */ + protected function getWriteValues() { + $values = array(); + + foreach ( $this->table->getFields() as $name => $type ) { + if ( array_key_exists( $name, $this->fields ) ) { + $value = $this->fields[$name]; + + switch ( $type ) { + case 'array': + $value = (array)$value; + case 'blob': + $value = serialize( $value ); + } + + $values[$this->table->getPrefixedField( $name )] = $value; + } + } + + return $values; + } + + /** + * Serializes the object to an associative array which + * can then easily be converted into JSON or similar. + * + * @since 1.20 + * + * @param null|array $fields + * @param boolean $incNullId + * + * @return array + */ + public function toArray( $fields = null, $incNullId = false ) { + $data = array(); + $setFields = array(); + + if ( !is_array( $fields ) ) { + $setFields = $this->getSetFieldNames(); + } else { + foreach ( $fields as $field ) { + if ( $this->hasField( $field ) ) { + $setFields[] = $field; + } + } + } + + foreach ( $setFields as $field ) { + if ( $incNullId || $field != 'id' || $this->hasIdField() ) { + $data[$field] = $this->getField( $field ); + } + } + + return $data; + } + + /** + * Load the default values, via getDefaults. + * + * @since 1.20 + * + * @param boolean $override + */ + public function loadDefaults( $override = true ) { + $this->setFields( $this->table->getDefaults(), $override ); + } + + /** + * Writes the answer to the database, either updating it + * when it already exists, or inserting it when it doesn't. + * + * @since 1.20 + * + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function save( $functionName = null ) { + if ( $this->hasIdField() ) { + return $this->saveExisting( $functionName ); + } else { + return $this->insert( $functionName ); + } + } + + /** + * Updates the object in the database. + * + * @since 1.20 + * + * @param string|null $functionName + * + * @return boolean Success indicator + */ + protected function saveExisting( $functionName = null ) { + $dbw = wfGetDB( DB_MASTER ); + + $success = $dbw->update( + $this->table->getName(), + $this->getWriteValues(), + $this->table->getPrefixedValues( $this->getUpdateConditions() ), + is_null( $functionName ) ? __METHOD__ : $functionName + ); + + // DatabaseBase::update does not always return true for success as documented... + return $success !== false; + } + + /** + * Returns the WHERE considtions needed to identify this object so + * it can be updated. + * + * @since 1.20 + * + * @return array + */ + protected function getUpdateConditions() { + return array( 'id' => $this->getId() ); + } + + /** + * Inserts the object into the database. + * + * @since 1.20 + * + * @param string|null $functionName + * @param array|null $options + * + * @return boolean Success indicator + */ + protected function insert( $functionName = null, array $options = null ) { + $dbw = wfGetDB( DB_MASTER ); + + $success = $dbw->insert( + $this->table->getName(), + $this->getWriteValues(), + is_null( $functionName ) ? __METHOD__ : $functionName, + is_null( $options ) ? array( 'IGNORE' ) : $options + ); + + // DatabaseBase::insert does not always return true for success as documented... + $success = $success !== false; + + if ( $success ) { + $this->setField( 'id', $dbw->insertId() ); + } + + return $success; + } + + /** + * Removes the object from the database. + * + * @since 1.20 + * + * @return boolean Success indicator + */ + public function remove() { + $this->beforeRemove(); + + $success = $this->table->delete( array( 'id' => $this->getId() ) ); + + // DatabaseBase::delete does not always return true for success as documented... + $success = $success !== false; + + if ( $success ) { + $this->onRemoved(); + } + + return $success; + } + + /** + * Gets called before an object is removed from the database. + * + * @since 1.20 + */ + protected function beforeRemove() { + $this->loadFields( $this->getBeforeRemoveFields(), false, true ); + } + + /** + * Before removal of an object happens, @see beforeRemove gets called. + * This method loads the fields of which the names have been returned by this one (or all fields if null is returned). + * This allows for loading info needed after removal to get rid of linked data and the like. + * + * @since 1.20 + * + * @return array|null + */ + protected function getBeforeRemoveFields() { + return array(); + } + + /** + * Gets called after successfull removal. + * Can be overriden to get rid of linked data. + * + * @since 1.20 + */ + protected function onRemoved() { + $this->setField( 'id', null ); + } + + /** + * Return the names and values of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFields() { + return $this->fields; + } + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getSetFieldNames() { + return array_keys( $this->fields ); + } + + /** + * Sets the value of a field. + * Strings can be provided for other types, + * so this method can be called from unserialization handlers. + * + * @since 1.20 + * + * @param string $name + * @param mixed $value + * + * @throws MWException + */ + public function setField( $name, $value ) { + $fields = $this->table->getFields(); + + if ( array_key_exists( $name, $fields ) ) { + switch ( $fields[$name] ) { + case 'int': + $value = (int)$value; + break; + case 'float': + $value = (float)$value; + break; + case 'bool': + if ( is_string( $value ) ) { + $value = $value !== '0'; + } elseif ( is_int( $value ) ) { + $value = $value !== 0; + } + break; + case 'array': + if ( is_string( $value ) ) { + $value = unserialize( $value ); + } + + if ( !is_array( $value ) ) { + $value = array(); + } + break; + case 'blob': + if ( is_string( $value ) ) { + $value = unserialize( $value ); + } + break; + case 'id': + if ( is_string( $value ) ) { + $value = (int)$value; + } + break; + } + + $this->fields[$name] = $value; + } else { + throw new MWException( 'Attempted to set unknown field ' . $name ); + } + } + + /** + * Add an amount (can be negative) to the specified field (needs to be numeric). + * TODO: most off this stuff makes more sense in the table class + * + * @since 1.20 + * + * @param string $field + * @param integer $amount + * + * @return boolean Success indicator + */ + public function addToField( $field, $amount ) { + if ( $amount == 0 ) { + return true; + } + + if ( !$this->hasIdField() ) { + return false; + } + + $absoluteAmount = abs( $amount ); + $isNegative = $amount < 0; + + $dbw = wfGetDB( DB_MASTER ); + + $fullField = $this->table->getPrefixedField( $field ); + + $success = $dbw->update( + $this->table->getName(), + array( "$fullField=$fullField" . ( $isNegative ? '-' : '+' ) . $absoluteAmount ), + array( $this->table->getPrefixedField( 'id' ) => $this->getId() ), + __METHOD__ + ); + + if ( $success && $this->hasField( $field ) ) { + $this->setField( $field, $this->getField( $field ) + $amount ); + } + + return $success; + } + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFieldNames() { + return array_keys( $this->table->getFields() ); + } + + /** + * Computes and updates the values of the summary fields. + * + * @since 1.20 + * + * @param array|string|null $summaryFields + */ + public function loadSummaryFields( $summaryFields = null ) { + + } + + /** + * Sets the value for the @see $updateSummaries field. + * + * @since 1.20 + * + * @param boolean $update + */ + public function setUpdateSummaries( $update ) { + $this->updateSummaries = $update; + } + + /** + * Sets the value for the @see $inSummaryMode field. + * + * @since 1.20 + * + * @param boolean $summaryMode + */ + public function setSummaryMode( $summaryMode ) { + $this->inSummaryMode = $summaryMode; + } + + /** + * Return if any fields got changed. + * + * @since 1.20 + * + * @param IORMRow $object + * @param boolean|array $excludeSummaryFields + * When set to true, summary field changes are ignored. + * Can also be an array of fields to ignore. + * + * @return boolean + */ + protected function fieldsChanged( IORMRow $object, $excludeSummaryFields = false ) { + $exclusionFields = array(); + + if ( $excludeSummaryFields !== false ) { + $exclusionFields = is_array( $excludeSummaryFields ) ? $excludeSummaryFields : $this->table->getSummaryFields(); + } + + foreach ( $this->fields as $name => $value ) { + $excluded = $excludeSummaryFields && in_array( $name, $exclusionFields ); + + if ( !$excluded && $object->getField( $name ) !== $value ) { + return true; + } + } + + return false; + } + + /** + * Returns the table this IORMRow is a row in. + * + * @since 1.20 + * + * @return IORMTable + */ + public function getTable() { + return $this->table; + } + +} diff --git a/includes/db/ORMTable.php b/includes/db/ORMTable.php new file mode 100644 index 00000000..a77074ff --- /dev/null +++ b/includes/db/ORMTable.php @@ -0,0 +1,675 @@ + + */ + +abstract class ORMTable implements IORMTable { + + /** + * Gets the db field prefix. + * + * @since 1.20 + * + * @return string + */ + protected abstract function getFieldPrefix(); + + /** + * Cache for instances, used by the singleton method. + * + * @since 1.20 + * @var array of DBTable + */ + protected static $instanceCache = array(); + + /** + * The database connection to use for read operations. + * Can be changed via @see setReadDb. + * + * @since 1.20 + * @var integer DB_ enum + */ + protected $readDb = DB_SLAVE; + + /** + * Returns a list of default field values. + * field name => field value + * + * @since 1.20 + * + * @return array + */ + public function getDefaults() { + return array(); + } + + /** + * Returns a list of the summary fields. + * These are fields that cache computed values, such as the amount of linked objects of $type. + * This is relevant as one might not want to do actions such as log changes when these get updated. + * + * @since 1.20 + * + * @return array + */ + public function getSummaryFields() { + return array(); + } + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as DBDataObject. Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return ORMResult + */ + public function select( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ) { + return new ORMResult( $this, $this->rawSelect( $fields, $conditions, $options, $functionName ) ); + } + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as DBDataObject. Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return array of self + */ + public function selectObjects( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ) { + $result = $this->selectFields( $fields, $conditions, $options, false, $functionName ); + + $objects = array(); + + foreach ( $result as $record ) { + $objects[] = $this->newRow( $record ); + } + + return $objects; + } + + /** + * Do the actual select. + * + * @since 1.20 + * + * @param null|string|array $fields + * @param array $conditions + * @param array $options + * @param null|string $functionName + * + * @return ResultWrapper + */ + public function rawSelect( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ) { + if ( is_null( $fields ) ) { + $fields = array_keys( $this->getFields() ); + } + else { + $fields = (array)$fields; + } + + return wfGetDB( $this->getReadDb() )->select( + $this->getName(), + $this->getPrefixedFields( $fields ), + $this->getPrefixedValues( $conditions ), + is_null( $functionName ) ? __METHOD__ : $functionName, + $options + ); + } + + /** + * Selects the the specified fields of the records matching the provided + * conditions and returns them as associative arrays. + * Provided field names get prefixed. + * Returned field names will not have a prefix. + * + * When $collapse is true: + * If one field is selected, each item in the result array will be this field. + * If two fields are selected, each item in the result array will have as key + * the first field and as value the second field. + * If more then two fields are selected, each item will be an associative array. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param boolean $collapse Set to false to always return each result row as associative array. + * @param string|null $functionName + * + * @return array of array + */ + public function selectFields( $fields = null, array $conditions = array(), + array $options = array(), $collapse = true, $functionName = null ) { + $objects = array(); + + $result = $this->rawSelect( $fields, $conditions, $options, $functionName ); + + foreach ( $result as $record ) { + $objects[] = $this->getFieldsFromDBResult( $record ); + } + + if ( $collapse ) { + if ( count( $fields ) === 1 ) { + $objects = array_map( 'array_shift', $objects ); + } + elseif ( count( $fields ) === 2 ) { + $o = array(); + + foreach ( $objects as $object ) { + $o[array_shift( $object )] = array_shift( $object ); + } + + $objects = $o; + } + } + + return $objects; + } + + /** + * Selects the the specified fields of the first matching record. + * Field names get prefixed. + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return IORMRow|bool False on failure + */ + public function selectRow( $fields = null, array $conditions = array(), + array $options = array(), $functionName = null ) { + $options['LIMIT'] = 1; + + $objects = $this->select( $fields, $conditions, $options, $functionName ); + + return $objects->isEmpty() ? false : $objects->current(); + } + + /** + * Selects the the specified fields of the records matching the provided + * conditions. Field names do NOT get prefixed. + * + * @since 1.20 + * + * @param array $fields + * @param array $conditions + * @param array $options + * @param string|null $functionName + * + * @return ResultWrapper + */ + public function rawSelectRow( array $fields, array $conditions = array(), + array $options = array(), $functionName = null ) { + $dbr = wfGetDB( $this->getReadDb() ); + + return $dbr->selectRow( + $this->getName(), + $fields, + $conditions, + is_null( $functionName ) ? __METHOD__ : $functionName, + $options + ); + } + + /** + * Selects the the specified fields of the first record matching the provided + * conditions and returns it as an associative array, or false when nothing matches. + * This method makes use of selectFields and expects the same parameters and + * returns the same results (if there are any, if there are none, this method returns false). + * @see ORMTable::selectFields + * + * @since 1.20 + * + * @param array|string|null $fields + * @param array $conditions + * @param array $options + * @param boolean $collapse Set to false to always return each result row as associative array. + * @param string|null $functionName + * + * @return mixed|array|bool False on failure + */ + public function selectFieldsRow( $fields = null, array $conditions = array(), + array $options = array(), $collapse = true, $functionName = null ) { + $options['LIMIT'] = 1; + + $objects = $this->selectFields( $fields, $conditions, $options, $collapse, $functionName ); + + return empty( $objects ) ? false : $objects[0]; + } + + /** + * Returns if there is at least one record matching the provided conditions. + * Condition field names get prefixed. + * + * @since 1.20 + * + * @param array $conditions + * + * @return boolean + */ + public function has( array $conditions = array() ) { + return $this->selectRow( array( 'id' ), $conditions ) !== false; + } + + /** + * Returns the amount of matching records. + * Condition field names get prefixed. + * + * Note that this can be expensive on large tables. + * In such cases you might want to use DatabaseBase::estimateRowCount instead. + * + * @since 1.20 + * + * @param array $conditions + * @param array $options + * + * @return integer + */ + public function count( array $conditions = array(), array $options = array() ) { + $res = $this->rawSelectRow( + array( 'rowcount' => 'COUNT(*)' ), + $this->getPrefixedValues( $conditions ), + $options + ); + + return $res->rowcount; + } + + /** + * Removes the object from the database. + * + * @since 1.20 + * + * @param array $conditions + * @param string|null $functionName + * + * @return boolean Success indicator + */ + public function delete( array $conditions, $functionName = null ) { + return wfGetDB( DB_MASTER )->delete( + $this->getName(), + $conditions === array() ? '*' : $this->getPrefixedValues( $conditions ), + $functionName + ) !== false; // DatabaseBase::delete does not always return true for success as documented... + } + + /** + * Get API parameters for the fields supported by this object. + * + * @since 1.20 + * + * @param boolean $requireParams + * @param boolean $setDefaults + * + * @return array + */ + public function getAPIParams( $requireParams = false, $setDefaults = false ) { + $typeMap = array( + 'id' => 'integer', + 'int' => 'integer', + 'float' => 'NULL', + 'str' => 'string', + 'bool' => 'integer', + 'array' => 'string', + 'blob' => 'string', + ); + + $params = array(); + $defaults = $this->getDefaults(); + + foreach ( $this->getFields() as $field => $type ) { + if ( $field == 'id' ) { + continue; + } + + $hasDefault = array_key_exists( $field, $defaults ); + + $params[$field] = array( + ApiBase::PARAM_TYPE => $typeMap[$type], + ApiBase::PARAM_REQUIRED => $requireParams && !$hasDefault + ); + + if ( $type == 'array' ) { + $params[$field][ApiBase::PARAM_ISMULTI] = true; + } + + if ( $setDefaults && $hasDefault ) { + $default = is_array( $defaults[$field] ) ? implode( '|', $defaults[$field] ) : $defaults[$field]; + $params[$field][ApiBase::PARAM_DFLT] = $default; + } + } + + return $params; + } + + /** + * Returns an array with the fields and their descriptions. + * + * field name => field description + * + * @since 1.20 + * + * @return array + */ + public function getFieldDescriptions() { + return array(); + } + + /** + * Get the database type used for read operations. + * + * @since 1.20 + * + * @return integer DB_ enum + */ + public function getReadDb() { + return $this->readDb; + } + + /** + * Set the database type to use for read operations. + * + * @param integer $db + * + * @since 1.20 + */ + public function setReadDb( $db ) { + $this->readDb = $db; + } + + /** + * Update the records matching the provided conditions by + * setting the fields that are keys in the $values param to + * their corresponding values. + * + * @since 1.20 + * + * @param array $values + * @param array $conditions + * + * @return boolean Success indicator + */ + public function update( array $values, array $conditions = array() ) { + $dbw = wfGetDB( DB_MASTER ); + + return $dbw->update( + $this->getName(), + $this->getPrefixedValues( $values ), + $this->getPrefixedValues( $conditions ), + __METHOD__ + ) !== false; // DatabaseBase::update does not always return true for success as documented... + } + + /** + * Computes the values of the summary fields of the objects matching the provided conditions. + * + * @since 1.20 + * + * @param array|string|null $summaryFields + * @param array $conditions + */ + public function updateSummaryFields( $summaryFields = null, array $conditions = array() ) { + $this->setReadDb( DB_MASTER ); + + /** + * @var IORMRow $item + */ + foreach ( $this->select( null, $conditions ) as $item ) { + $item->loadSummaryFields( $summaryFields ); + $item->setSummaryMode( true ); + $item->save(); + } + + $this->setReadDb( DB_SLAVE ); + } + + /** + * Takes in an associative array with field names as keys and + * their values as value. The field names are prefixed with the + * db field prefix. + * + * @since 1.20 + * + * @param array $values + * + * @return array + */ + public function getPrefixedValues( array $values ) { + $prefixedValues = array(); + + foreach ( $values as $field => $value ) { + if ( is_integer( $field ) ) { + if ( is_array( $value ) ) { + $field = $value[0]; + $value = $value[1]; + } + else { + $value = explode( ' ', $value, 2 ); + $value[0] = $this->getPrefixedField( $value[0] ); + $prefixedValues[] = implode( ' ', $value ); + continue; + } + } + + $prefixedValues[$this->getPrefixedField( $field )] = $value; + } + + return $prefixedValues; + } + + /** + * Takes in a field or array of fields and returns an + * array with their prefixed versions, ready for db usage. + * + * @since 1.20 + * + * @param array|string $fields + * + * @return array + */ + public function getPrefixedFields( array $fields ) { + foreach ( $fields as &$field ) { + $field = $this->getPrefixedField( $field ); + } + + return $fields; + } + + /** + * Takes in a field and returns an it's prefixed version, ready for db usage. + * + * @since 1.20 + * + * @param string|array $field + * + * @return string + */ + public function getPrefixedField( $field ) { + return $this->getFieldPrefix() . $field; + } + + /** + * Takes an array of field names with prefix and returns the unprefixed equivalent. + * + * @since 1.20 + * + * @param array $fieldNames + * + * @return array + */ + public function unprefixFieldNames( array $fieldNames ) { + return array_map( array( $this, 'unprefixFieldName' ), $fieldNames ); + } + + /** + * Takes a field name with prefix and returns the unprefixed equivalent. + * + * @since 1.20 + * + * @param string $fieldName + * + * @return string + */ + public function unprefixFieldName( $fieldName ) { + return substr( $fieldName, strlen( $this->getFieldPrefix() ) ); + } + + /** + * Get an instance of this class. + * + * @since 1.20 + * + * @return IORMTable + */ + public static function singleton() { + $class = get_called_class(); + + if ( !array_key_exists( $class, self::$instanceCache ) ) { + self::$instanceCache[$class] = new $class; + } + + return self::$instanceCache[$class]; + } + + /** + * Get an array with fields from a database result, + * that can be fed directly to the constructor or + * to setFields. + * + * @since 1.20 + * + * @param stdClass $result + * + * @return array + */ + public function getFieldsFromDBResult( stdClass $result ) { + $result = (array)$result; + return array_combine( + $this->unprefixFieldNames( array_keys( $result ) ), + array_values( $result ) + ); + } + + /** + * @see ORMTable::newRowFromFromDBResult + * + * @deprecated use newRowFromDBResult instead + * @since 1.20 + * + * @param stdClass $result + * + * @return IORMRow + */ + public function newFromDBResult( stdClass $result ) { + return self::newRowFromDBResult( $result ); + } + + /** + * Get a new instance of the class from a database result. + * + * @since 1.20 + * + * @param stdClass $result + * + * @return IORMRow + */ + public function newRowFromDBResult( stdClass $result ) { + return $this->newRow( $this->getFieldsFromDBResult( $result ) ); + } + + /** + * @see ORMTable::newRow + * + * @deprecated use newRow instead + * @since 1.20 + * + * @param array $data + * @param boolean $loadDefaults + * + * @return IORMRow + */ + public function newFromArray( array $data, $loadDefaults = false ) { + return static::newRow( $data, $loadDefaults ); + } + + /** + * Get a new instance of the class from an array. + * + * @since 1.20 + * + * @param array $data + * @param boolean $loadDefaults + * + * @return IORMRow + */ + public function newRow( array $data, $loadDefaults = false ) { + $class = $this->getRowClass(); + return new $class( $this, $data, $loadDefaults ); + } + + /** + * Return the names of the fields. + * + * @since 1.20 + * + * @return array + */ + public function getFieldNames() { + return array_keys( $this->getFields() ); + } + + /** + * Gets if the object can take a certain field. + * + * @since 1.20 + * + * @param string $name + * + * @return boolean + */ + public function canHaveField( $name ) { + return array_key_exists( $name, $this->getFields() ); + } + +} -- cgit v1.2.2