From 9db190c7e736ec8d063187d4241b59feaf7dc2d1 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 22 Jun 2011 11:28:20 +0200 Subject: update to MediaWiki 1.17.0 --- includes/db/Database.php | 1654 +++++++++++++++++++++++-------------- includes/db/DatabaseIbm_db2.php | 1341 ++++++++++++++---------------- includes/db/DatabaseMssql.php | 1684 ++++++++++++++++++++++---------------- includes/db/DatabaseMysql.php | 240 +++++- includes/db/DatabaseOracle.php | 664 +++++++++------ includes/db/DatabasePostgres.php | 922 ++++++--------------- includes/db/DatabaseSqlite.php | 247 +++--- includes/db/LBFactory.php | 76 +- includes/db/LBFactory_Multi.php | 6 +- includes/db/LBFactory_Single.php | 57 ++ includes/db/LoadBalancer.php | 123 +-- includes/db/LoadMonitor.php | 22 +- 12 files changed, 3779 insertions(+), 3257 deletions(-) create mode 100644 includes/db/LBFactory_Single.php (limited to 'includes/db') diff --git a/includes/db/Database.php b/includes/db/Database.php index ea5d77da..5acb67fa 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -4,7 +4,7 @@ * * @file * @ingroup Database - * This file deals with MySQL interface functions + * This file deals with database interface functions * and query specifics/optimisations */ @@ -15,15 +15,201 @@ define( 'DEADLOCK_DELAY_MIN', 500000 ); /** Maximum time to wait before retry */ define( 'DEADLOCK_DELAY_MAX', 1500000 ); +/** + * Base interface for all DBMS-specific code. At a bare minimum, all of the + * following must be implemented to support MediaWiki + * + * @file + * @ingroup Database + */ +interface DatabaseType { + /** + * Get the type of the DBMS, as it appears in $wgDBtype. + * + * @return string + */ + public function getType(); + + /** + * Open a connection to the database. Usually aborts on failure + * + * @param $server String: database server host + * @param $user String: database user name + * @param $password String: database user password + * @param $dbName String: database name + * @return bool + * @throws DBConnectionError + */ + public function open( $server, $user, $password, $dbName ); + + /** + * The DBMS-dependent part of query() + * @todo Fixme: Make this private someday + * + * @param $sql String: SQL query. + * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure + * @private + */ + /*private*/ function doQuery( $sql ); + + /** + * Fetch the next row from the given result object, in object form. + * Fields can be retrieved with $row->fieldname, with fields acting like + * member variables. + * + * @param $res SQL result object as returned from DatabaseBase::query(), etc. + * @return Row object + * @throws DBUnexpectedError Thrown if the database returns an error + */ + public function fetchObject( $res ); + + /** + * Fetch the next row from the given result object, in associative array + * form. Fields are retrieved with $row['fieldname']. + * + * @param $res SQL result object as returned from DatabaseBase::query(), etc. + * @return Row object + * @throws DBUnexpectedError Thrown if the database returns an error + */ + public function fetchRow( $res ); + + /** + * Get the number of rows in a result object + * + * @param $res Mixed: A SQL result + * @return int + */ + public function numRows( $res ); + + /** + * Get the number of fields in a result object + * @see http://www.php.net/mysql_num_fields + * + * @param $res Mixed: A SQL result + * @return int + */ + public function numFields( $res ); + + /** + * Get a field name in a result object + * @see http://www.php.net/mysql_field_name + * + * @param $res Mixed: A SQL result + * @param $n Integer + * @return string + */ + public function fieldName( $res, $n ); + + /** + * Get the inserted value of an auto-increment row + * + * The value inserted should be fetched from nextSequenceValue() + * + * Example: + * $id = $dbw->nextSequenceValue('page_page_id_seq'); + * $dbw->insert('page',array('page_id' => $id)); + * $id = $dbw->insertId(); + * + * @return int + */ + public function insertId(); + + /** + * Change the position of the cursor in a result object + * @see http://www.php.net/mysql_data_seek + * + * @param $res Mixed: A SQL result + * @param $row Mixed: Either MySQL row or ResultWrapper + */ + public function dataSeek( $res, $row ); + + /** + * Get the last error number + * @see http://www.php.net/mysql_errno + * + * @return int + */ + public function lastErrno(); + + /** + * Get a description of the last error + * @see http://www.php.net/mysql_error + * + * @return string + */ + public function lastError(); + + /** + * mysql_fetch_field() wrapper + * Returns false if the field doesn't exist + * + * @param $table string: table name + * @param $field string: field name + */ + public function fieldInfo( $table, $field ); + + /** + * Get information about an index into an object + * @param $table string: Table name + * @param $index string: Index name + * @param $fname string: Calling function name + * @return Mixed: Database-specific index description class or false if the index does not exist + */ + function indexInfo( $table, $index, $fname = 'Database::indexInfo' ); + + /** + * Get the number of rows affected by the last write query + * @see http://www.php.net/mysql_affected_rows + * + * @return int + */ + public function affectedRows(); + + /** + * Wrapper for addslashes() + * + * @param $s string: to be slashed. + * @return string: slashed string. + */ + public function strencode( $s ); + + /** + * Returns a wikitext link to the DB's website, e.g., + * return "[http://www.mysql.com/ MySQL]"; + * Should at least contain plain text, if for some reason + * your database has no website. + * + * @return string: wikitext of a link to the server software's web site + */ + public static function getSoftwareLink(); + + /** + * A string describing the current software version, like from + * mysql_get_server_info(). + * + * @return string: Version information from the database server. + */ + public function getServerVersion(); + + /** + * A string describing the current software version, and possibly + * other details in a user-friendly way. Will be listed on Special:Version, etc. + * Use getServerVersion() to get machine-friendly information. + * + * @return string: Version information from the database server + */ + public function getServerInfo(); +} + /** * Database abstraction object * @ingroup Database */ -abstract class DatabaseBase { +abstract class DatabaseBase implements DatabaseType { -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Variables -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ protected $mLastQuery = ''; protected $mDoneWrites = false; @@ -32,7 +218,6 @@ abstract class DatabaseBase { protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname; protected $mOpened = false; - protected $mFailFunction; protected $mTablePrefix; protected $mFlags; protected $mTrxLevel = 0; @@ -40,26 +225,22 @@ abstract class DatabaseBase { protected $mLBInfo = array(); protected $mFakeSlaveLag = null, $mFakeMaster = false; protected $mDefaultBigSelects = null; + protected $mSchemaVars = false; -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Accessors -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # These optionally set a variable and return the previous state /** - * Fail function, takes a Database as a parameter - * Set to false for default, 1 for ignore errors - */ - function failFunction( $function = null ) { - return wfSetVar( $this->mFailFunction, $function ); - } - - /** - * Output page, used for reporting errors - * FALSE means discard output + * A string describing the current software version, and possibly + * other details in a user-friendly way. Will be listed on Special:Version, etc. + * Use getServerVersion() to get machine-friendly information. + * + * @return string: Version information from the database server */ - function setOutputPage( $out ) { - wfDeprecated( __METHOD__ ); + public function getServerInfo() { + return $this->getServerVersion(); } /** @@ -193,8 +374,8 @@ abstract class DatabaseBase { } /** - * 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 + * 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 */ function standardSelectDistinct() { return true; @@ -216,7 +397,7 @@ abstract class DatabaseBase { } /** - * Return the last query that went through Database::query() + * Return the last query that went through DatabaseBase::query() * @return String */ function lastQuery() { return $this->mLastQuery; } @@ -244,7 +425,7 @@ abstract class DatabaseBase { * - DBO_TRX: automatically start transactions * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode * and removes it in command line mode - * - DBO_PERSISTENT: use persistant database connection + * - DBO_PERSISTENT: use persistant database connection */ function setFlag( $flag ) { $this->mFlags |= $flag; @@ -266,7 +447,7 @@ abstract class DatabaseBase { * @return Boolean */ function getFlag( $flag ) { - return !!($this->mFlags & $flag); + return !!( $this->mFlags & $flag ); } /** @@ -277,7 +458,7 @@ abstract class DatabaseBase { } function getWikiID() { - if( $this->mTablePrefix ) { + if ( $this->mTablePrefix ) { return "{$this->mDBname}-{$this->mTablePrefix}"; } else { return $this->mDBname; @@ -285,13 +466,20 @@ abstract class DatabaseBase { } /** - * Get the type of the DBMS, as it appears in $wgDBtype. + * Return a path to the DBMS-specific schema, otherwise default to tables.sql */ - abstract function getType(); + public function getSchema() { + global $IP; + if ( file_exists( "$IP/maintenance/" . $this->getType() . "/tables.sql" ) ) { + return "$IP/maintenance/" . $this->getType() . "/tables.sql"; + } else { + return "$IP/maintenance/tables.sql"; + } + } -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Other functions -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ /** * Constructor. @@ -299,20 +487,18 @@ abstract class DatabaseBase { * @param $user String: database user name * @param $password String: database user password * @param $dbName String: database name - * @param $failFunction * @param $flags * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php */ function __construct( $server = false, $user = false, $password = false, $dbName = false, - $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) { - + $flags = 0, $tablePrefix = 'get from global' + ) { global $wgOut, $wgDBprefix, $wgCommandLineMode; + # Can't get a reference if it hasn't been set yet if ( !isset( $wgOut ) ) { $wgOut = null; } - - $this->mFailFunction = $failFunction; $this->mFlags = $flags; if ( $this->mFlags & DBO_DEFAULT ) { @@ -344,27 +530,53 @@ abstract class DatabaseBase { /** * Same as new DatabaseMysql( ... ), kept for backward compatibility - * @param $server String: database server host - * @param $user String: database user name - * @param $password String: database user password - * @param $dbName String: database name - * @param failFunction - * @param $flags + * @deprecated */ - static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) - { - return new DatabaseMysql( $server, $user, $password, $dbName, $failFunction, $flags ); + static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { + wfDeprecated( __METHOD__ ); + return new DatabaseMysql( $server, $user, $password, $dbName, $flags ); } /** - * Usually aborts on failure - * If the failFunction is set to a non-zero integer, returns success - * @param $server String: database server host - * @param $user String: database user name - * @param $password String: database user password - * @param $dbName String: database name - */ - abstract function open( $server, $user, $password, $dbName ); + * Given a DB type, construct the name of the appropriate child class of + * DatabaseBase. This is designed to replace all of the manual stuff like: + * $class = 'Database' . ucfirst( strtolower( $type ) ); + * as well as validate against the canonical list of DB types we have + * + * This factory function is mostly useful for when you need to connect to a + * database other than the MediaWiki default (such as for external auth, + * an extension, et cetera). Do not use this to connect to the MediaWiki + * database. Example uses in core: + * @see LoadBalancer::reallyOpenConnection() + * @see ExternalUser_MediaWiki::initFromCond() + * @see ForeignDBRepo::getMasterDB() + * @see WebInstaller_DBConnect::execute() + * + * @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 + * @return DatabaseBase subclass or null + */ + public final static function newFromType( $dbType, $p = array() ) { + $canonicalDBTypes = array( + 'mysql', 'postgres', 'sqlite', 'oracle', 'mssql', 'ibm_db2' + ); + $dbType = strtolower( $dbType ); + + if( in_array( $dbType, $canonicalDBTypes ) ) { + $class = 'Database' . ucfirst( $dbType ); + return new $class( + isset( $p['host'] ) ? $p['host'] : false, + isset( $p['user'] ) ? $p['user'] : false, + isset( $p['password'] ) ? $p['password'] : false, + isset( $p['dbname'] ) ? $p['dbname'] : false, + isset( $p['flags'] ) ? $p['flags'] : 0, + isset( $p['tableprefix'] ) ? $p['tableprefix'] : 'get from global' + ); + } else { + return null; + } + } protected function installErrorHandler() { $this->mPHPError = false; @@ -402,7 +614,7 @@ abstract class DatabaseBase { } /** - * @param $error String: fallback error message, used if none is given by MySQL + * @param $error String: fallback error message, used if none is given by DB */ function reportConnectionError( $error = 'Unknown error' ) { $myError = $this->lastError(); @@ -410,16 +622,8 @@ abstract class DatabaseBase { $error = $myError; } - if ( $this->mFailFunction ) { - # Legacy error handling method - if ( !is_int( $this->mFailFunction ) ) { - $ff = $this->mFailFunction; - $ff( $this, $error ); - } - } else { - # New method - throw new DBConnectionError( $this, $error ); - } + # New method + throw new DBConnectionError( $this, $error ); } /** @@ -434,11 +638,11 @@ abstract class DatabaseBase { * Usually aborts on failure. If errors are explicitly ignored, returns success. * * @param $sql String: SQL query - * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST + * @param $fname String: Name of the calling function, for profiling/SHOW PROCESSLIST * comment (you can use __METHOD__ or add some extra info) - * @param $tempIgnore Boolean: Whether to avoid throwing an exception on errors... + * @param $tempIgnore Boolean: Whether to avoid throwing an exception on errors... * maybe best to catch the exception instead? - * @return true for a successful write query, ResultWrapper object for a successful read query, + * @return boolean or ResultWrapper. true for a successful write query, ResultWrapper object for a successful read query, * or false on failure if $tempIgnore set * @throws DBQueryError Thrown when the database returns an error of any kind */ @@ -451,15 +655,16 @@ abstract class DatabaseBase { # logging size most of the time. The substr is really just a sanity check. # Who's been wasting my precious column space? -- TS - #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); + # $profName = 'query: ' . $fname . ' ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); if ( $isMaster ) { - $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); - $totalProf = 'Database::query-master'; + $queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'DatabaseBase::query-master'; } else { - $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 ); - $totalProf = 'Database::query'; + $queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 ); + $totalProf = 'DatabaseBase::query'; } + wfProfileIn( $totalProf ); wfProfileIn( $queryProf ); } @@ -467,14 +672,14 @@ abstract class DatabaseBase { $this->mLastQuery = $sql; if ( !$this->mDoneWrites && $this->isWriteQuery( $sql ) ) { // Set a flag indicating that writes have been done - wfDebug( __METHOD__.": Writes done: $sql\n" ); + wfDebug( __METHOD__ . ": Writes done: $sql\n" ); $this->mDoneWrites = true; } # Add a comment for easy SHOW PROCESSLIST interpretation - #if ( $fname ) { + # if ( $fname ) { global $wgUser; - if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) { + if ( is_object( $wgUser ) && $wgUser->mDataLoaded ) { $userName = $wgUser->getName(); if ( mb_strlen( $userName ) > 15 ) { $userName = mb_substr( $userName, 0, 15 ) . '...'; @@ -483,29 +688,33 @@ abstract class DatabaseBase { } else { $userName = ''; } - $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1); - #} else { + $commentedSql = preg_replace( '/\s/', " /* $fname $userName */ ", $sql, 1 ); + # } else { # $commentedSql = $sql; - #} + # } # If DBO_TRX is set, start a transaction - if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && - $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') { + if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && + $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' ) { // avoid establishing transactions for SHOW and SET statements too - - // that would delay transaction initializations to once connection + // that would delay transaction initializations to once connection // is really used by application - $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm) - if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0) - $this->begin(); + $sqlstart = substr( $sql, 0, 10 ); // very much worth it, benchmark certified(tm) + if ( strpos( $sqlstart, "SHOW " ) !== 0 and strpos( $sqlstart, "SET " ) !== 0 ) + $this->begin(); } if ( $this->debug() ) { + static $cnt = 0; + + $cnt++; $sqlx = substr( $commentedSql, 0, 500 ); $sqlx = strtr( $sqlx, "\t\n", ' ' ); + if ( $isMaster ) { - wfDebug( "SQL-master: $sqlx\n" ); + wfDebug( "Query $cnt (master): $sqlx\n" ); } else { - wfDebug( "SQL: $sqlx\n" ); + wfDebug( "Query $cnt (slave): $sqlx\n" ); } } @@ -521,13 +730,17 @@ abstract class DatabaseBase { # Transaction is gone, like it or not $this->mTrxLevel = 0; wfDebug( "Connection lost, reconnecting...\n" ); + if ( $this->ping() ) { wfDebug( "Reconnected\n" ); $sqlx = substr( $commentedSql, 0, 500 ); $sqlx = strtr( $sqlx, "\t\n", ' ' ); global $wgRequestTime; - $elapsed = round( microtime(true) - $wgRequestTime, 3 ); - wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" ); + $elapsed = round( microtime( true ) - $wgRequestTime, 3 ); + if ( $elapsed < 300 ) { + # Not a database error to lose a transaction after a minute or two + wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" ); + } $ret = $this->doQuery( $commentedSql ); } else { wfDebug( "Failed\n" ); @@ -542,17 +755,10 @@ abstract class DatabaseBase { wfProfileOut( $queryProf ); wfProfileOut( $totalProf ); } + return $this->resultObject( $ret ); } - /** - * The DBMS-dependent part of query() - * @param $sql String: SQL query. - * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure - * @private - */ - /*private*/ abstract function doQuery( $sql ); - /** * @param $error String * @param $errno Integer @@ -561,18 +767,17 @@ abstract class DatabaseBase { * @param $tempIgnore Boolean */ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - global $wgCommandLineMode; # Ignore errors during error handling to avoid infinite recursion $ignore = $this->ignoreErrors( true ); ++$this->mErrorCount; - if( $ignore || $tempIgnore ) { - wfDebug("SQL ERROR (ignored): $error\n"); + if ( $ignore || $tempIgnore ) { + wfDebug( "SQL ERROR (ignored): $error\n" ); $this->ignoreErrors( $ignore ); } else { $sql1line = str_replace( "\n", "\\n", $sql ); - wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n"); - wfDebug("SQL ERROR: " . $error . "\n"); + wfLogDBError( "$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n" ); + wfDebug( "SQL ERROR: " . $error . "\n" ); throw new DBQueryError( $this, $error, $errno, $sql, $fname ); } } @@ -587,7 +792,7 @@ abstract class DatabaseBase { * & = filename; reads the file and inserts as a blob * (we don't use this though...) */ - function prepare( $sql, $func = 'Database::prepare' ) { + 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. */ @@ -595,7 +800,7 @@ abstract class DatabaseBase { } function freePrepared( $prepared ) { - /* No-op for MySQL */ + /* No-op by default */ } /** @@ -604,12 +809,14 @@ abstract class DatabaseBase { * @param $args Mixed: Either an array here, or put scalars as varargs */ function execute( $prepared, $args = null ) { - if( !is_array( $args ) ) { + if ( !is_array( $args ) ) { # Pull the var args $args = func_get_args(); array_shift( $args ); } + $sql = $this->fillPrepared( $prepared['query'], $args ); + return $this->query( $sql, $prepared['func'] ); } @@ -620,14 +827,17 @@ abstract class DatabaseBase { * @param $args ... */ function safeQuery( $query, $args = null ) { - $prepared = $this->prepare( $query, 'Database::safeQuery' ); - if( !is_array( $args ) ) { + $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; } @@ -641,6 +851,7 @@ abstract class DatabaseBase { function fillPrepared( $preparedQuery, $args ) { reset( $args ); $this->preparedArgs =& $args; + return preg_replace_callback( '/(\\\\[?!&]|[?!&])/', array( &$this, 'fillPreparedArg' ), $preparedQuery ); } @@ -660,7 +871,9 @@ abstract class DatabaseBase { case '\\!': return '!'; case '\\&': return '&'; } + list( /* $n */ , $arg ) = each( $this->preparedArgs ); + switch( $matches[1] ) { case '?': return $this->addQuotes( $arg ); case '!': return $arg; @@ -681,99 +894,19 @@ abstract class DatabaseBase { # be freed by PHP when the variable goes out of scope anyway. } - /** - * Fetch the next row from the given result object, in object form. - * Fields can be retrieved with $row->fieldname, with fields acting like - * member variables. - * - * @param $res SQL result object as returned from Database::query(), etc. - * @return MySQL row object - * @throws DBUnexpectedError Thrown if the database returns an error - */ - abstract function fetchObject( $res ); - - /** - * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. - * - * @param $res SQL result object as returned from Database::query(), etc. - * @return MySQL row object - * @throws DBUnexpectedError Thrown if the database returns an error - */ - abstract function fetchRow( $res ); - - /** - * Get the number of rows in a result object - * @param $res Mixed: A SQL result - */ - abstract function numRows( $res ); - - /** - * Get the number of fields in a result object - * See documentation for mysql_num_fields() - * @param $res Mixed: A SQL result - */ - abstract function numFields( $res ); - - /** - * Get a field name in a result object - * See documentation for mysql_field_name(): - * http://www.php.net/mysql_field_name - * @param $res Mixed: A SQL result - * @param $n Integer - */ - abstract function fieldName( $res, $n ); - - /** - * Get the inserted value of an auto-increment row - * - * The value inserted should be fetched from nextSequenceValue() - * - * Example: - * $id = $dbw->nextSequenceValue('page_page_id_seq'); - * $dbw->insert('page',array('page_id' => $id)); - * $id = $dbw->insertId(); - */ - abstract function insertId(); - - /** - * Change the position of the cursor in a result object - * See mysql_data_seek() - * @param $res Mixed: A SQL result - * @param $row Mixed: Either MySQL row or ResultWrapper - */ - abstract function dataSeek( $res, $row ); - - /** - * Get the last error number - * See mysql_errno() - */ - abstract function lastErrno(); - - /** - * Get a description of the last error - * See mysql_error() for more details - */ - abstract function lastError(); - - /** - * Get the number of rows affected by the last write query - * See mysql_affected_rows() for more details - */ - abstract function affectedRows(); - /** * Simple UPDATE wrapper * Usually aborts on failure * If errors are explicitly ignored, returns success * - * This function exists for historical reasons, Database::update() has a more standard + * This function exists for historical reasons, DatabaseBase::update() has a more standard * calling convention and feature set */ - function set( $table, $var, $value, $cond, $fname = 'Database::set' ) { + 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 ); } @@ -782,19 +915,22 @@ abstract class DatabaseBase { * Usually aborts on failure * If errors are explicitly ignored, returns FALSE on failure */ - function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) { + function selectField( $table, $var, $cond = '', $fname = 'DatabaseBase::selectField', $options = array() ) { if ( !is_array( $options ) ) { $options = array( $options ); } + $options['LIMIT'] = 1; $res = $this->select( $table, $var, $cond, $fname, $options ); + if ( $res === false || !$this->numRows( $res ) ) { return false; } + $row = $this->fetchRow( $res ); + if ( $row !== false ) { - $this->freeResult( $res ); return reset( $row ); } else { return false; @@ -816,42 +952,82 @@ abstract class DatabaseBase { $startOpts = ''; $noKeyOptions = array(); + foreach ( $options as $key => $option ) { if ( is_numeric( $key ) ) { $noKeyOptions[$option] = true; } } - if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; - if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}"; - if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; - - //if (isset($options['LIMIT'])) { + if ( isset( $options['GROUP BY'] ) ) { + $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; + } + + if ( isset( $options['HAVING'] ) ) { + $preLimitTail .= " HAVING {$options['HAVING']}"; + } + + if ( isset( $options['ORDER BY'] ) ) { + $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; + } + + // if (isset($options['LIMIT'])) { // $tailOpts .= $this->limitResult('', $options['LIMIT'], - // isset($options['OFFSET']) ? $options['OFFSET'] + // isset($options['OFFSET']) ? $options['OFFSET'] // : false); - //} + // } + + if ( isset( $noKeyOptions['FOR UPDATE'] ) ) { + $postLimitTail .= ' FOR UPDATE'; + } + + if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) { + $postLimitTail .= ' LOCK IN SHARE MODE'; + } - 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'; + if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) { + $startOpts .= 'DISTINCT'; + } # Various MySQL extensions - if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */'; - if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY'; - if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT'; - if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT'; - if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT'; - if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS'; - if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE'; - if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE'; + if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) { + $startOpts .= ' /*! STRAIGHT_JOIN */'; + } + + if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) { + $startOpts .= ' HIGH_PRIORITY'; + } + + if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) { + $startOpts .= ' SQL_BIG_RESULT'; + } + + if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) { + $startOpts .= ' SQL_BUFFER_RESULT'; + } + + if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) { + $startOpts .= ' SQL_SMALL_RESULT'; + } + + if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) { + $startOpts .= ' SQL_CALC_FOUND_ROWS'; + } + + if ( isset( $noKeyOptions['SQL_CACHE'] ) ) { + $startOpts .= ' SQL_CACHE'; + } + + if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) { + $startOpts .= ' SQL_NO_CACHE'; + } if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { $useIndex = $this->useIndexClause( $options['USE INDEX'] ); } else { $useIndex = ''; } - + return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); } @@ -863,44 +1039,47 @@ abstract class DatabaseBase { * @param $conds Mixed: Array or string, condition(s) for WHERE * @param $fname String: Calling function name (use __METHOD__) for logs/profiling * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), - * see Database::makeSelectOptions code for list of supported stuff + * see DatabaseBase::makeSelectOptions code for list of supported stuff * @param $join_conds Array: Associative array of table join conditions (optional) * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) - * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure + * @return mixed Database result resource (feed to DatabaseBase::fetchObject or whatever), or false on failure */ - function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) - { + function select( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) { $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); + return $this->query( $sql, $fname ); } - + /** * SELECT wrapper * - * @param $table Mixed: Array or string, table name(s) (prefix auto-added) + * @param $table Mixed: Array or string, table name(s) (prefix auto-added). Array keys are table aliases (optional) * @param $vars Mixed: Array or string, field name(s) to be retrieved * @param $conds Mixed: Array or string, condition(s) for WHERE * @param $fname String: Calling function name (use __METHOD__) for logs/profiling * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), - * see Database::makeSelectOptions code for list of supported stuff + * see DatabaseBase::makeSelectOptions code for list of supported stuff * @param $join_conds Array: Associative array of table join conditions (optional) * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) * @return string, the SQL text */ - function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) { - if( is_array( $vars ) ) { + function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseBase::select', $options = array(), $join_conds = array() ) { + if ( is_array( $vars ) ) { $vars = implode( ',', $vars ); } - if( !is_array( $options ) ) { + + if ( !is_array( $options ) ) { $options = array( $options ); } - if( is_array( $table ) ) { - if ( !empty($join_conds) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) ) + + if ( is_array( $table ) ) { + if ( !empty( $join_conds ) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) ) { $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds ); - else - $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) ); - } elseif ($table!='') { - if ($table{0}==' ') { + } else { + $from = ' FROM ' . implode( ',', $this->tableNamesWithAlias( $table ) ); + } + } elseif ( $table != '' ) { + if ( $table { 0 } == ' ' ) { $from = ' FROM ' . $table; } else { $from = ' FROM ' . $this->tableName( $table ); @@ -911,7 +1090,7 @@ abstract class DatabaseBase { list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options ); - if( !empty( $conds ) ) { + if ( !empty( $conds ) ) { if ( is_array( $conds ) ) { $conds = $this->makeList( $conds, LIST_AND ); } @@ -920,14 +1099,15 @@ abstract class DatabaseBase { $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail"; } - if (isset($options['LIMIT'])) - $sql = $this->limitResult($sql, $options['LIMIT'], - isset($options['OFFSET']) ? $options['OFFSET'] : false); + if ( isset( $options['LIMIT'] ) ) + $sql = $this->limitResult( $sql, $options['LIMIT'], + isset( $options['OFFSET'] ) ? $options['OFFSET'] : false ); $sql = "$sql $postLimitTail"; - - if (isset($options['EXPLAIN'])) { + + if ( isset( $options['EXPLAIN'] ) ) { $sql = 'EXPLAIN ' . $sql; } + return $sql; } @@ -949,42 +1129,45 @@ abstract class DatabaseBase { * * @todo migrate documentation to phpdocumentor format */ - function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) { + function selectRow( $table, $vars, $conds, $fname = 'DatabaseBase::selectRow', $options = array(), $join_conds = array() ) { $options['LIMIT'] = 1; $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds ); - if ( $res === false ) + + if ( $res === false ) { return false; - if ( !$this->numRows($res) ) { - $this->freeResult($res); + } + + if ( !$this->numRows( $res ) ) { return false; } + $obj = $this->fetchObject( $res ); - $this->freeResult( $res ); - return $obj; + return $obj; } - + /** * Estimate rows in dataset * Returns estimated count - not necessarily an accurate estimate across different databases, * so use sparingly - * Takes same arguments as Database::select() + * Takes same arguments as DatabaseBase::select() * - * @param string $table table name - * @param array $vars unused - * @param array $conds filters on the table - * @param string $fname function name for profiling - * @param array $options options for select - * @return int row count + * @param $table String: table name + * @param $vars Array: unused + * @param $conds Array: filters on the table + * @param $fname String: function name for profiling + * @param $options Array: options for select + * @return Integer: row count */ - public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { + public function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseBase::estimateRowCount', $options = array() ) { $rows = 0; $res = $this->select ( $table, 'COUNT(*) AS rowcount', $conds, $fname, $options ); + if ( $res ) { $row = $this->fetchRow( $res ); $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0; } - $this->freeResult( $res ); + return $rows; } @@ -999,42 +1182,33 @@ abstract class DatabaseBase { # as to avoid crashing php on some large strings. # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql); - $sql = str_replace ( "\\\\", '', $sql); - $sql = str_replace ( "\\'", '', $sql); - $sql = str_replace ( "\\\"", '', $sql); - $sql = preg_replace ("/'.*'/s", "'X'", $sql); - $sql = preg_replace ('/".*"/s', "'X'", $sql); + $sql = str_replace ( "\\\\", '', $sql ); + $sql = str_replace ( "\\'", '', $sql ); + $sql = str_replace ( "\\\"", '', $sql ); + $sql = preg_replace ( "/'.*'/s", "'X'", $sql ); + $sql = preg_replace ( '/".*"/s', "'X'", $sql ); # All newlines, tabs, etc replaced by single space - $sql = preg_replace ( '/\s+/', ' ', $sql); + $sql = preg_replace ( '/\s+/', ' ', $sql ); # All numbers => N - $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql); + $sql = preg_replace ( '/-?[0-9]+/s', 'N', $sql ); return $sql; } /** * Determines whether a field exists in a table - * Usually aborts on failure - * If errors are explicitly ignored, returns NULL on failure + * + * @param $table String: table name + * @param $field String: filed to check on that table + * @param $fname String: calling function name (optional) + * @return Boolean: whether $table has filed $field */ - function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) { - $table = $this->tableName( $table ); - $res = $this->query( 'DESCRIBE '.$table, $fname ); - if ( !$res ) { - return null; - } - - $found = false; + function fieldExists( $table, $field, $fname = 'DatabaseBase::fieldExists' ) { + $info = $this->fieldInfo( $table, $field ); - while ( $row = $this->fetchObject( $res ) ) { - if ( $row->Field == $field ) { - $found = true; - break; - } - } - return $found; + return (bool)$info; } /** @@ -1042,7 +1216,7 @@ abstract class DatabaseBase { * Usually aborts on failure * If errors are explicitly ignored, returns NULL on failure */ - function indexExists( $table, $index, $fname = 'Database::indexExists' ) { + function indexExists( $table, $index, $fname = 'DatabaseBase::indexExists' ) { $info = $this->indexInfo( $table, $index, $fname ); if ( is_null( $info ) ) { return null; @@ -1051,58 +1225,17 @@ abstract class DatabaseBase { } } - - /** - * Get information about an index into an object - * Returns false if the index does not exist - */ - function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) { - # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not. - # SHOW INDEX should work for 3.x and up: - # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html - $table = $this->tableName( $table ); - $index = $this->indexName( $index ); - $sql = 'SHOW INDEX FROM '.$table; - $res = $this->query( $sql, $fname ); - if ( !$res ) { - return null; - } - - $result = array(); - while ( $row = $this->fetchObject( $res ) ) { - if ( $row->Key_name == $index ) { - $result[] = $row; - } - } - $this->freeResult($res); - - return empty($result) ? false : $result; - } - /** * Query whether a given table exists */ function tableExists( $table ) { $table = $this->tableName( $table ); $old = $this->ignoreErrors( true ); - $res = $this->query( "SELECT 1 FROM $table LIMIT 1" ); + $res = $this->query( "SELECT 1 FROM $table LIMIT 1", __METHOD__ ); $this->ignoreErrors( $old ); - if( $res ) { - $this->freeResult( $res ); - return true; - } else { - return false; - } - } - /** - * mysql_fetch_field() wrapper - * Returns false if the field doesn't exist - * - * @param $table - * @param $field - */ - abstract function fieldInfo( $table, $field ); + return (bool)$res; + } /** * mysql_field_type() wrapper @@ -1111,6 +1244,7 @@ abstract class DatabaseBase { if ( $res instanceof ResultWrapper ) { $res = $res->result; } + return mysql_field_type( $res, $index ); } @@ -1119,9 +1253,11 @@ abstract class DatabaseBase { */ function indexUnique( $table, $index ) { $indexInfo = $this->indexInfo( $table, $index ); + if ( !$indexInfo ) { return null; } + return !$indexInfo[0]->Non_unique; } @@ -1133,17 +1269,26 @@ abstract class DatabaseBase { * * Usually aborts on failure * If errors are explicitly ignored, returns success + * + * @param $table String: table name (prefix auto-added) + * @param $a Array: Array of rows to insert + * @param $fname String: Calling function name (use __METHOD__) for logs/profiling + * @param $options Mixed: Associative array of options + * + * @return bool */ - function insert( $table, $a, $fname = 'Database::insert', $options = array() ) { + function insert( $table, $a, $fname = 'DatabaseBase::insert', $options = array() ) { # No rows to insert, easy just return now if ( !count( $a ) ) { return true; } $table = $this->tableName( $table ); + if ( !is_array( $options ) ) { $options = array( $options ); } + if ( isset( $a[0] ) && is_array( $a[0] ) ) { $multi = true; $keys = array_keys( $a[0] ); @@ -1168,26 +1313,33 @@ abstract class DatabaseBase { } else { $sql .= '(' . $this->makeList( $a ) . ')'; } + return (bool)$this->query( $sql, $fname ); } /** - * Make UPDATE options for the Database::update function + * Make UPDATE options for the DatabaseBase::update function * * @private - * @param $options Array: The options passed to Database::update + * @param $options Array: The options passed to DatabaseBase::update * @return string */ function makeUpdateOptions( $options ) { - if( !is_array( $options ) ) { + if ( !is_array( $options ) ) { $options = array( $options ); } + $opts = array(); - if ( in_array( 'LOW_PRIORITY', $options ) ) + + if ( in_array( 'LOW_PRIORITY', $options ) ) { $opts[] = $this->lowPriorityOption(); - if ( in_array( 'IGNORE', $options ) ) + } + + if ( in_array( 'IGNORE', $options ) ) { $opts[] = 'IGNORE'; - return implode(' ', $opts); + } + + return implode( ' ', $opts ); } /** @@ -1202,13 +1354,15 @@ abstract class DatabaseBase { * more of IGNORE, LOW_PRIORITY * @return Boolean */ - function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) { + function update( $table, $values, $conds, $fname = 'DatabaseBase::update', $options = array() ) { $table = $this->tableName( $table ); $opts = $this->makeUpdateOptions( $options ); $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET ); + if ( $conds != '*' ) { $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); } + return $this->query( $sql, $fname ); } @@ -1223,16 +1377,17 @@ abstract class DatabaseBase { */ function makeList( $a, $mode = LIST_COMMA ) { if ( !is_array( $a ) ) { - throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' ); + throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' ); } $first = true; $list = ''; + foreach ( $a as $field => $value ) { if ( !$first ) { if ( $mode == LIST_AND ) { $list .= ' AND '; - } elseif($mode == LIST_OR) { + } elseif ( $mode == LIST_OR ) { $list .= ' OR '; } else { $list .= ','; @@ -1240,23 +1395,24 @@ abstract class DatabaseBase { } else { $first = false; } - if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) { + + if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) { $list .= "($value)"; - } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) { + } elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) { $list .= "$value"; - } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) { - if( count( $value ) == 0 ) { - throw new MWException( __METHOD__.': empty input' ); - } elseif( count( $value ) == 1 ) { + } elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) { + if ( count( $value ) == 0 ) { + throw new MWException( __METHOD__ . ': empty input' ); + } elseif ( count( $value ) == 1 ) { // Special-case single values, as IN isn't terribly efficient // Don't necessarily assume the single key is 0; we don't // enforce linear numeric ordering on other arrays here. $value = array_values( $value ); - $list .= $field." = ".$this->addQuotes( $value[0] ); + $list .= $field . " = " . $this->addQuotes( $value[0] ); } else { - $list .= $field." IN (".$this->makeList($value).") "; + $list .= $field . " IN (" . $this->makeList( $value ) . ") "; } - } elseif( $value === null ) { + } elseif ( $value === null ) { if ( $mode == LIST_AND || $mode == LIST_OR ) { $list .= "$field IS "; } elseif ( $mode == LIST_SET ) { @@ -1270,35 +1426,64 @@ abstract class DatabaseBase { $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value ); } } + return $list; } + /** + * Build a partial where clause from a 2-d array such as used for LinkBatch. + * The keys on each level may be either integers or strings. + * + * @param $data Array: organized as 2-d array(baseKeyVal => array(subKeyVal => , ...), ...) + * @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 ) { + $conds = array(); + + foreach ( $data as $base => $sub ) { + if ( count( $sub ) ) { + $conds[] = $this->makeList( + array( $baseKey => $base, $subKey => array_keys( $sub ) ), + LIST_AND ); + } + } + + if ( $conds ) { + return $this->makeList( $conds, LIST_OR ); + } else { + // Nothing to search for... + return false; + } + } + /** * Bitwise operations */ - function bitNot($field) { - return "(~$bitField)"; + function bitNot( $field ) { + return "(~$field)"; } - function bitAnd($fieldLeft, $fieldRight) { + function bitAnd( $fieldLeft, $fieldRight ) { return "($fieldLeft & $fieldRight)"; } - function bitOr($fieldLeft, $fieldRight) { + function bitOr( $fieldLeft, $fieldRight ) { return "($fieldLeft | $fieldRight)"; } /** * Change the current database * + * @todo Explain what exactly will fail if this is not overridden. * @return bool Success or failure */ 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. TODO: explain what exactly will fail if - # this is not overridden. + # databases you may as well. return true; } @@ -1335,8 +1520,10 @@ abstract class DatabaseBase { # Note that we check the end so that we will still quote any use of # use of `database`.table. But won't break things if someone wants # to query a database table with a dot in the name. - if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name; - + if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) { + return $name; + } + # Lets test for any bits of text that should never show up in a table # name. Basically anything like JOIN or ON which are actually part of # SQL queries, but may end up inside of the table value to combine @@ -1344,23 +1531,30 @@ abstract class DatabaseBase { # Note that we use a whitespace test rather than a \b test to avoid # any remote case where a word like on may be inside of a table name # surrounded by symbols which may be considered word breaks. - if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name; - + if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) { + return $name; + } + # Split database and table into proper variables. # We reverse the explode so that database.table and table both output # the correct table. $dbDetails = array_reverse( explode( '.', $name, 2 ) ); - if( isset( $dbDetails[1] ) ) @list( $table, $database ) = $dbDetails; - else @list( $table ) = $dbDetails; + if ( isset( $dbDetails[1] ) ) { + @list( $table, $database ) = $dbDetails; + } else { + @list( $table ) = $dbDetails; + } $prefix = $this->mTablePrefix; # Default prefix - + # A database name has been specified in input. Quote the table name # because we don't want any prefixes added. - if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" ); - + if ( isset( $database ) ) { + $table = ( $table[0] == '`' ? $table : "`{$table}`" ); + } + # Note that we use the long format because php will complain in in_array if # the input is not an array, and will complain in is_array if it is not set. - if( !isset( $database ) # Don't use shared database if pre selected. + if ( !isset( $database ) # Don't use shared database if pre selected. && isset( $wgSharedDB ) # We have a shared database && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`' && isset( $wgSharedTables ) @@ -1369,15 +1563,16 @@ abstract class DatabaseBase { $database = $wgSharedDB; $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix; } - + # Quote the $database and $table and apply the prefix if not quoted. - if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" ); + if ( isset( $database ) ) { + $database = ( $database[0] == '`' ? $database : "`{$database}`" ); + } $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" ); - + # Merge our database and table into our final table name. - $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" ); - - # We're finished, return. + $tableName = ( isset( $database ) ? "{$database}.{$table}" : "{$table}" ); + return $tableName; } @@ -1393,12 +1588,14 @@ abstract class DatabaseBase { public function tableNames() { $inArray = func_get_args(); $retVal = array(); + foreach ( $inArray as $name ) { $retVal[$name] = $this->tableName( $name ); } + return $retVal; } - + /** * Fetch a number of table names into an zero-indexed numerical array * This is handy when you need to construct SQL for joins @@ -1411,47 +1608,97 @@ abstract class DatabaseBase { public function tableNamesN() { $inArray = func_get_args(); $retVal = array(); + foreach ( $inArray as $name ) { $retVal[] = $this->tableName( $name ); } + return $retVal; } + /** + * Get an aliased table name + * e.g. tableName AS newTableName + * + * @param $name string Table name, see tableName() + * @param $alias string Alias (optional) + * @return string SQL name for aliased table. Will not alias a table to its own name + */ + public function tableNameWithAlias( $name, $alias = false ) { + if ( !$alias || $alias == $name ) { + return $this->tableName( $name ); + } else { + return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias ); + } + } + + /** + * Gets an array of aliased table names + * + * @param $tables array( [alias] => table ) + * @return array of strings, see tableNameWithAlias() + */ + public function tableNamesWithAlias( $tables ) { + $retval = array(); + foreach ( $tables as $alias => $table ) { + if ( is_numeric( $alias ) ) { + $alias = $table; + } + $retval[] = $this->tableNameWithAlias( $table, $alias ); + } + return $retval; + } + /** * @private */ function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) { $ret = array(); $retJOIN = array(); - $use_index_safe = is_array($use_index) ? $use_index : array(); - $join_conds_safe = is_array($join_conds) ? $join_conds : array(); - foreach ( $tables as $table ) { + $use_index_safe = is_array( $use_index ) ? $use_index : array(); + $join_conds_safe = is_array( $join_conds ) ? $join_conds : array(); + + foreach ( $tables as $alias => $table ) { + if ( !is_string( $alias ) ) { + // No alias? Set it equal to the table name + $alias = $table; + } // Is there a JOIN and INDEX clause for this table? - if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) { - $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table ); - $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) ); - $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')'; + if ( isset( $join_conds_safe[$alias] ) && isset( $use_index_safe[$alias] ) ) { + $tableClause = $join_conds_safe[$alias][0] . ' ' . $this->tableNameWithAlias( $table, $alias ); + $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$alias] ) ); + $on = $this->makeList( (array)$join_conds_safe[$alias][1], LIST_AND ); + if ( $on != '' ) { + $tableClause .= ' ON (' . $on . ')'; + } + $retJOIN[] = $tableClause; // Is there an INDEX clause? - } else if ( isset($use_index_safe[$table]) ) { - $tableClause = $this->tableName( $table ); - $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) ); + } else if ( isset( $use_index_safe[$alias] ) ) { + $tableClause = $this->tableNameWithAlias( $table, $alias ); + $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$alias] ) ); $ret[] = $tableClause; // Is there a JOIN clause? - } else if ( isset($join_conds_safe[$table]) ) { - $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table ); - $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')'; + } else if ( isset( $join_conds_safe[$alias] ) ) { + $tableClause = $join_conds_safe[$alias][0] . ' ' . $this->tableNameWithAlias( $table, $alias ); + $on = $this->makeList( (array)$join_conds_safe[$alias][1], LIST_AND ); + if ( $on != '' ) { + $tableClause .= ' ON (' . $on . ')'; + } + $retJOIN[] = $tableClause; } else { - $tableClause = $this->tableName( $table ); + $tableClause = $this->tableNameWithAlias( $table, $alias ); $ret[] = $tableClause; } } + // We can't separate explicit JOIN clauses with ',', use ' ' for those - $straightJoins = !empty($ret) ? implode( ',', $ret ) : ""; - $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : ""; + $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : ""; + $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : ""; + // Compile our final table clause - return implode(' ',array($straightJoins,$otherJoins) ); + return implode( ' ', array( $straightJoins, $otherJoins ) ); } /** @@ -1464,20 +1711,14 @@ abstract class DatabaseBase { 'un_user_id' => 'user_id', 'un_user_ip' => 'user_ip', ); - if( isset( $renamed[$index] ) ) { + + if ( isset( $renamed[$index] ) ) { return $renamed[$index]; } else { return $index; } } - /** - * Wrapper for addslashes() - * @param $s String: to be slashed. - * @return String: slashed string. - */ - abstract function strencode( $s ); - /** * If it's a string, adds quotes and backslashes * Otherwise returns as-is @@ -1494,15 +1735,43 @@ abstract class DatabaseBase { } } + /** + * Quotes an identifier using `backticks` or "double quotes" depending on the database type. + * MySQL uses `backticks` while basically everything else uses double quotes. + * Since MySQL is the odd one out here the double quotes are our generic + * and we implement backticks in DatabaseMysql. + */ + public function addIdentifierQuotes( $s ) { + return '"' . str_replace( '"', '""', $s ) . '"'; + } + + /** + * Backwards compatibility, identifier quoting originated in DatabasePostgres + * which used quote_ident which does not follow our naming conventions + * was renamed to addIdentifierQuotes. + * @deprecated use addIdentifierQuotes + */ + function quote_ident( $s ) { + wfDeprecated( __METHOD__ ); + 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 in 1.17, warnings in 1.17, removed in ??? */ - function escapeLike( $s ) { + public function escapeLike( $s ) { + wfDeprecated( __METHOD__ ); + return $this->escapeLikeInternal( $s ); + } + + protected function escapeLikeInternal( $s ) { $s = str_replace( '\\', '\\\\', $s ); $s = $this->strencode( $s ); $s = str_replace( array( '%', '_' ), array( '\%', '\_' ), $s ); + return $s; } @@ -1510,27 +1779,31 @@ abstract class DatabaseBase { * LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match * containing either string literals that will be escaped or tokens returned by anyChar() or anyString(). * Alternatively, the function could be provided with an array of aforementioned parameters. - * + * * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns a LIKE clause that searches * for subpages of 'My page title'. * Alternatively: $pattern = array( 'My_page_title/', $dbr->anyString() ); $query .= $dbr->buildLike( $pattern ); * - * @ return String: fully built LIKE statement + * @since 1.16 + * @return String: fully built LIKE statement */ function buildLike() { $params = func_get_args(); - if (count($params) > 0 && is_array($params[0])) { + + if ( count( $params ) > 0 && is_array( $params[0] ) ) { $params = $params[0]; } $s = ''; - foreach( $params as $value) { - if( $value instanceof LikeMatch ) { + + foreach ( $params as $value ) { + if ( $value instanceof LikeMatch ) { $s .= $value->toString(); } else { - $s .= $this->escapeLike( $value ); + $s .= $this->escapeLikeInternal( $value ); } } + return " LIKE '" . $s . "' "; } @@ -1580,9 +1853,12 @@ abstract class DatabaseBase { * However if you do this, you run the risk of encountering errors which wouldn't have * occurred in MySQL * - * @todo migrate comment to phodocumentor format + * @param $table String: The table to replace the row(s) in. + * @param $uniqueIndexes Array: An associative array of indexes + * @param $rows Array: Array of rows to replace + * @param $fname String: Calling function name (use __METHOD__) for logs/profiling */ - function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) { + function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseBase::replace' ) { $table = $this->tableName( $table ); # Single row case @@ -1590,16 +1866,19 @@ abstract class DatabaseBase { $rows = array( $rows ); } - $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES '; + $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES '; $first = true; + foreach ( $rows as $row ) { if ( $first ) { $first = false; } else { $sql .= ','; } + $sql .= '(' . $this->makeList( $row ) . ')'; } + return $this->query( $sql, $fname ); } @@ -1619,14 +1898,15 @@ abstract class DatabaseBase { * @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause * @param $fname String: Calling function name (use __METHOD__) for logs/profiling */ - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) { + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseBase::deleteJoin' ) { if ( !$conds ) { - throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' ); + throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' ); } $delTable = $this->tableName( $delTable ); $joinTable = $this->tableName( $joinTable ); $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar "; + if ( $conds != '*' ) { $sql .= ' AND ' . $this->makeList( $conds, LIST_AND ); } @@ -1640,16 +1920,17 @@ abstract class DatabaseBase { function textFieldSize( $table, $field ) { $table = $this->tableName( $table ); $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";"; - $res = $this->query( $sql, 'Database::textFieldSize' ); + $res = $this->query( $sql, 'DatabaseBase::textFieldSize' ); $row = $this->fetchObject( $res ); - $this->freeResult( $res ); $m = array(); + if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) { $size = $m[1]; } else { $size = -1; } + return $size; } @@ -1669,48 +1950,59 @@ abstract class DatabaseBase { * * Use $conds == "*" to delete all rows */ - function delete( $table, $conds, $fname = 'Database::delete' ) { + function delete( $table, $conds, $fname = 'DatabaseBase::delete' ) { if ( !$conds ) { - throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' ); + throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' ); } + $table = $this->tableName( $table ); $sql = "DELETE FROM $table"; + if ( $conds != '*' ) { $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); } + return $this->query( $sql, $fname ); } /** * INSERT SELECT wrapper * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...) - * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() + * Source items may be literals rather than field names, but strings should be quoted with DatabaseBase::addQuotes() * $conds may be "*" to copy the whole table * srcTable may be an array of tables. */ - function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect', + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseBase::insertSelect', $insertOptions = array(), $selectOptions = array() ) { $destTable = $this->tableName( $destTable ); + if ( is_array( $insertOptions ) ) { $insertOptions = implode( ' ', $insertOptions ); } - if( !is_array( $selectOptions ) ) { + + if ( !is_array( $selectOptions ) ) { $selectOptions = array( $selectOptions ); } + list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions ); - if( is_array( $srcTable ) ) { + + if ( is_array( $srcTable ) ) { $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); } else { $srcTable = $this->tableName( $srcTable ); } + $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . " SELECT $startOpts " . implode( ',', $varMap ) . " FROM $srcTable $useIndex "; + if ( $conds != '*' ) { $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); } + $sql .= " $tailOpts"; + return $this->query( $sql, $fname ); } @@ -1732,20 +2024,22 @@ abstract class DatabaseBase { * @param $limit Integer: the SQL limit * @param $offset Integer the SQL offset (default false) */ - function limitResult( $sql, $limit, $offset=false ) { - if( !is_numeric( $limit ) ) { + 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}," : "" ) + . ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" ) . "{$limit} "; } + function limitResultForUpdate( $sql, $num ) { return $this->limitResult( $sql, $num, 0 ); } /** - * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries + * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries * within the UNION construct. * @return Boolean */ @@ -1761,9 +2055,9 @@ abstract class DatabaseBase { * @param $all Boolean: use UNION ALL * @return String: SQL fragment */ - function unionQueries($sqls, $all) { + function unionQueries( $sqls, $all ) { $glue = $all ? ') UNION ALL (' : ') UNION ('; - return '('.implode( $glue, $sqls ) . ')'; + return '(' . implode( $glue, $sqls ) . ')'; } /** @@ -1791,6 +2085,16 @@ abstract class DatabaseBase { return "REPLACE({$orig}, {$old}, {$new})"; } + /** + * Convert a field to an unix timestamp + * + * @param $field String: field name + * @return String: SQL statement + */ + public function unixTimestamp( $field ) { + return "EXTRACT(epoch FROM $field)"; + } + /** * Determines if the last failure was due to a deadlock * STUB @@ -1800,7 +2104,7 @@ abstract class DatabaseBase { } /** - * Determines if the last query error was something that should be dealt + * Determines if the last query error was something that should be dealt * with by pinging the connection and reissuing the query. * STUB */ @@ -1833,18 +2137,20 @@ abstract class DatabaseBase { * reached. */ function deadlockLoop() { - $myFname = 'Database::deadlockLoop'; + $myFname = 'DatabaseBase::deadlockLoop'; $this->begin(); $args = func_get_args(); $function = array_shift( $args ); $oldIgnore = $this->ignoreErrors( true ); $tries = DEADLOCK_TRIES; + if ( is_array( $function ) ) { $fname = $function[0]; } else { $fname = $function; } + do { $retVal = call_user_func_array( $function, $args ); $error = $this->lastError(); @@ -1859,14 +2165,16 @@ abstract class DatabaseBase { $this->reportQueryError( $error, $errno, $sql, $fname ); } } - } while( $this->wasDeadlock() && --$tries > 0 ); + } while ( $this->wasDeadlock() && --$tries > 0 ); + $this->ignoreErrors( $oldIgnore ); + if ( $tries <= 0 ) { - $this->query( 'ROLLBACK', $myFname ); + $this->rollback( $myFname ); $this->reportQueryError( $error, $errno, $sql, $fname ); return false; } else { - $this->query( 'COMMIT', $myFname ); + $this->commit( $myFname ); return $retVal; } } @@ -1878,7 +2186,7 @@ abstract class DatabaseBase { * @param $timeout Integer: the maximum number of seconds to wait for synchronisation */ function masterPosWait( MySQLMasterPos $pos, $timeout ) { - $fname = 'Database::masterPosWait'; + $fname = 'DatabaseBase::masterPosWait'; wfProfileIn( $fname ); # Commit any open transactions @@ -1887,7 +2195,8 @@ abstract class DatabaseBase { } if ( !is_null( $this->mFakeSlaveLag ) ) { - $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 ); + $wait = intval( ( $pos->pos - microtime( true ) + $this->mFakeSlaveLag ) * 1e6 ); + if ( $wait > $timeout * 1e6 ) { wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" ); wfProfileOut( $fname ); @@ -1909,8 +2218,8 @@ abstract class DatabaseBase { $encPos = intval( $pos->pos ); $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)"; $res = $this->doQuery( $sql ); + if ( $res && $row = $this->fetchRow( $res ) ) { - $this->freeResult( $res ); wfProfileOut( $fname ); return $row[0]; } else { @@ -1924,14 +2233,16 @@ abstract class DatabaseBase { */ function getSlavePos() { if ( !is_null( $this->mFakeSlaveLag ) ) { - $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag ); - wfDebug( __METHOD__.": fake slave pos = $pos\n" ); + $pos = new MySQLMasterPos( 'fake', microtime( true ) - $this->mFakeSlaveLag ); + wfDebug( __METHOD__ . ": fake slave pos = $pos\n" ); return $pos; } - $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' ); + + $res = $this->query( 'SHOW SLAVE STATUS', 'DatabaseBase::getSlavePos' ); $row = $this->fetchObject( $res ); + if ( $row ) { - $pos = isset($row->Exec_master_log_pos) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos; + $pos = isset( $row->Exec_master_log_pos ) ? $row->Exec_master_log_pos : $row->Exec_Master_Log_Pos; return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos ); } else { return false; @@ -1945,8 +2256,10 @@ abstract class DatabaseBase { if ( $this->mFakeMaster ) { return new MySQLMasterPos( 'fake', microtime( true ) ); } - $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' ); + + $res = $this->query( 'SHOW MASTER STATUS', 'DatabaseBase::getMasterPos' ); $row = $this->fetchObject( $res ); + if ( $row ) { return new MySQLMasterPos( $row->File, $row->Position ); } else { @@ -1957,7 +2270,7 @@ abstract class DatabaseBase { /** * Begin a transaction, committing any previously open transaction */ - function begin( $fname = 'Database::begin' ) { + function begin( $fname = 'DatabaseBase::begin' ) { $this->query( 'BEGIN', $fname ); $this->mTrxLevel = 1; } @@ -1965,25 +2278,30 @@ abstract class DatabaseBase { /** * End a transaction */ - function commit( $fname = 'Database::commit' ) { - $this->query( 'COMMIT', $fname ); - $this->mTrxLevel = 0; + function commit( $fname = 'DatabaseBase::commit' ) { + if ( $this->mTrxLevel ) { + $this->query( 'COMMIT', $fname ); + $this->mTrxLevel = 0; + } } /** * Rollback a transaction. * No-op on non-transactional databases. */ - function rollback( $fname = 'Database::rollback' ) { - $this->query( 'ROLLBACK', $fname, true ); - $this->mTrxLevel = 0; + function rollback( $fname = 'DatabaseBase::rollback' ) { + if ( $this->mTrxLevel ) { + $this->query( 'ROLLBACK', $fname, true ); + $this->mTrxLevel = 0; + } } /** * Begin a transaction, committing any previously open transaction * @deprecated use begin() */ - function immediateBegin( $fname = 'Database::immediateBegin' ) { + function immediateBegin( $fname = 'DatabaseBase::immediateBegin' ) { + wfDeprecated( __METHOD__ ); $this->begin(); } @@ -1991,7 +2309,8 @@ abstract class DatabaseBase { * Commit transaction, if one is open * @deprecated use commit() */ - function immediateCommit( $fname = 'Database::immediateCommit' ) { + function immediateCommit( $fname = 'DatabaseBase::immediateCommit' ) { + wfDeprecated( __METHOD__ ); $this->commit(); } @@ -2004,24 +2323,25 @@ abstract class DatabaseBase { * @param $oldName String: name of table whose structure should be copied * @param $newName String: name of table to be created * @param $temporary Boolean: whether the new table should be temporary + * @param $fname String: calling function name * @return Boolean: true if operation was successful */ - function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'Database::duplicateTableStructure' ) { + function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseBase::duplicateTableStructure' ) { throw new MWException( 'DatabaseBase::duplicateTableStructure is not implemented in descendant class' ); } /** * Return MW-style timestamp used for MySQL schema */ - function timestamp( $ts=0 ) { - return wfTimestamp(TS_MW,$ts); + function timestamp( $ts = 0 ) { + return wfTimestamp( TS_MW, $ts ); } /** * Local database timestamp format or null */ function timestampOrNull( $ts = null ) { - if( is_null( $ts ) ) { + if ( is_null( $ts ) ) { return null; } else { return $this->timestamp( $ts ); @@ -2032,7 +2352,7 @@ abstract class DatabaseBase { * @todo document */ function resultObject( $result ) { - if( empty( $result ) ) { + if ( empty( $result ) ) { return false; } elseif ( $result instanceof ResultWrapper ) { return $result; @@ -2047,28 +2367,10 @@ abstract class DatabaseBase { /** * Return aggregated value alias */ - function aggregateValue ($valuedata,$valuename='value') { + function aggregateValue ( $valuedata, $valuename = 'value' ) { return $valuename; } - /** - * Returns a wikitext link to the DB's website, e.g., - * return "[http://www.mysql.com/ MySQL]"; - * Should at least contain plain text, if for some reason - * your database has no website. - * - * @return String: wikitext of a link to the server software's web site - */ - abstract function getSoftwareLink(); - - /** - * A string describing the current software version, like from - * mysql_get_server_info(). Will be listed on Special:Version, etc. - * - * @return String: Version information from the database - */ - abstract function getServerVersion(); - /** * Ping the server and try to reconnect if it there is no connection * @@ -2081,52 +2383,24 @@ abstract class DatabaseBase { /** * Get slave lag. - * At the moment, this will only work if the DB user has the PROCESS privilege + * Currently supported only by MySQL + * @return Database replication lag in seconds */ function getLag() { - if ( !is_null( $this->mFakeSlaveLag ) ) { - wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" ); - return $this->mFakeSlaveLag; - } - $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ ); - # Find slave SQL thread - while ( $row = $this->fetchObject( $res ) ) { - /* This should work for most situations - when default db - * for thread is not specified, it had no events executed, - * and therefore it doesn't know yet how lagged it is. - * - * Relay log I/O thread does not select databases. - */ - if ( $row->User == 'system user' && - $row->State != 'Waiting for master to send event' && - $row->State != 'Connecting to master' && - $row->State != 'Queueing master event to the relay log' && - $row->State != 'Waiting for master update' && - $row->State != 'Requesting binlog dump' && - $row->State != 'Waiting to reconnect after a failed master event read' && - $row->State != 'Reconnecting after a failed master event read' && - $row->State != 'Registering slave on master' - ) { - # This is it, return the time (except -ve) - if ( $row->Time > 0x7fffffff ) { - return false; - } else { - return $row->Time; - } - } - } - return false; + return intval( $this->mFakeSlaveLag ); } /** * Get status information from SHOW STATUS in an associative array */ - function getStatus($which="%") { + function getStatus( $which = "%" ) { $res = $this->query( "SHOW STATUS LIKE '{$which}'" ); $status = array(); - while ( $row = $this->fetchObject( $res ) ) { + + foreach ( $res as $row ) { $status[$row->Variable_name] = $row->Value; } + return $status; } @@ -2137,11 +2411,11 @@ abstract class DatabaseBase { return 0; } - function encodeBlob($b) { + function encodeBlob( $b ) { return $b; } - function decodeBlob($b) { + function decodeBlob( $b ) { return $b; } @@ -2161,56 +2435,79 @@ abstract class DatabaseBase { * @param $filename String: File name to open * @param $lineCallback Callback: Optional function called before reading each line * @param $resultCallback Callback: Optional function called for each MySQL result + * @param $fname String: Calling function name or false if name should be generated dynamically + * using $filename */ - function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) { + function sourceFile( $filename, $lineCallback = false, $resultCallback = false, $fname = false ) { $fp = fopen( $filename, 'r' ); + if ( false === $fp ) { - if (!defined("MEDIAWIKI_INSTALL")) + if ( !defined( "MEDIAWIKI_INSTALL" ) ) throw new MWException( "Could not open \"{$filename}\".\n" ); else return "Could not open \"{$filename}\".\n"; } + + if ( !$fname ) { + $fname = __METHOD__ . "( $filename )"; + } + try { - $error = $this->sourceStream( $fp, $lineCallback, $resultCallback ); + $error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname ); } - catch( MWException $e ) { - if ( defined("MEDIAWIKI_INSTALL") ) { + catch ( MWException $e ) { + if ( defined( "MEDIAWIKI_INSTALL" ) ) { $error = $e->getMessage(); } else { fclose( $fp ); throw $e; } } - + fclose( $fp ); + return $error; } /** * Get the full path of a patch file. Originally based on archive() - * from updaters.inc. Keep in mind this always returns a patch, as + * from updaters.inc. Keep in mind this always returns a patch, as * it fails back to MySQL if no DB-specific patch can be found * * @param $patch String The name of the patch, like patch-something.sql * @return String Full path to patch file */ - public static function patchPath( $patch ) { - global $wgDBtype, $IP; - if ( file_exists( "$IP/maintenance/$wgDBtype/archives/$patch" ) ) { - return "$IP/maintenance/$wgDBtype/archives/$patch"; + public function patchPath( $patch ) { + global $IP; + + $dbType = $this->getType(); + if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) { + return "$IP/maintenance/$dbType/archives/$patch"; } else { return "$IP/maintenance/archives/$patch"; } } + /** + * Set variables to be used in sourceFile/sourceStream, in preference to the + * ones in $GLOBALS. If an array is set here, $GLOBALS will not be used at + * all. If it's set to false, $GLOBALS will be used. + * + * @param $vars False, or array mapping variable name to value. + */ + function setSchemaVars( $vars ) { + $this->mSchemaVars = $vars; + } + /** * Read and execute commands from an open file handle * Returns true on success, error string or exception on failure (depending on object's error ignore settings) * @param $fp String: File handle * @param $lineCallback Callback: Optional function called before reading each line * @param $resultCallback Callback: Optional function called for each MySQL result + * @param $fname String: Calling function name */ - function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) { + function sourceStream( $fp, $lineCallback = false, $resultCallback = false, $fname = 'DatabaseBase::sourceStream' ) { $cmd = ""; $done = false; $dollarquote = false; @@ -2219,15 +2516,21 @@ abstract class DatabaseBase { if ( $lineCallback ) { call_user_func( $lineCallback ); } + $line = trim( fgets( $fp, 1024 ) ); $sl = strlen( $line ) - 1; - if ( $sl < 0 ) { continue; } - if ( '-' == $line{0} && '-' == $line{1} ) { continue; } + if ( $sl < 0 ) { + continue; + } + + if ( '-' == $line { 0 } && '-' == $line { 1 } ) { + continue; + } - ## Allow dollar quoting for function declarations - if (substr($line,0,4) == '$mw$') { - if ($dollarquote) { + # # Allow dollar quoting for function declarations + if ( substr( $line, 0, 4 ) == '$mw$' ) { + if ( $dollarquote ) { $dollarquote = false; $done = true; } @@ -2235,20 +2538,24 @@ abstract class DatabaseBase { $dollarquote = true; } } - else if (!$dollarquote) { - if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) { + else if ( !$dollarquote ) { + if ( ';' == $line { $sl } && ( $sl < 2 || ';' != $line { $sl - 1 } ) ) { $done = true; $line = substr( $line, 0, $sl ); } } - if ( $cmd != '' ) { $cmd .= ' '; } + if ( $cmd != '' ) { + $cmd .= ' '; + } + $cmd .= "$line\n"; if ( $done ) { - $cmd = str_replace(';;', ";", $cmd); + $cmd = str_replace( ';;', ";", $cmd ); $cmd = $this->replaceVars( $cmd ); - $res = $this->query( $cmd, __METHOD__ ); + $res = $this->query( $cmd, $fname ); + if ( $resultCallback ) { call_user_func( $resultCallback, $res, $this ); } @@ -2262,40 +2569,75 @@ abstract class DatabaseBase { $done = false; } } + return true; } + /** + * Database independent variable replacement, replaces a set of variables + * in a sql statement with their contents as given by $this->getSchemaVars(). + * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables + * + * '{$var}' should be used for text and is passed through the database's addQuotes method + * `{$var}` should be used for identifiers (eg: table and database names), it is passed through + * the database's addIdentifierQuotes method which can be overridden if the database + * uses something other than backticks. + * / *$var* / is just encoded, besides traditional dbprefix and tableoptions it's use should be avoided + * + * @param $ins String: SQL statement to replace variables in + * @return String The new SQL statement with variables replaced + */ + protected function replaceSchemaVars( $ins ) { + $vars = $this->getSchemaVars(); + foreach ( $vars as $var => $value ) { + // replace '{$var}' + $ins = str_replace( '\'{$' . $var . '}\'', $this->addQuotes( $value ), $ins ); + // replace `{$var}` + $ins = str_replace( '`{$' . $var . '}`', $this->addIdentifierQuotes( $value ), $ins ); + // replace /*$var*/ + $ins = str_replace( '/*$' . $var . '*/', $this->strencode( $value ) , $ins ); + } + return $ins; + } /** * Replace variables in sourced SQL */ protected function replaceVars( $ins ) { - $varnames = array( - 'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser', - 'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword', - 'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions', - ); - - // Ordinary variables - foreach ( $varnames as $var ) { - if( isset( $GLOBALS[$var] ) ) { - $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check? - $ins = str_replace( '{$' . $var . '}', $val, $ins ); - $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins ); - $ins = str_replace( '/*$' . $var . '*/', $val, $ins ); - } - } + $ins = $this->replaceSchemaVars( $ins ); // Table prefixes $ins = preg_replace_callback( '!/\*(?:\$wgDBprefix|_)\*/([a-zA-Z_0-9]*)!', array( $this, 'tableNameCallback' ), $ins ); // Index names - $ins = preg_replace_callback( '!/\*i\*/([a-zA-Z_0-9]*)!', + $ins = preg_replace_callback( '!/\*i\*/([a-zA-Z_0-9]*)!', array( $this, 'indexNameCallback' ), $ins ); + return $ins; } + /** + * Get schema variables. If none have been set via setSchemaVars(), then + * use some defaults from the current object. + */ + protected function getSchemaVars() { + if ( $this->mSchemaVars ) { + return $this->mSchemaVars; + } else { + return $this->getDefaultSchemaVars(); + } + } + + /** + * Get schema variables to use if none have been set via setSchemaVars(). + * Override this in derived classes to provide variables for tables.sql + * and SQL patch files. + */ + protected function getDefaultSchemaVars() { + return array(); + } + /** * Table name callback * @private @@ -2319,16 +2661,17 @@ abstract class DatabaseBase { function buildConcat( $stringList ) { return 'CONCAT(' . implode( ',', $stringList ) . ')'; } - + /** * Acquire a named lock - * + * * Abstracted from Filestore::lock() so child classes can implement for * their own needs. - * - * @param $lockName String: Name of lock to aquire - * @param $method String: Name of method calling us - * @return bool + * + * @param $lockName String: name of lock to aquire + * @param $method String: name of method calling us + * @param $timeout Integer: timeout + * @return Boolean */ public function lock( $lockName, $method, $timeout = 5 ) { return true; @@ -2336,13 +2679,12 @@ abstract class DatabaseBase { /** * Release a lock. - * + * * @param $lockName String: Name of lock to release * @param $method String: Name of method calling us * - * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock * @return 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 + * by this thread (in which case the lock is not released), and NULL if the named * lock did not exist */ public function unlock( $lockName, $method ) { @@ -2360,7 +2702,7 @@ abstract class DatabaseBase { public function lockTables( $read, $write, $method, $lowPriority = true ) { return true; } - + /** * Unlock specific tables * @@ -2369,31 +2711,30 @@ abstract class DatabaseBase { public function unlockTables( $method ) { return true; } - + /** - * Get search engine class. All subclasses of this - * need to implement this if they wish to use searching. - * + * Get search engine class. All subclasses of this need to implement this + * if they wish to use searching. + * * @return String */ public function getSearchEngine() { - return "SearchMySQL"; + return 'SearchEngineDummy'; } /** - * Allow or deny "big selects" for this session only. This is done by setting + * Allow or deny "big selects" for this session only. This is done by setting * the sql_big_selects session variable. * - * This is a MySQL-specific feature. + * This is a MySQL-specific feature. * - * @param mixed $value true for allow, false for deny, or "default" to restore the initial value + * @param $value Mixed: true for allow, false for deny, or "default" to restore the initial value */ public function setBigSelects( $value = true ) { // no-op } } - /****************************************************************************** * Utility classes *****************************************************************************/ @@ -2405,7 +2746,7 @@ abstract class DatabaseBase { class DBObject { public $mData; - function DBObject($data) { + function __construct( $data ) { $this->mData = $data; } @@ -2426,65 +2767,44 @@ class DBObject { */ class Blob { private $mData; - function __construct($data) { + + function __construct( $data ) { $this->mData = $data; } + function fetch() { return $this->mData; } } /** - * Utility class. + * Base for all database-specific classes representing information about database fields * @ingroup Database */ -class MySQLField { - private $name, $tablename, $default, $max_length, $nullable, - $is_pk, $is_unique, $is_multiple, $is_key, $type; - function __construct ($info) { - $this->name = $info->name; - $this->tablename = $info->table; - $this->default = $info->def; - $this->max_length = $info->max_length; - $this->nullable = !$info->not_null; - $this->is_pk = $info->primary_key; - $this->is_unique = $info->unique_key; - $this->is_multiple = $info->multiple_key; - $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple); - $this->type = $info->type; - } - - function name() { - return $this->name; - } - - function tableName() { - return $this->tableName; - } - - function defaultValue() { - return $this->default; - } - - function maxLength() { - return $this->max_length; - } - - function nullable() { - return $this->nullable; - } +interface Field { + /** + * Field name + * @return string + */ + function name(); - function isKey() { - return $this->is_key; - } + /** + * Name of table this field belongs to + * @return string + */ + function tableName(); - function isMultipleKey() { - return $this->is_multiple; - } + /** + * Database type + * @return string + */ + function type(); - function type() { - return $this->type; - } + /** + * Whether this field can store NULL values + * @return bool + */ + function isNullable(); } /****************************************************************************** @@ -2510,10 +2830,13 @@ class DBError extends MWException { function getText() { global $wgShowDBErrorBacktrace; + $s = $this->getMessage() . "\n"; + if ( $wgShowDBErrorBacktrace ) { $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n"; } + return $s; } } @@ -2523,13 +2846,16 @@ class DBError extends MWException { */ class DBConnectionError extends DBError { public $error; - + function __construct( DatabaseBase &$db, $error = 'unknown error' ) { $msg = 'DB connection error'; + if ( trim( $error ) != '' ) { $msg .= ": $error"; } + $this->error = $error; + parent::__construct( $db, $msg ); } @@ -2542,7 +2868,7 @@ class DBConnectionError extends DBError { // Not likely to work return false; } - + function getLogMessage() { # Don't send to the exception log return false; @@ -2550,11 +2876,13 @@ class DBConnectionError extends DBError { function getPageTitle() { global $wgSitename, $wgLang; + $header = "$wgSitename has a problem"; + if ( $wgLang instanceof Language ) { $header = htmlspecialchars( $wgLang->getMessage( 'dberr-header' ) ); } - + return $header; } @@ -2577,7 +2905,7 @@ class DBConnectionError extends DBError { } if ( trim( $this->error ) == '' ) { - $this->error = $this->db->getProperty('mServer'); + $this->error = $this->db->getProperty( 'mServer' ); } $noconnect = "

$sorry
$again

$info

"; @@ -2589,33 +2917,38 @@ class DBConnectionError extends DBError { $extra = $this->searchForm(); - if( $wgUseFileCache ) { + if ( $wgUseFileCache ) { try { $cache = $this->fileCachedPage(); # Cached version on file system? - if( $cache !== null ) { + if ( $cache !== null ) { # Hack: extend the body for error messages - $cache = str_replace( array('',''), '', $cache ); + $cache = str_replace( array( '', '' ), '', $cache ); # Add cache notice... $cachederror = "This is a cached copy of the requested page, and may not be up to date. "; + # Localize it if possible... - if( $wgLang instanceof Language ) { + if ( $wgLang instanceof Language ) { $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) ); } + $warning = "
$cachederror
"; + # Output cached page with notices on bottom and re-close body return "{$cache}{$warning}
$text
$extra"; } - } catch( MWException $e ) { + } catch ( MWException $e ) { // Do nothing, just use the default page } } + # Headers needed here - output is just the error message - return $this->htmlHeader()."$text
$extra".$this->htmlFooter(); + return $this->htmlHeader() . "$text
$extra" . $this->htmlFooter(); } function searchForm() { - global $wgSitename, $wgServer, $wgLang, $wgInputEncoding; + global $wgSitename, $wgServer, $wgLang; + $usegoogle = "You can try searching via Google in the meantime."; $outofdate = "Note that their indexes of our content may be out of date."; $googlesearch = "Search"; @@ -2626,23 +2959,26 @@ class DBConnectionError extends DBError { $googlesearch = htmlspecialchars( $wgLang->getMessage( 'searchbutton' ) ); } - $search = htmlspecialchars(@$_REQUEST['search']); + $search = htmlspecialchars( @$_REQUEST['search'] ); + + $server = htmlspecialchars( $wgServer ); + $sitename = htmlspecialchars( $wgSitename ); $trygoogle = <<$usegoogle
$outofdate
- - - - + + + + - - + +
- - + +
@@ -2651,33 +2987,35 @@ EOT; } function fileCachedPage() { - global $wgTitle, $title, $wgLang, $wgOut; - if( $wgOut->isDisabled() ) return; // Done already? + global $wgTitle, $wgLang, $wgOut; + + if ( $wgOut->isDisabled() ) { + return; // Done already? + } + $mainpage = 'Main Page'; + if ( $wgLang instanceof Language ) { $mainpage = htmlspecialchars( $wgLang->getMessage( 'mainpage' ) ); } - if( $wgTitle ) { + if ( $wgTitle ) { $t =& $wgTitle; - } elseif( $title ) { - $t = Title::newFromURL( $title ); } else { $t = Title::newFromText( $mainpage ); } $cache = new HTMLFileCache( $t ); - if( $cache->isFileCached() ) { + if ( $cache->isFileCached() ) { return $cache->fetchPageText(); } else { return ''; } } - + function htmlBodyOnly() { return true; } - } /** @@ -2685,14 +3023,15 @@ EOT; */ class DBQueryError extends DBError { public $error, $errno, $sql, $fname; - + function __construct( DatabaseBase &$db, $error, $errno, $sql, $fname ) { - $message = "A database error has occurred\n" . + $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . "Query: $sql\n" . "Function: $fname\n" . "Error: $errno $error\n"; parent::__construct( $db, $message ); + $this->error = $error; $this->errno = $errno; $this->sql = $sql; @@ -2701,27 +3040,31 @@ class DBQueryError extends DBError { function getText() { global $wgShowDBErrorBacktrace; + if ( $this->useMessageCache() ) { $s = wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ), htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n"; + if ( $wgShowDBErrorBacktrace ) { $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n"; } + return $s; } else { return parent::getText(); } } - + function getSQL() { global $wgShowSQLErrors; - if( !$wgShowSQLErrors ) { + + if ( !$wgShowSQLErrors ) { return $this->msg( 'sqlhidden', 'SQL hidden' ); } else { return $this->sql; } } - + function getLogMessage() { # Don't send to the exception log return false; @@ -2733,15 +3076,18 @@ class DBQueryError extends DBError { function getHTML() { global $wgShowDBErrorBacktrace; + if ( $this->useMessageCache() ) { $s = wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ), htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ); } else { $s = nl2br( htmlspecialchars( $this->getMessage() ) ); } + if ( $wgShowDBErrorBacktrace ) { $s .= '

Backtrace:

' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ); } + return $s; } } @@ -2762,8 +3108,9 @@ class ResultWrapper implements Iterator { /** * Create a new result object from a result resource and a Database object */ - function ResultWrapper( $database, $result ) { + function __construct( $database, $result ) { $this->db = $database; + if ( $result instanceof ResultWrapper ) { $this->result = $result->result; } else { @@ -2783,7 +3130,6 @@ class ResultWrapper implements Iterator { * Fields can be retrieved with $row->fieldname, with fields acting like * member variables. * - * @param $res SQL result object as returned from Database::query(), etc. * @return MySQL row object * @throws DBUnexpectedError Thrown if the database returns an error */ @@ -2795,7 +3141,6 @@ class ResultWrapper implements Iterator { * 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 MySQL row object * @throws DBUnexpectedError Thrown if the database returns an error */ @@ -2827,8 +3172,8 @@ class ResultWrapper implements Iterator { */ function rewind() { - if ($this->numRows()) { - $this->db->dataSeek($this, 0); + if ( $this->numRows() ) { + $this->db->dataSeek( $this, 0 ); } $this->pos = 0; $this->currentRow = null; @@ -2856,9 +3201,50 @@ class ResultWrapper implements Iterator { } } +/** + * Overloads the relevant methods of the real ResultsWrapper so it + * doesn't go anywhere near an actual database. + */ +class FakeResultWrapper extends ResultWrapper { + var $result = array(); + var $db = null; // And it's going to stay that way :D + var $pos = 0; + var $currentRow = null; + + function __construct( $array ) { + $this->result = $array; + } + + function numRows() { + return count( $this->result ); + } + + function fetchRow() { + $this->currentRow = $this->result[$this->pos++]; + return $this->currentRow; + } + + function seek( $row ) { + $this->pos = $row; + } + + function free() {} + + // Callers want to be able to access fields with $this->fieldName + function fetchObject() { + $this->currentRow = $this->result[$this->pos++]; + return (object)$this->currentRow; + } + + function rewind() { + $this->pos = 0; + $this->currentRow = null; + } +} + /** * Used by DatabaseBase::buildLike() to represent characters that have special meaning in SQL LIKE clauses - * and thus need no escaping. Don't instantiate it manually, use Database::anyChar() and anyString() instead. + * and thus need no escaping. Don't instantiate it manually, use DatabaseBase::anyChar() and anyString() instead. */ class LikeMatch { private $str; diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php index 9b62af82..becca11e 100644 --- a/includes/db/DatabaseIbm_db2.php +++ b/includes/db/DatabaseIbm_db2.php @@ -1,10 +1,11 @@ query(sprintf($q, - $db->addQuotes($wgDBmwschema), - $db->addQuotes($table), - $db->addQuotes($field))); - $row = $db->fetchObject($res); - if (!$row) + $res = $db->query( + sprintf( $q, + $db->addQuotes( $wgDBmwschema ), + $db->addQuotes( $table ), + $db->addQuotes( $field ) + ) + ); + $row = $db->fetchObject( $res ); + if ( !$row ) { return null; + } $n = new IBM_DB2Field; $n->type = $row->typname; - $n->nullable = ($row->attnotnull == 'N'); + $n->nullable = ( $row->attnotnull == 'N' ); $n->name = $field; $n->tablename = $table; $n->max_length = $row->attlen; @@ -70,7 +75,7 @@ SQL; * Can column be null? * @return bool true or false */ - function nullable() { return $this->nullable; } + function isNullable() { return $this->nullable; } /** * How much can you fit in the column per row? * @return int length @@ -85,18 +90,17 @@ SQL; class IBM_DB2Blob { private $mData; - public function __construct($data) { + public function __construct( $data ) { $this->mData = $data; } public function getData() { return $this->mData; } - - public function __toString() - { - return $this->mData; - } + + public function __toString() { + return $this->mData; + } } /** @@ -112,7 +116,6 @@ class DatabaseIbm_db2 extends DatabaseBase { protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname; protected $mOut, $mOpened = false; - protected $mFailFunction; protected $mTablePrefix; protected $mFlags; protected $mTrxLevel = 0; @@ -121,207 +124,50 @@ class DatabaseIbm_db2 extends DatabaseBase { protected $mFakeSlaveLag = null, $mFakeMaster = false; * */ - - /// Server port for uncataloged connections + + /** Database server port */ protected $mPort = null; - /// Whether connection is cataloged - protected $mCataloged = null; - /// Schema for tables, stored procedures, triggers + /** Schema for tables, stored procedures, triggers */ protected $mSchema = null; - /// Whether the schema has been applied in this session + /** Whether the schema has been applied in this session */ protected $mSchemaSet = false; - /// Result of last query + /** Result of last query */ protected $mLastResult = null; - /// Number of rows affected by last INSERT/UPDATE/DELETE + /** Number of rows affected by last INSERT/UPDATE/DELETE */ protected $mAffectedRows = null; - /// Number of rows returned by last SELECT + /** Number of rows returned by last SELECT */ protected $mNumRows = null; - - /// Connection config options - see constructor + + /** Connection config options - see constructor */ public $mConnOptions = array(); - /// Statement config options -- see constructor + /** Statement config options -- see constructor */ public $mStmtOptions = array(); - - - const CATALOGED = "cataloged"; - const UNCATALOGED = "uncataloged"; - const USE_GLOBAL = "get from global"; - + + /** Default schema */ + const USE_GLOBAL = 'mediawiki'; + + /** Option that applies to nothing */ const NONE_OPTION = 0x00; + /** Option that applies to connection objects */ const CONN_OPTION = 0x01; + /** Option that applies to statement objects */ const STMT_OPTION = 0x02; - + + /** Regular operation mode -- minimal debug messages */ const REGULAR_MODE = 'regular'; + /** Installation mode -- lots of debug messages */ const INSTALL_MODE = 'install'; - - // Whether this is regular operation or the initial installation + + /** Controls the level of debug message output */ protected $mMode = self::REGULAR_MODE; - - /// Last sequence value used for a primary key + + /** Last sequence value used for a primary key */ protected $mInsertId = null; - - /* - * These can be safely inherited - * - * Getter/Setter: (18) - * failFunction - * setOutputPage - * bufferResults - * ignoreErrors - * trxLevel - * errorCount - * getLBInfo - * setLBInfo - * lastQuery - * isOpen - * setFlag - * clearFlag - * getFlag - * getProperty - * getDBname - * getServer - * tableNameCallback - * tablePrefix - * - * Administrative: (8) - * debug - * installErrorHandler - * restoreErrorHandler - * connectionErrorHandler - * reportConnectionError - * sourceFile - * sourceStream - * replaceVars - * - * Database: (5) - * query - * set - * selectField - * generalizeSQL - * update - * strreplace - * deadlockLoop - * - * Prepared Statement: 6 - * prepare - * freePrepared - * execute - * safeQuery - * fillPrepared - * fillPreparedArg - * - * Slave/Master: (4) - * masterPosWait - * getSlavePos - * getMasterPos - * getLag - * - * Generation: (9) - * tableNames - * tableNamesN - * tableNamesWithUseIndexOrJOIN - * escapeLike - * delete - * insertSelect - * timestampOrNull - * resultObject - * aggregateValue - * selectSQLText - * selectRow - * makeUpdateOptions - * - * Reflection: (1) - * indexExists - */ - - /* - * These have been implemented - * - * Administrative: 7 / 7 - * constructor [Done] - * open [Done] - * openCataloged [Done] - * close [Done] - * newFromParams [Done] - * openUncataloged [Done] - * setup_database [Done] - * - * Getter/Setter: 13 / 13 - * cascadingDeletes [Done] - * cleanupTriggers [Done] - * strictIPs [Done] - * realTimestamps [Done] - * impliciGroupby [Done] - * implicitOrderby [Done] - * searchableIPs [Done] - * functionalIndexes [Done] - * getWikiID [Done] - * isOpen [Done] - * getServerVersion [Done] - * getSoftwareLink [Done] - * getSearchEngine [Done] - * - * Database driver wrapper: 23 / 23 - * lastError [Done] - * lastErrno [Done] - * doQuery [Done] - * tableExists [Done] - * fetchObject [Done] - * fetchRow [Done] - * freeResult [Done] - * numRows [Done] - * numFields [Done] - * fieldName [Done] - * insertId [Done] - * dataSeek [Done] - * affectedRows [Done] - * selectDB [Done] - * strencode [Done] - * conditional [Done] - * wasDeadlock [Done] - * ping [Done] - * getStatus [Done] - * setTimeout [Done] - * lock [Done] - * unlock [Done] - * insert [Done] - * select [Done] - * - * Slave/master: 2 / 2 - * setFakeSlaveLag [Done] - * setFakeMaster [Done] - * - * Reflection: 6 / 6 - * fieldExists [Done] - * indexInfo [Done] - * fieldInfo [Done] - * fieldType [Done] - * indexUnique [Done] - * textFieldSize [Done] - * - * Generation: 16 / 16 - * tableName [Done] - * addQuotes [Done] - * makeList [Done] - * makeSelectOptions [Done] - * estimateRowCount [Done] - * nextSequenceValue [Done] - * useIndexClause [Done] - * replace [Done] - * deleteJoin [Done] - * lowPriorityOption [Done] - * limitResult [Done] - * limitResultForUpdate [Done] - * timestamp [Done] - * encodeBlob [Done] - * decodeBlob [Done] - * buildConcat [Done] - */ - + ###################################### # Getters and Setters ###################################### - + /** * Returns true if this database supports (and uses) cascading deletes */ @@ -330,20 +176,22 @@ class DatabaseIbm_db2 extends DatabaseBase { } /** - * Returns true if this database supports (and uses) triggers (e.g. on the page table) + * Returns true if this database supports (and uses) triggers (e.g. on the + * page table) */ function cleanupTriggers() { return true; } /** - * Returns true if this database is strict about what can be put into an IP field. + * 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. */ function strictIPs() { return true; } - + /** * Returns true if this database uses timestamps rather than integers */ @@ -359,7 +207,8 @@ class DatabaseIbm_db2 extends DatabaseBase { } /** - * Returns true if this database does an implicit order by when the column has an index + * 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 */ function implicitOrderby() { @@ -380,7 +229,7 @@ class DatabaseIbm_db2 extends DatabaseBase { function functionalIndexes() { return true; } - + /** * Returns a unique string representing the wiki on the server */ @@ -395,153 +244,148 @@ class DatabaseIbm_db2 extends DatabaseBase { function getType() { return 'ibm_db2'; } - - ###################################### - # Setup - ###################################### - - + /** - * + * * @param $server String: hostname of database server * @param $user String: username * @param $password String: password * @param $dbName String: database name on the server - * @param $failFunction Callback (optional) * @param $flags Integer: database behaviour flags (optional, unused) * @param $schema String */ - public function DatabaseIbm_db2($server = false, $user = false, $password = false, - $dbName = false, $failFunction = false, $flags = 0, + public function __construct( $server = false, $user = false, + $password = false, + $dbName = false, $flags = 0, $schema = self::USE_GLOBAL ) { + global $wgDBmwschema; - global $wgOut, $wgDBmwschema; - # Can't get a reference if it hasn't been set yet - if ( !isset( $wgOut ) ) { - $wgOut = null; - } - $this->mOut =& $wgOut; - $this->mFailFunction = $failFunction; - $this->mFlags = DBO_TRX | $flags; - if ( $schema == self::USE_GLOBAL ) { $this->mSchema = $wgDBmwschema; - } - else { + } else { $this->mSchema = $schema; } - + // configure the connection and statement objects - $this->setDB2Option('db2_attr_case', 'DB2_CASE_LOWER', self::CONN_OPTION | self::STMT_OPTION); - $this->setDB2Option('deferred_prepare', 'DB2_DEFERRED_PREPARE_ON', self::STMT_OPTION); - $this->setDB2Option('rowcount', 'DB2_ROWCOUNT_PREFETCH_ON', self::STMT_OPTION); - - $this->open( $server, $user, $password, $dbName); + $this->setDB2Option( 'db2_attr_case', 'DB2_CASE_LOWER', + self::CONN_OPTION | self::STMT_OPTION ); + $this->setDB2Option( 'deferred_prepare', 'DB2_DEFERRED_PREPARE_ON', + self::STMT_OPTION ); + $this->setDB2Option( 'rowcount', 'DB2_ROWCOUNT_PREFETCH_ON', + self::STMT_OPTION ); + + parent::__construct( $server, $user, $password, $dbName, DBO_TRX | $flags ); } - + /** * Enables options only if the ibm_db2 extension version supports them * @param $name String: name of the option in the options array * @param $const String: name of the constant holding the right option value * @param $type Integer: whether this is a Connection or Statement otion */ - private function setDB2Option($name, $const, $type) { - if (defined($const)) { - if ($type & self::CONN_OPTION) $this->mConnOptions[$name] = constant($const); - if ($type & self::STMT_OPTION) $this->mStmtOptions[$name] = constant($const); - } - else { - $this->installPrint("$const is not defined. ibm_db2 version is likely too low."); + private function setDB2Option( $name, $const, $type ) { + if ( defined( $const ) ) { + if ( $type & self::CONN_OPTION ) { + $this->mConnOptions[$name] = constant( $const ); + } + if ( $type & self::STMT_OPTION ) { + $this->mStmtOptions[$name] = constant( $const ); + } + } else { + $this->installPrint( + "$const is not defined. ibm_db2 version is likely too low." ); } } - + /** * Outputs debug information in the appropriate place * @param $string String: the relevant debug message */ - private function installPrint($string) { - wfDebug("$string"); - if ($this->mMode == self::INSTALL_MODE) { - print "

  • $string
  • "; + private function installPrint( $string ) { + wfDebug( "$string\n" ); + if ( $this->mMode == self::INSTALL_MODE ) { + print "
  • $string
  • "; flush(); - } + } } - + /** * Opens a database connection and returns it * Closes any existing connection - * @return a fresh connection + * * @param $server String: hostname * @param $user String * @param $password String * @param $dbName String: database name + * @return a fresh connection */ - public function open( $server, $user, $password, $dbName ) - { + public function open( $server, $user, $password, $dbName ) { // Load the port number - global $wgDBport_db2, $wgDBcataloged; + global $wgDBport; wfProfileIn( __METHOD__ ); - + // Load IBM DB2 driver if missing - if (!@extension_loaded('ibm_db2')) { - @dl('ibm_db2.so'); - } + wfDl( 'ibm_db2' ); + // Test for IBM DB2 support, to avoid suppressed fatal error if ( !function_exists( 'db2_connect' ) ) { - $error = "DB2 functions missing, have you enabled the ibm_db2 extension for PHP?\n"; - $this->installPrint($error); - $this->reportConnectionError($error); + $error = <<installPrint( $error ); + $this->reportConnectionError( $error ); } - if (!strlen($user)) { // Copied from Postgres + if ( strlen( $user ) < 1 ) { + wfProfileOut( __METHOD__ ); return null; } - + // Close existing connection $this->close(); // Cache conn info $this->mServer = $server; - $this->mPort = $port = $wgDBport_db2; + $this->mPort = $port = $wgDBport; $this->mUser = $user; $this->mPassword = $password; $this->mDBname = $dbName; - $this->mCataloged = $cataloged = $wgDBcataloged; - - if ( $cataloged == self::CATALOGED ) { - $this->openCataloged($dbName, $user, $password); - } - elseif ( $cataloged == self::UNCATALOGED ) { - $this->openUncataloged($dbName, $user, $password, $server, $port); - } + + $this->openUncataloged( $dbName, $user, $password, $server, $port ); + // Apply connection config - db2_set_option($this->mConn, $this->mConnOptions, 1); - // Not all MediaWiki code is transactional - // Rather, turn autocommit off in the begin function and turn on after a commit - db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON); + db2_set_option( $this->mConn, $this->mConnOptions, 1 ); + // Some MediaWiki code is still transaction-less (?). + // The strategy is to keep AutoCommit on for that code + // but switch it off whenever a transaction is begun. + db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON ); - if ( $this->mConn == false ) { + if ( !$this->mConn ) { $this->installPrint( "DB connection error\n" ); - $this->installPrint( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); - $this->installPrint( $this->lastError()."\n" ); + $this->installPrint( + "Server: $server, Database: $dbName, User: $user, Password: " + . substr( $password, 0, 3 ) . "...\n" ); + $this->installPrint( $this->lastError() . "\n" ); + + wfProfileOut( __METHOD__ ); return null; } $this->mOpened = true; $this->applySchema(); - + wfProfileOut( __METHOD__ ); return $this->mConn; } - + /** * Opens a cataloged database connection, sets mConn */ - protected function openCataloged( $dbName, $user, $password ) - { - @$this->mConn = db2_connect($dbName, $user, $password); + protected function openCataloged( $dbName, $user, $password ) { + @$this->mConn = db2_pconnect( $dbName, $user, $password ); } - + /** * Opens an uncataloged database connection, sets mConn */ @@ -550,14 +394,15 @@ class DatabaseIbm_db2 extends DatabaseBase { $str = "DRIVER={IBM DB2 ODBC DRIVER};"; $str .= "DATABASE=$dbName;"; $str .= "HOSTNAME=$server;"; - if ($port) $str .= "PORT=$port;"; + // port was formerly validated to not be 0 + $str .= "PORT=$port;"; $str .= "PROTOCOL=TCPIP;"; $str .= "UID=$user;"; $str .= "PWD=$password;"; - - @$this->mConn = db2_connect($str, $user, $password); + + @$this->mConn = db2_pconnect( $str, $user, $password ); } - + /** * Closes a database connection, if it is open * Returns success, true if already closed @@ -565,16 +410,15 @@ class DatabaseIbm_db2 extends DatabaseBase { public function close() { $this->mOpened = false; if ( $this->mConn ) { - if ($this->trxLevel() > 0) { + if ( $this->trxLevel() > 0 ) { $this->commit(); } return db2_close( $this->mConn ); - } - else { + } else { return true; } } - + /** * Returns a fresh instance of this class * @@ -582,34 +426,35 @@ class DatabaseIbm_db2 extends DatabaseBase { * @param $user String: username * @param $password String * @param $dbName String: database name on the server - * @param $failFunction Callback (optional) * @param $flags Integer: database behaviour flags (optional, unused) * @return DatabaseIbm_db2 object */ - static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0) + static function newFromParams( $server, $user, $password, $dbName, + $flags = 0 ) { - return new DatabaseIbm_db2( $server, $user, $password, $dbName, $failFunction, $flags ); + return new DatabaseIbm_db2( $server, $user, $password, $dbName, + $flags ); } - + /** * Retrieves the most current database error * Forces a database rollback */ public function lastError() { $connerr = db2_conn_errormsg(); - if ($connerr) { + if ( $connerr ) { //$this->rollback(); return $connerr; } $stmterr = db2_stmt_errormsg(); - if ($stmterr) { + if ( $stmterr ) { //$this->rollback(); return $stmterr; } - + return false; } - + /** * Get the last error number * Return 0 if no error @@ -617,43 +462,45 @@ class DatabaseIbm_db2 extends DatabaseBase { */ public function lastErrno() { $connerr = db2_conn_error(); - if ($connerr) return $connerr; + if ( $connerr ) { + return $connerr; + } $stmterr = db2_stmt_error(); - if ($stmterr) return $stmterr; + if ( $stmterr ) { + return $stmterr; + } return 0; } - + /** * Is a database connection open? - * @return + * @return */ public function isOpen() { return $this->mOpened; } - + /** * The DBMS-dependent part of query() * @param $sql String: SQL query. - * @return object Result object to feed to fetchObject, fetchRow, ...; or false on failure + * @return object Result object for fetch functions or false on failure * @access private */ /*private*/ public function doQuery( $sql ) { - //print "
  • $sql
  • "; - // Switch into the correct namespace $this->applySchema(); - + $ret = db2_exec( $this->mConn, $sql, $this->mStmtOptions ); - if( !$ret ) { - print "
    ";
    -			print $sql;
    -			print "

    "; + if( $ret == false ) { $error = db2_stmt_errormsg(); - throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( $error ) ); + $this->installPrint( "
    $sql
    " ); + $this->installPrint( $error ); + throw new DBUnexpectedError( $this, 'SQL error: ' + . htmlspecialchars( $error ) ); } $this->mLastResult = $ret; - $this->mAffectedRows = null; // Not calculated until asked for + $this->mAffectedRows = null; // Not calculated until asked for return $ret; } - + /** * @return string Version information from the database */ @@ -661,7 +508,7 @@ class DatabaseIbm_db2 extends DatabaseBase { $info = db2_server_info( $this->mConn ); return $info->DBMS_VER; } - + /** * Queries whether a given table exists * @return boolean @@ -669,22 +516,24 @@ class DatabaseIbm_db2 extends DatabaseBase { public function tableExists( $table ) { $schema = $this->mSchema; $sql = <<< EOF -SELECT COUNT(*) FROM SYSIBM.SYSTABLES ST +SELECT COUNT( * ) FROM SYSIBM.SYSTABLES ST WHERE ST.NAME = '$table' AND ST.CREATOR = '$schema' EOF; $res = $this->query( $sql ); - if (!$res) return false; - + if ( !$res ) { + return false; + } + // If the table exists, there should be one of it - @$row = $this->fetchRow($res); + @$row = $this->fetchRow( $res ); $count = $row[0]; - if ($count == '1' or $count == 1) { + if ( $count == '1' || $count == 1 ) { return true; } - + return false; } - + /** * Fetch the next row from the given result object, in object form. * Fields can be retrieved with $row->fieldname, with fields acting like @@ -700,14 +549,15 @@ EOF; } @$row = db2_fetch_object( $res ); if( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); + throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' + . htmlspecialchars( $this->lastError() ) ); } return $row; } /** * Fetch the next row from the given result object, in associative array - * form. Fields are retrieved with $row['fieldname']. + * form. Fields are retrieved with $row['fieldname']. * * @param $res SQL result object as returned from Database::query(), etc. * @return DB2 row object @@ -719,55 +569,47 @@ EOF; } @$row = db2_fetch_array( $res ); if ( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); + throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' + . htmlspecialchars( $this->lastError() ) ); } return $row; } - - /** - * Override if introduced to base Database class - */ - public function initial_setup() { - // do nothing - } - + /** * Create tables, stored procedures, and so on */ public function setup_database() { - // Timeout was being changed earlier due to mysterious crashes - // Changing it now may cause more problems than not changing it - //set_time_limit(240); try { // TODO: switch to root login if available - + // Switch into the correct namespace $this->applySchema(); $this->begin(); - + $res = $this->sourceFile( "../maintenance/ibm_db2/tables.sql" ); - if ($res !== true) { - print " FAILED: " . htmlspecialchars( $res ) . ""; + if ( $res !== true ) { + print ' FAILED: ' . htmlspecialchars( $res ) . ''; } else { - print " done"; + print ' done'; } - $res = null; - - // TODO: update mediawiki_version table - + $res = $this->sourceFile( "../maintenance/ibm_db2/foreignkeys.sql" ); + if ( $res !== true ) { + print ' FAILED: ' . htmlspecialchars( $res ) . ''; + } else { + print '
  • Foreign keys done
  • '; + } + // TODO: populate interwiki links - - if ($this->lastError()) { - print "
  • Errors encountered during table creation -- rolled back
  • \n"; - print "
  • Please install again
  • \n"; + + if ( $this->lastError() ) { + $this->installPrint( + 'Errors encountered during table creation -- rolled back' ); + $this->installPrint( 'Please install again' ); $this->rollback(); - } - else { + } else { $this->commit(); } - } - catch (MWException $mwe) - { + } catch ( MWException $mwe ) { print "
    $mwe

    "; } } @@ -775,47 +617,48 @@ EOF; /** * Escapes strings * Doesn't escape numbers + * * @param $s String: string to escape * @return escaped string */ public function addQuotes( $s ) { - //$this->installPrint("DB2::addQuotes($s)\n"); + //$this->installPrint( "DB2::addQuotes( $s )\n" ); if ( is_null( $s ) ) { - return "NULL"; - } else if ($s instanceof Blob) { - return "'".$s->fetch($s)."'"; - } else if ($s instanceof IBM_DB2Blob) { - return "'".$this->decodeBlob($s)."'"; - } - $s = $this->strencode($s); - if ( is_numeric($s) ) { + return 'NULL'; + } elseif ( $s instanceof Blob ) { + return "'" . $s->fetch( $s ) . "'"; + } elseif ( $s instanceof IBM_DB2Blob ) { + return "'" . $this->decodeBlob( $s ) . "'"; + } + $s = $this->strencode( $s ); + if ( is_numeric( $s ) ) { return $s; - } - else { + } else { return "'$s'"; } } - + /** * Verifies that a DB2 column/field type is numeric - * @return bool true if numeric + * * @param $type String: DB2 column type + * @return Boolean: true if numeric */ public function is_numeric_type( $type ) { - switch (strtoupper($type)) { - case 'SMALLINT': - case 'INTEGER': - case 'INT': - case 'BIGINT': - case 'DECIMAL': - case 'REAL': - case 'DOUBLE': - case 'DECFLOAT': - return true; + switch ( strtoupper( $type ) ) { + case 'SMALLINT': + case 'INTEGER': + case 'INT': + case 'BIGINT': + case 'DECIMAL': + case 'REAL': + case 'DOUBLE': + case 'DECFLOAT': + return true; } return false; } - + /** * Alias for addQuotes() * @param $s String: string to escape @@ -823,178 +666,153 @@ EOF; */ public function strencode( $s ) { // Bloody useless function - // Prepends backslashes to \x00, \n, \r, \, ', " and \x1a. + // Prepends backslashes to \x00, \n, \r, \, ', " and \x1a. // But also necessary - $s = db2_escape_string($s); + $s = db2_escape_string( $s ); // Wide characters are evil -- some of them look like ' - $s = utf8_encode($s); + $s = utf8_encode( $s ); // Fix its stupidity - $from = array("\\\\", "\\'", '\\n', '\\t', '\\"', '\\r'); - $to = array("\\", "''", "\n", "\t", '"', "\r"); - $s = str_replace($from, $to, $s); // DB2 expects '', not \' escaping + $from = array( "\\\\", "\\'", '\\n', '\\t', '\\"', '\\r' ); + $to = array( "\\", "''", "\n", "\t", '"', "\r" ); + $s = str_replace( $from, $to, $s ); // DB2 expects '', not \' escaping return $s; } - + /** * Switch into the database schema */ protected function applySchema() { - if ( !($this->mSchemaSet) ) { + if ( !( $this->mSchemaSet ) ) { $this->mSchemaSet = true; $this->begin(); - $this->doQuery("SET SCHEMA = $this->mSchema"); + $this->doQuery( "SET SCHEMA = $this->mSchema" ); $this->commit(); - } + } } - + /** * Start a transaction (mandatory) */ public function begin( $fname = 'DatabaseIbm_db2::begin' ) { - // turn off auto-commit - db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF); + // BEGIN is implicit for DB2 + // However, it requires that AutoCommit be off. + + // Some MediaWiki code is still transaction-less (?). + // The strategy is to keep AutoCommit on for that code + // but switch it off whenever a transaction is begun. + db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_OFF ); + $this->mTrxLevel = 1; } - + /** * End a transaction * Must have a preceding begin() */ public function commit( $fname = 'DatabaseIbm_db2::commit' ) { - db2_commit($this->mConn); - // turn auto-commit back on - db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON); + db2_commit( $this->mConn ); + + // Some MediaWiki code is still transaction-less (?). + // The strategy is to keep AutoCommit on for that code + // but switch it off whenever a transaction is begun. + db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON ); + $this->mTrxLevel = 0; } - + /** * Cancel a transaction */ public function rollback( $fname = 'DatabaseIbm_db2::rollback' ) { - db2_rollback($this->mConn); + db2_rollback( $this->mConn ); // turn auto-commit back on // not sure if this is appropriate - db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON); + db2_autocommit( $this->mConn, DB2_AUTOCOMMIT_ON ); $this->mTrxLevel = 0; } - + /** * Makes an encoded list of strings from an array * $mode: - * LIST_COMMA - comma separated, no field names - * LIST_AND - ANDed WHERE clause (without the WHERE) - * LIST_OR - ORed WHERE clause (without the WHERE) - * LIST_SET - comma separated with field names, like a SET clause - * LIST_NAMES - comma separated field names - */ - public function makeList( $a, $mode = LIST_COMMA ) { + * LIST_COMMA - comma separated, no field names + * LIST_AND - ANDed WHERE clause (without the WHERE) + * LIST_OR - ORed WHERE clause (without the WHERE) + * LIST_SET - comma separated with field names, like a SET clause + * LIST_NAMES - comma separated field names + * LIST_SET_PREPARED - like LIST_SET, except with ? tokens as values + */ + function makeList( $a, $mode = LIST_COMMA ) { if ( !is_array( $a ) ) { - throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' ); + throw new DBUnexpectedError( $this, + 'DatabaseIbm_db2::makeList called with incorrect parameters' ); } - $first = true; - $list = ''; - foreach ( $a as $field => $value ) { - if ( !$first ) { - if ( $mode == LIST_AND ) { - $list .= ' AND '; - } elseif($mode == LIST_OR) { - $list .= ' OR '; + // if this is for a prepared UPDATE statement + // (this should be promoted to the parent class + // once other databases use prepared statements) + if ( $mode == LIST_SET_PREPARED ) { + $first = true; + $list = ''; + foreach ( $a as $field => $value ) { + if ( !$first ) { + $list .= ", $field = ?"; } else { - $list .= ','; - } - } else { - $first = false; - } - if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) { - $list .= "($value)"; - } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) { - $list .= "$value"; - } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) { - if( count( $value ) == 0 ) { - throw new MWException( __METHOD__.': empty input' ); - } elseif( count( $value ) == 1 ) { - // Special-case single values, as IN isn't terribly efficient - // Don't necessarily assume the single key is 0; we don't - // enforce linear numeric ordering on other arrays here. - $value = array_values( $value ); - $list .= $field." = ".$this->addQuotes( $value[0] ); - } else { - $list .= $field." IN (".$this->makeList($value).") "; - } - } elseif( is_null($value) ) { - if ( $mode == LIST_AND || $mode == LIST_OR ) { - $list .= "$field IS "; - } elseif ( $mode == LIST_SET ) { - $list .= "$field = "; - } - $list .= 'NULL'; - } else { - if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) { - $list .= "$field = "; - } - if ( $mode == LIST_NAMES ) { - $list .= $value; - } - // Leo: Can't insert quoted numbers into numeric columns - // (?) Might cause other problems. May have to check column type before insertion. - else if ( is_numeric($value) ) { - $list .= $value; - } - else { - $list .= $this->addQuotes( $value ); + $list .= "$field = ?"; + $first = false; } } + $list .= ''; + + return $list; } - return $list; + + // otherwise, call the usual function + return parent::makeList( $a, $mode ); } - + /** * Construct a LIMIT query with optional offset * This is used for query pages + * * @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) */ - public function limitResult($sql, $limit, $offset=false) { - if( !is_numeric($limit) ) { - throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" ); + public function limitResult( $sql, $limit, $offset=false ) { + if( !is_numeric( $limit ) ) { + throw new DBUnexpectedError( $this, + "Invalid non-numeric limit passed to limitResult()\n" ); } if( $offset ) { - $this->installPrint("Offset parameter not supported in limitResult()\n"); + if ( stripos( $sql, 'where' ) === false ) { + return "$sql AND ( ROWNUM BETWEEN $offset AND $offset+$limit )"; + } else { + return "$sql WHERE ( ROWNUM BETWEEN $offset AND $offset+$limit )"; + } } - // TODO implement proper offset handling - // idea: get all the rows between 0 and offset, advance cursor to offset return "$sql FETCH FIRST $limit ROWS ONLY "; } - + /** * Handle reserved keyword replacement in table names - * @return + * * @param $name Object + * @return String */ public function tableName( $name ) { - # Replace reserved words with better ones -// switch( $name ) { -// case 'user': -// return 'mwuser'; -// case 'text': -// return 'pagecontent'; -// default: -// return $name; -// } // we want maximum compatibility with MySQL schema return $name; } - + /** * Generates a timestamp in an insertable format - * @return string timestamp value + * * @param $ts timestamp + * @return String: timestamp value */ - public function timestamp( $ts=0 ) { + public function timestamp( $ts = 0 ) { // TS_MW cannot be easily distinguished from an integer - return wfTimestamp(TS_DB2,$ts); + return wfTimestamp( TS_DB2, $ts ); } /** @@ -1003,19 +821,20 @@ EOF; * @return next value in that sequence */ public function nextSequenceValue( $seqName ) { - // Not using sequences in the primary schema to allow for easy third-party migration scripts - // Emulating MySQL behaviour of using NULL to signal that sequences aren't used + // Not using sequences in the primary schema to allow for easier migration + // from MySQL + // Emulating MySQL behaviour of using NULL to signal that sequences + // aren't used /* $safeseq = preg_replace( "/'/", "''", $seqName ); $res = $this->query( "VALUES NEXTVAL FOR $safeseq" ); $row = $this->fetchRow( $res ); $this->mInsertId = $row[0]; - $this->freeResult( $res ); return $this->mInsertId; */ return null; } - + /** * This must be called after nextSequenceVal * @return Last sequence value used as a primary key @@ -1023,26 +842,27 @@ EOF; public function insertId() { return $this->mInsertId; } - + /** - * Updates the mInsertId property with the value of the last insert into a generated column + * Updates the mInsertId property with the value of the last insert + * into a generated column + * * @param $table String: sanitized table name - * @param $primaryKey Mixed: string name of the primary key or a bool if this call is a do-nothing + * @param $primaryKey Mixed: string name of the primary key * @param $stmt Resource: prepared statement resource * of the SELECT primary_key FROM FINAL TABLE ( INSERT ... ) form */ - private function calcInsertId($table, $primaryKey, $stmt) { - if ($primaryKey) { - $id_row = $this->fetchRow($stmt); - $this->mInsertId = $id_row[0]; + private function calcInsertId( $table, $primaryKey, $stmt ) { + if ( $primaryKey ) { + $this->mInsertId = db2_last_insert_id( $this->mConn ); } } - + /** * INSERT wrapper, inserts an array into a table * - * $args may be a single associative array, or an array of these with numeric keys, - * for multi-row insert + * $args may be a single associative array, or an array of arrays + * with numeric keys, for multi-row insert * * @param $table String: Name of the table to insert to. * @param $args Array: Items to insert into the table. @@ -1051,30 +871,33 @@ EOF; * * @return bool Success of insert operation. IGNORE always returns true. */ - public function insert( $table, $args, $fname = 'DatabaseIbm_db2::insert', $options = array() ) { + public function insert( $table, $args, $fname = 'DatabaseIbm_db2::insert', + $options = array() ) + { if ( !count( $args ) ) { return true; } // get database-specific table name (not used) $table = $this->tableName( $table ); // format options as an array - if ( !is_array( $options ) ) $options = array( $options ); + $options = IBM_DB2Helper::makeArray( $options ); // format args as an array of arrays if ( !( isset( $args[0] ) && is_array( $args[0] ) ) ) { - $args = array($args); + $args = array( $args ); } + // prevent insertion of NULL into primary key columns - list($args, $primaryKeys) = $this->removeNullPrimaryKeys($table, $args); + list( $args, $primaryKeys ) = $this->removeNullPrimaryKeys( $table, $args ); // if there's only one primary key // we'll be able to read its value after insertion $primaryKey = false; - if (count($primaryKeys) == 1) { + if ( count( $primaryKeys ) == 1 ) { $primaryKey = $primaryKeys[0]; } - + // get column names $keys = array_keys( $args[0] ); - $key_count = count($keys); + $key_count = count( $keys ); // If IGNORE is set, we use savepoints to emulate mysql's behavior $ignore = in_array( 'IGNORE', $options ) ? 'mw' : ''; @@ -1082,144 +905,169 @@ EOF; // assume success $res = true; // If we are not in a transaction, we need to be for savepoint trickery - $didbegin = 0; - if (! $this->mTrxLevel) { + if ( !$this->mTrxLevel ) { $this->begin(); - $didbegin = 1; } - $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES '; - switch($key_count) { - //case 0 impossible - case 1: - $sql .= '(?)'; - break; - default: - $sql .= '(?' . str_repeat(',?', $key_count-1) . ')'; - } - // add logic to read back the new primary key value - if ($primaryKey) { - $sql = "SELECT $primaryKey FROM FINAL TABLE($sql)"; + $sql = "INSERT INTO $table ( " . implode( ',', $keys ) . ' ) VALUES '; + if ( $key_count == 1 ) { + $sql .= '( ? )'; + } else { + $sql .= '( ?' . str_repeat( ',?', $key_count-1 ) . ' )'; } - $stmt = $this->prepare($sql); - + //$this->installPrint( "Preparing the following SQL:" ); + //$this->installPrint( "$sql" ); + //$this->installPrint( print_r( $args, true )); + $stmt = $this->prepare( $sql ); + // start a transaction/enter transaction mode $this->begin(); if ( !$ignore ) { - $first = true; + //$first = true; foreach ( $args as $row ) { + //$this->installPrint( "Inserting " . print_r( $row, true )); // insert each row into the database - $res = $res & $this->execute($stmt, $row); + $res = $res & $this->execute( $stmt, $row ); + if ( !$res ) { + $this->installPrint( 'Last error:' ); + $this->installPrint( $this->lastError() ); + } // get the last inserted value into a generated column - $this->calcInsertId($table, $primaryKey, $stmt); + $this->calcInsertId( $table, $primaryKey, $stmt ); } - } - else { + } else { $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 $numrowsinserted = 0; - + // always return true $res = true; - + foreach ( $args as $row ) { $overhead = "SAVEPOINT $ignore ON ROLLBACK RETAIN CURSORS"; - db2_exec($this->mConn, $overhead, $this->mStmtOptions); - - $res2 = $this->execute($stmt, $row); + db2_exec( $this->mConn, $overhead, $this->mStmtOptions ); + + $res2 = $this->execute( $stmt, $row ); + + if ( !$res2 ) { + $this->installPrint( 'Last error:' ); + $this->installPrint( $this->lastError() ); + } // get the last inserted value into a generated column - $this->calcInsertId($table, $primaryKey, $stmt); - + $this->calcInsertId( $table, $primaryKey, $stmt ); + $errNum = $this->lastErrno(); - if ($errNum) { - db2_exec( $this->mConn, "ROLLBACK TO SAVEPOINT $ignore", $this->mStmtOptions ); - } - else { - db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore", $this->mStmtOptions ); + if ( $errNum ) { + db2_exec( $this->mConn, "ROLLBACK TO SAVEPOINT $ignore", + $this->mStmtOptions ); + } else { + db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore", + $this->mStmtOptions ); $numrowsinserted++; } } - + $olde = error_reporting( $olde ); // Set the affected row count for the whole operation $this->mAffectedRows = $numrowsinserted; } // commit either way $this->commit(); - + $this->freePrepared( $stmt ); + return $res; } - + /** * Given a table name and a hash of columns with values * Removes primary key columns from the hash where the value is NULL - * + * * @param $table String: name of the table * @param $args Array of hashes of column names with values - * @return Array: tuple containing filtered array of columns, array of primary keys + * @return Array: tuple( filtered array of columns, array of primary keys ) */ - private function removeNullPrimaryKeys($table, $args) { + private function removeNullPrimaryKeys( $table, $args ) { $schema = $this->mSchema; // find out the primary keys - $keyres = db2_primary_keys($this->mConn, null, strtoupper($schema), strtoupper($table)); + $keyres = db2_primary_keys( $this->mConn, null, strtoupper( $schema ), + strtoupper( $table ) + ); $keys = array(); - for ($row = $this->fetchObject($keyres); $row != null; $row = $this->fetchRow($keyres)) { - $keys[] = strtolower($row->column_name); + for ( + $row = $this->fetchObject( $keyres ); + $row != null; + $row = $this->fetchObject( $keyres ) + ) + { + $keys[] = strtolower( $row->column_name ); } // remove primary keys - foreach ($args as $ai => $row) { - foreach ($keys as $ki => $key) { - if ($row[$key] == null) { - unset($row[$key]); + foreach ( $args as $ai => $row ) { + foreach ( $keys as $key ) { + if ( $row[$key] == null ) { + unset( $row[$key] ); } } $args[$ai] = $row; } // return modified hash - return array($args, $keys); + return array( $args, $keys ); } - + /** * 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 $conds An array of conditions ( WHERE ). Use '*' to update all rows. * @param $fname String: The Class::Function calling this function - * (for the log) + * ( for the log ) * @param $options An array of UPDATE options, can be one or * more of IGNORE, LOW_PRIORITY * @return Boolean */ - public function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) { + public function update( $table, $values, $conds, $fname = 'DatabaseIbm_db2::update', + $options = array() ) + { $table = $this->tableName( $table ); $opts = $this->makeUpdateOptions( $options ); - $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET ); + $sql = "UPDATE $opts $table SET " + . $this->makeList( $values, LIST_SET_PREPARED ); if ( $conds != '*' ) { $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); } - return $this->query( $sql, $fname ); + $stmt = $this->prepare( $sql ); + $this->installPrint( 'UPDATE: ' . print_r( $values, true ) ); + // assuming for now that an array with string keys will work + // if not, convert to simple array first + $result = $this->execute( $stmt, $values ); + $this->freePrepared( $stmt ); + + return $result; } - + /** * DELETE query wrapper * * Use $conds == "*" to delete all rows */ - public function delete( $table, $conds, $fname = 'Database::delete' ) { + public function delete( $table, $conds, $fname = 'DatabaseIbm_db2::delete' ) { if ( !$conds ) { - throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' ); + throw new DBUnexpectedError( $this, + 'DatabaseIbm_db2::delete() called with no conditions' ); } $table = $this->tableName( $table ); $sql = "DELETE FROM $table"; if ( $conds != '*' ) { $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); } - return $this->query( $sql, $fname ); + $result = $this->query( $sql, $fname ); + + return $result; } - + /** * Returns the number of rows affected by the last query or 0 * @return Integer: the number of rows affected by the last query @@ -1229,11 +1077,12 @@ EOF; // Forced result for simulated queries return $this->mAffectedRows; } - if( empty( $this->mLastResult ) ) + if( empty( $this->mLastResult ) ) { return 0; + } return db2_num_rows( $this->mLastResult ); } - + /** * Simulates REPLACE with a DELETE followed by INSERT * @param $table Object @@ -1242,10 +1091,12 @@ EOF; * @param $fname String: name of the function for profiling * @return nothing */ - function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseIbm_db2::replace' ) { + function replace( $table, $uniqueIndexes, $rows, + $fname = 'DatabaseIbm_db2::replace' ) + { $table = $this->tableName( $table ); - if (count($rows)==0) { + if ( count( $rows )==0 ) { return; } @@ -1262,9 +1113,9 @@ EOF; foreach ( $uniqueIndexes as $index ) { if ( $first ) { $first = false; - $sql .= "("; + $sql .= '( '; } else { - $sql .= ') OR ('; + $sql .= ' ) OR ( '; } if ( is_array( $index ) ) { $first2 = true; @@ -1274,23 +1125,24 @@ EOF; } else { $sql .= ' AND '; } - $sql .= $col.'=' . $this->addQuotes( $row[$col] ); + $sql .= $col . '=' . $this->addQuotes( $row[$col] ); } } else { - $sql .= $index.'=' . $this->addQuotes( $row[$index] ); + $sql .= $index . '=' . $this->addQuotes( $row[$index] ); } } - $sql .= ')'; + $sql .= ' )'; $this->query( $sql, $fname ); } # Now insert the row - $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' . - $this->makeList( $row, LIST_COMMA ) . ')'; + $sql = "INSERT INTO $table ( " + . $this->makeList( array_keys( $row ), LIST_NAMES ) + .' ) VALUES ( ' . $this->makeList( $row, LIST_COMMA ) . ' )'; $this->query( $sql, $fname ); } } - + /** * Returns the number of rows in the result set * Has to be called right after the corresponding select query @@ -1303,12 +1155,11 @@ EOF; } if ( $this->mNumRows ) { return $this->mNumRows; - } - else { + } else { return 0; } } - + /** * Moves the row pointer of the result set * @param $res Object: result set @@ -1321,11 +1172,11 @@ EOF; } return db2_fetch_row( $res, $row ); } - + ### - # Fix notices in Block.php + # Fix notices in Block.php ### - + /** * Frees memory associated with a statement resource * @param $res Object: statement resource to free @@ -1336,10 +1187,10 @@ EOF; $res = $res->result; } if ( !@db2_free_result( $res ) ) { - throw new DBUnexpectedError($this, "Unable to free DB2 result\n" ); + throw new DBUnexpectedError( $this, "Unable to free DB2 result\n" ); } } - + /** * Returns the number of columns in a resource * @param $res Object: statement resource @@ -1351,7 +1202,7 @@ EOF; } return db2_num_fields( $res ); } - + /** * Returns the nth column name * @param $res Object: statement resource @@ -1364,57 +1215,65 @@ EOF; } return db2_field_name( $res, $n ); } - + /** * SELECT wrapper * * @param $table Array or string, table name(s) (prefix auto-added) * @param $vars Array or string, field name(s) to be retrieved * @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 (e.g. array('GROUP BY' => 'page_title')), - * see Database::makeSelectOptions code for list of supported stuff + * @param $fname String: calling function name (use __METHOD__) + * for logs/profiling + * @param $options 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) - * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) - * @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure + * (e.g. array( 'page' => array('LEFT JOIN', + * 'page_latest=rev_id') ) + * @return Mixed: database result resource for fetch functions or false + * on failure */ - public function select( $table, $vars, $conds='', $fname = 'DatabaseIbm_db2::select', $options = array(), $join_conds = array() ) + public function select( $table, $vars, $conds = '', $fname = 'DatabaseIbm_db2::select', $options = array(), $join_conds = array() ) { - $res = parent::select( $table, $vars, $conds, $fname, $options, $join_conds ); - + $res = parent::select( $table, $vars, $conds, $fname, $options, + $join_conds ); + // We must adjust for offset - if ( isset( $options['LIMIT'] ) ) { - if ( isset ($options['OFFSET'] ) ) { - $limit = $options['LIMIT']; - $offset = $options['OFFSET']; - } + if ( isset( $options['LIMIT'] ) && isset ( $options['OFFSET'] ) ) { + $limit = $options['LIMIT']; + $offset = $options['OFFSET']; } - - - // DB2 does not have a proper num_rows() function yet, so we must emulate it - // DB2 9.5.3/9.5.4 and the corresponding ibm_db2 driver will introduce a working one - // Yay! - + + // DB2 does not have a proper num_rows() function yet, so we must emulate + // DB2 9.5.4 and the corresponding ibm_db2 driver will introduce + // a working one + // TODO: Yay! + // we want the count - $vars2 = array('count(*) as num_rows'); + $vars2 = array( 'count( * ) as num_rows' ); // respecting just the limit option $options2 = array(); - if ( isset( $options['LIMIT'] ) ) $options2['LIMIT'] = $options['LIMIT']; + if ( isset( $options['LIMIT'] ) ) { + $options2['LIMIT'] = $options['LIMIT']; + } // but don't try to emulate for GROUP BY - if ( isset( $options['GROUP BY'] ) ) return $res; - - $res2 = parent::select( $table, $vars2, $conds, $fname, $options2, $join_conds ); - $obj = $this->fetchObject($res2); + if ( isset( $options['GROUP BY'] ) ) { + return $res; + } + + $res2 = parent::select( $table, $vars2, $conds, $fname, $options2, + $join_conds ); + $obj = $this->fetchObject( $res2 ); $this->mNumRows = $obj->num_rows; - - + return $res; } - + /** * Handles ordering, grouping, and having options ('GROUP BY' => colname) * Has limited support for per-column options (colnum => 'DISTINCT') - * + * * @private * * @param $options Associative array of options to be turned into @@ -1432,31 +1291,41 @@ EOF; } } - if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; - if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}"; - if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; - - if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; - + if ( isset( $options['GROUP BY'] ) ) { + $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; + } + if ( isset( $options['HAVING'] ) ) { + $preLimitTail .= " HAVING {$options['HAVING']}"; + } + if ( isset( $options['ORDER BY'] ) ) { + $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; + } + + if ( isset( $noKeyOptions['DISTINCT'] ) + || isset( $noKeyOptions['DISTINCTROW'] ) ) + { + $startOpts .= 'DISTINCT'; + } + return array( $startOpts, '', $preLimitTail, $postLimitTail ); } - + /** * Returns link to IBM DB2 free download - * @return string wikitext of a link to the server software's web site + * @return String: wikitext of a link to the server software's web site */ - public function getSoftwareLink() { - return "[http://www.ibm.com/software/data/db2/express/?s_cmp=ECDDWW01&s_tact=MediaWiki IBM DB2]"; + public static function getSoftwareLink() { + return '[http://www.ibm.com/db2/express/ IBM DB2]'; } - + /** * Get search engine class. All subclasses of this * need to implement this if they wish to use searching. - * + * * @return String */ public function getSearchEngine() { - return "SearchIBM_DB2"; + return 'SearchIBM_DB2'; } /** @@ -1466,16 +1335,17 @@ EOF; public function wasDeadlock() { // get SQLSTATE $err = $this->lastErrno(); - switch($err) { + switch( $err ) { + // This is literal port of the MySQL logic and may be wrong for DB2 case '40001': // sql0911n, Deadlock or timeout, rollback case '57011': // sql0904n, Resource unavailable, no rollback case '57033': // sql0913n, Deadlock or timeout, no rollback - $this->installPrint("In a deadlock because of SQLSTATE $err"); + $this->installPrint( "In a deadlock because of SQLSTATE $err" ); return true; } return false; } - + /** * Ping the server and try to reconnect if it there is no connection * The connection may be closed and reopened while this happens @@ -1485,15 +1355,9 @@ EOF; // db2_ping() doesn't exist // Emulate $this->close(); - if ($this->mCataloged == null) { - return false; - } - else if ($this->mCataloged) { - $this->mConn = $this->openCataloged($this->mDBName, $this->mUser, $this->mPassword); - } - else if (!$this->mCataloged) { - $this->mConn = $this->openUncataloged($this->mDBName, $this->mUser, $this->mPassword, $this->mServer, $this->mPort); - } + $this->mConn = $this->openUncataloged( $this->mDBName, $this->mUser, + $this->mPassword, $this->mServer, $this->mPort ); + return false; } ###################################### @@ -1502,65 +1366,33 @@ EOF; /** * Not implemented * @return string '' - * @deprecated */ - public function getStatus( $which="%" ) { $this->installPrint('Not implemented for DB2: getStatus()'); return ''; } - /** - * Not implemented - * TODO - * @return bool true - */ - /** - * Not implemented - * @deprecated - */ - public function setFakeSlaveLag( $lag ) { $this->installPrint('Not implemented for DB2: setFakeSlaveLag()'); } - /** - * Not implemented - * @deprecated - */ - public function setFakeMaster( $enabled = true ) { $this->installPrint('Not implemented for DB2: setFakeMaster()'); } + public function getStatus( $which = '%' ) { + $this->installPrint( 'Not implemented for DB2: getStatus()' ); + return ''; + } /** * Not implemented * @return string $sql - * @deprecated - */ - public function limitResultForUpdate($sql, $num) { $this->installPrint('Not implemented for DB2: limitResultForUpdate()'); return $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 * @return string */ - public function fillPreparedArg( $matches ) { $this->installPrint('Not useful for DB2: fillPreparedArg()'); return ''; } - + public function fillPreparedArg( $matches ) { + $this->installPrint( 'Not useful for DB2: fillPreparedArg()' ); + return ''; + } + ###################################### # Reflection ###################################### - - /** - * Query whether a given column exists in the mediawiki schema - * @param $table String: name of the table - * @param $field String: name of the column - * @param $fname String: function name for logging and profiling - */ - public function fieldExists( $table, $field, $fname = 'DatabaseIbm_db2::fieldExists' ) { - $table = $this->tableName( $table ); - $schema = $this->mSchema; - $etable = preg_replace("/'/", "''", $table); - $eschema = preg_replace("/'/", "''", $schema); - $ecol = preg_replace("/'/", "''", $field); - $sql = <<query( $sql, $fname ); - $count = $res ? $this->numRows($res) : 0; - if ($res) - $this->freeResult( $res ); - return $count; - } - + /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure @@ -1569,22 +1401,28 @@ SQL; * @param $fname String: function name for logging and profiling * @return Object query row in object form */ - public function indexInfo( $table, $index, $fname = 'DatabaseIbm_db2::indexExists' ) { + public function indexInfo( $table, $index, + $fname = 'DatabaseIbm_db2::indexExists' ) + { $table = $this->tableName( $table ); $sql = <<query( $sql, $fname ); if ( !$res ) { return null; } $row = $this->fetchObject( $res ); - if ($row != null) return $row; - else return false; + if ( $row != null ) { + return $row; + } else { + return false; + } } - + /** * Returns an information object on a table column * @param $table String: table name @@ -1592,9 +1430,9 @@ SQL; * @return IBM_DB2Field */ public function fieldInfo( $table, $field ) { - return IBM_DB2Field::fromText($this, $table, $field); + return IBM_DB2Field::fromText( $this, $table, $field ); } - + /** * db2_field_type() wrapper * @param $res Object: result of executed statement @@ -1607,7 +1445,7 @@ SQL; } return db2_field_type( $res, $index ); } - + /** * Verifies that an index was created as unique * @param $table String: table name @@ -1615,25 +1453,28 @@ SQL; * @param $fname function name for profiling * @return Bool */ - public function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) { + public function indexUnique ( $table, $index, + $fname = 'DatabaseIbm_db2::indexUnique' ) + { $table = $this->tableName( $table ); $sql = <<query( $sql, $fname ); if ( !$res ) { return null; } - if ($this->fetchObject( $res )) { + if ( $this->fetchObject( $res ) ) { return true; } return false; } - + /** * Returns the size of a text field, or -1 for "unlimited" * @param $table String: table name @@ -1645,15 +1486,15 @@ SQL; $sql = <<query($sql); - $row = $this->fetchObject($res); + $res = $this->query( $sql ); + $row = $this->fetchObject( $res ); $size = $row->size; - $this->freeResult( $res ); return $size; } - + /** * DELETE where the condition is a join * @param $delTable String: deleting from this table @@ -1663,18 +1504,26 @@ SQL; * @param $conds Array: conditionals for join table * @param $fname String: function name for profiling */ - public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseIbm_db2::deleteJoin" ) { + public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, + $conds, $fname = "DatabaseIbm_db2::deleteJoin" ) + { if ( !$conds ) { - throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' ); + throw new DBUnexpectedError( $this, + 'DatabaseIbm_db2::deleteJoin() called with empty $conds' ); } $delTable = $this->tableName( $delTable ); $joinTable = $this->tableName( $joinTable ); - $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; + $sql = <<makeList( $conds, LIST_AND ); } - $sql .= ')'; + $sql .= ' )'; $this->query( $sql, $fname ); } @@ -1684,22 +1533,23 @@ SQL; * @param $b Mixed: data to be encoded * @return IBM_DB2Blob */ - public function encodeBlob($b) { - return new IBM_DB2Blob($b); + public function encodeBlob( $b ) { + return new IBM_DB2Blob( $b ); } - + /** * Description is left as an exercise for the reader * @param $b IBM_DB2Blob: data to be decoded * @return mixed */ - public function decodeBlob($b) { - return $b->getData(); + public function decodeBlob( $b ) { + return "$b"; } - + /** * Convert into a list of string being concatenated - * @param $stringList Array: strings that need to be joined together by the SQL engine + * @param $stringList Array: strings that need to be joined together + * by the SQL engine * @return String: joined by the concatenation operator */ public function buildConcat( $stringList ) { @@ -1707,7 +1557,7 @@ SQL; // Sample query: VALUES 'foo' CONCAT 'bar' CONCAT 'baz' return implode( ' || ', $stringList ); } - + /** * Generates the SQL required to convert a DB2 timestamp into a Unix epoch * @param $column String: name of timestamp column @@ -1717,11 +1567,11 @@ SQL; // TODO // see SpecialAncientpages } - + ###################################### # Prepared statements ###################################### - + /** * Intended to be compatible with the PEAR::DB wrapper functions. * http://pear.php.net/manual/en/package.database.db.intro-execute.php @@ -1735,7 +1585,7 @@ SQL; * @return resource a prepared DB2 SQL statement */ public function prepare( $sql, $func = 'DB2::prepare' ) { - $stmt = db2_prepare($this->mConn, $sql, $this->mStmtOptions); + $stmt = db2_prepare( $this->mConn, $sql, $this->mStmtOptions ); return $stmt; } @@ -1744,7 +1594,7 @@ SQL; * @return Boolean success or failure */ public function freePrepared( $prepared ) { - return db2_free_stmt($prepared); + return db2_free_stmt( $prepared ); } /** @@ -1759,7 +1609,10 @@ SQL; $args = func_get_args(); array_shift( $args ); } - $res = db2_execute($prepared, $args); + $res = db2_execute( $prepared, $args ); + if ( !$res ) { + $this->installPrint( db2_stmt_errormsg() ); + } return $res; } @@ -1792,32 +1645,32 @@ SQL; public function fillPrepared( $preparedQuery, $args ) { reset( $args ); $this->preparedArgs =& $args; - - foreach ($args as $i => $arg) { - db2_bind_param($preparedQuery, $i+1, $args[$i]); + + foreach ( $args as $i => $arg ) { + db2_bind_param( $preparedQuery, $i+1, $args[$i] ); } - + return $preparedQuery; } - + /** * Switches module between regular and install modes */ - public function setMode($mode) { - $old = $this->mMode; + public function setMode( $mode ) { + $old = $this->mMode; $this->mMode = $mode; return $old; } - + /** * Bitwise negation of a column or value in SQL * Same as (~field) in C * @param $field String * @return String */ - function bitNot($field) { - //expecting bit-fields smaller than 4bytes - return 'BITNOT('.$bitField.')'; + function bitNot( $field ) { + // expecting bit-fields smaller than 4bytes + return "BITNOT( $field )"; } /** @@ -1827,8 +1680,8 @@ SQL; * @param $fieldRight String * @return String */ - function bitAnd($fieldLeft, $fieldRight) { - return 'BITAND('.$fieldLeft.', '.$fieldRight.')'; + function bitAnd( $fieldLeft, $fieldRight ) { + return "BITAND( $fieldLeft, $fieldRight )"; } /** @@ -1838,7 +1691,17 @@ SQL; * @param $fieldRight String * @return String */ - function bitOr($fieldLeft, $fieldRight) { - return 'BITOR('.$fieldLeft.', '.$fieldRight.')'; + function bitOr( $fieldLeft, $fieldRight ) { + return "BITOR( $fieldLeft, $fieldRight )"; + } +} + +class IBM_DB2Helper { + public static function makeArray( $maybeArray ) { + if ( !is_array( $maybeArray ) ) { + return array( $maybeArray ); + } + + return $maybeArray; } } diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php index 6b1206b0..41ba2d08 100644 --- a/includes/db/DatabaseMssql.php +++ b/includes/db/DatabaseMssql.php @@ -1,968 +1,1226 @@ + * @author Chris Pucci + * @author Ryan Biesemeyer */ /** * @ingroup Database */ class DatabaseMssql extends DatabaseBase { + var $mInsertId = NULL; + var $mLastResult = NULL; + var $mAffectedRows = NULL; - var $mAffectedRows; - var $mLastResult; - var $mLastError; - var $mLastErrorNo; - var $mDatabaseFile; - - /** - * Constructor - */ - function __construct($server = false, $user = false, $password = false, $dbName = false, - $failFunction = false, $flags = 0, $tablePrefix = 'get from global') { - - global $wgOut, $wgDBprefix, $wgCommandLineMode; - if (!isset($wgOut)) $wgOut = null; # Can't get a reference if it hasn't been set yet - $this->mOut =& $wgOut; - $this->mFailFunction = $failFunction; - $this->mFlags = $flags; - - if ( $this->mFlags & DBO_DEFAULT ) { - if ( $wgCommandLineMode ) { - $this->mFlags &= ~DBO_TRX; - } else { - $this->mFlags |= DBO_TRX; - } - } - - /** Get the default table prefix*/ - $this->mTablePrefix = $tablePrefix == 'get from global' ? $wgDBprefix : $tablePrefix; - - if ($server) $this->open($server, $user, $password, $dbName); - + function cascadingDeletes() { + return true; } - - function getType() { - return 'mssql'; + function cleanupTriggers() { + return true; + } + function strictIPs() { + return true; + } + function realTimestamps() { + return true; + } + function implicitGroupby() { + return false; + } + function implicitOrderby() { + return false; + } + function functionalIndexes() { + return true; + } + function unionSupportsOrderAndLimit() { + return false; } - /** - * todo: check if these should be true like parent class - */ - function implicitGroupby() { return false; } - function implicitOrderby() { return false; } - - static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) { - return new DatabaseMssql($server, $user, $password, $dbName, $failFunction, $flags); + static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { + return new DatabaseMssql( $server, $user, $password, $dbName, $flags ); } - /** Open an MSSQL database and return a resource handle to it - * NOTE: only $dbName is used, the other parameters are irrelevant for MSSQL databases + /** + * Usually aborts on failure */ - function open($server,$user,$password,$dbName) { - wfProfileIn(__METHOD__); - - # Test for missing mysql.so - # First try to load it - if (!@extension_loaded('mssql')) { - @dl('mssql.so'); + function open( $server, $user, $password, $dbName ) { + # Test for driver support, to avoid suppressed fatal error + if ( !function_exists( 'sqlsrv_connect' ) ) { + throw new DBConnectionError( $this, "MS Sql Server Native (sqlsrv) functions missing. You can download the driver from: http://go.microsoft.com/fwlink/?LinkId=123470\n" ); } - # Fail now - # Otherwise we get a suppressed fatal error, which is very hard to track down - if (!function_exists( 'mssql_connect')) { - throw new DBConnectionError( $this, "MSSQL functions missing, have you compiled PHP with the --with-mssql option?\n" ); + global $wgDBport; + + if ( !strlen( $user ) ) { # e.g. the class is being loaded + return; } $this->close(); - $this->mServer = $server; - $this->mUser = $user; + $this->mServer = $server; + $this->mPort = $wgDBport; + $this->mUser = $user; $this->mPassword = $password; - $this->mDBname = $dbName; - - wfProfileIn("dbconnect-$server"); - - # Try to connect up to three times - # The kernel's default SYN retransmission period is far too slow for us, - # so we use a short timeout plus a manual retry. - $this->mConn = false; - $max = 3; - for ( $i = 0; $i < $max && !$this->mConn; $i++ ) { - if ( $i > 1 ) { - usleep( 1000 ); - } - if ($this->mFlags & DBO_PERSISTENT) { - @/**/$this->mConn = mssql_pconnect($server, $user, $password); - } else { - # Create a new connection... - @/**/$this->mConn = mssql_connect($server, $user, $password, true); - } + $this->mDBname = $dbName; + + $connectionInfo = array(); + + if( $dbName ) { + $connectionInfo['Database'] = $dbName; } - - wfProfileOut("dbconnect-$server"); - - if ($dbName != '') { - if ($this->mConn !== false) { - $success = @/**/mssql_select_db($dbName, $this->mConn); - 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 { - wfDebug("DB connection error\n"); - wfDebug("Server: $server, User: $user, Password: ".substr($password, 0, 3)."...\n"); - $success = false; - } + + // Start NT Auth Hack + // Quick and dirty work around to provide NT Auth designation support. + // Current solution requires installer to know to input 'ntauth' for both username and password + // to trigger connection via NT Auth. - ugly, ugly, ugly + // TO-DO: Make this better and add NT Auth choice to MW installer when SQL Server option is chosen. + $ntAuthUserTest = strtolower( $user ); + $ntAuthPassTest = strtolower( $password ); + + // Decide which auth scenerio to use + if( ( $ntAuthPassTest == 'ntauth' && $ntAuthUserTest == 'ntauth' ) ){ + // Don't add credentials to $connectionInfo } else { - # Delay USE query - $success = (bool)$this->mConn; + $connectionInfo['UID'] = $user; + $connectionInfo['PWD'] = $password; + } + // End NT Auth Hack + + $this->mConn = @sqlsrv_connect( $server, $connectionInfo ); + + if ( $this->mConn === false ) { + wfDebug( "DB connection error\n" ); + wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); + wfDebug( $this->lastError() . "\n" ); + return false; } - if (!$success) $this->reportConnectionError(); - $this->mOpened = $success; - wfProfileOut(__METHOD__); - return $success; + $this->mOpened = true; + return $this->mConn; } /** - * Close an MSSQL database + * Closes a database connection, if it is open + * Returns success, true if already closed */ function close() { $this->mOpened = false; - if ($this->mConn) { - if ($this->trxLevel()) $this->commit(); - return mssql_close($this->mConn); - } else return true; + if ( $this->mConn ) { + return sqlsrv_close( $this->mConn ); + } else { + return true; + } } - /** - * - MSSQL doesn't seem to do buffered results - * - the trasnaction syntax is modified here to avoid having to replicate - * Database::query which uses BEGIN, COMMIT, ROLLBACK - */ - function doQuery($sql) { - if ($sql == 'BEGIN' || $sql == 'COMMIT' || $sql == 'ROLLBACK') return true; # $sql .= ' TRANSACTION'; - $sql = preg_replace('|[^\x07-\x7e]|','?',$sql); # TODO: need to fix unicode - just removing it here while testing - $ret = mssql_query($sql, $this->mConn); - if ($ret === false) { - $err = mssql_get_last_message(); - if ($err) $this->mlastError = $err; - $row = mssql_fetch_row(mssql_query('select @@ERROR')); - if ($row[0]) $this->mlastErrorNo = $row[0]; - } else $this->mlastErrorNo = false; - return $ret; + function doQuery( $sql ) { + wfDebug( "SQL: [$sql]\n" ); + $this->offset = 0; + + // several extensions seem to think that all databases support limits via LIMIT N after the WHERE clause + // well, MSSQL uses SELECT TOP N, so to catch any of those extensions we'll do a quick check for a LIMIT + // clause and pass $sql through $this->LimitToTopN() which parses the limit clause and passes the result to + // $this->limitResult(); + if ( preg_match( '/\bLIMIT\s*/i', $sql ) ) { + // massage LIMIT -> TopN + $sql = $this->LimitToTopN( $sql ) ; + } + + // MSSQL doesn't have EXTRACT(epoch FROM XXX) + if ( preg_match('#\bEXTRACT\s*?\(\s*?EPOCH\s+FROM\b#i', $sql, $matches ) ) { + // This is same as UNIX_TIMESTAMP, we need to calc # of seconds from 1970 + $sql = str_replace( $matches[0], "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),", $sql ); + } + + // perform query + $stmt = sqlsrv_query( $this->mConn, $sql ); + if ( $stmt == false ) { + $message = "A database error has occurred. Did you forget to run maintenance/update.php after upgrading? See: http://www.mediawiki.org/wiki/Manual:Upgrading#Run_the_update_script\n" . + "Query: " . htmlentities( $sql ) . "\n" . + "Function: " . __METHOD__ . "\n"; + // process each error (our driver will give us an array of errors unlike other providers) + foreach ( sqlsrv_errors() as $error ) { + $message .= $message . "ERROR[" . $error['code'] . "] " . $error['message'] . "\n"; + } + + throw new DBUnexpectedError( $this, $message ); + } + // remember number of rows affected + $this->mAffectedRows = sqlsrv_rows_affected( $stmt ); + + // if it is a SELECT statement, or an insert with a request to output something we want to return a row. + if ( ( preg_match( '#\bSELECT\s#i', $sql ) ) || + ( preg_match( '#\bINSERT\s#i', $sql ) && preg_match( '#\bOUTPUT\s+INSERTED\b#i', $sql ) ) ) { + // this is essentially a rowset, but Mediawiki calls these 'result' + // the rowset owns freeing the statement + $res = new MssqlResult( $stmt ); + } else { + // otherwise we simply return it was successful, failure throws an exception + $res = true; + } + return $res; } - /** - * Free a result object - */ function freeResult( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - if ( !@/**/mssql_free_result( $res ) ) { - throw new DBUnexpectedError( $this, "Unable to free MSSQL result" ); - } + $res->free(); } - /** - * Fetch the next row from the given result object, in object form. - * Fields can be retrieved with $row->fieldname, with fields acting like - * member variables. - * - * @param $res SQL result object as returned from Database::query(), etc. - * @return MySQL row object - * @throws DBUnexpectedError Thrown if the database returns an error - */ function fetchObject( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @/**/$row = mssql_fetch_object( $res ); - if ( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) ); - } + $row = $res->fetch( 'OBJECT' ); return $row; } - /** - * 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 MySQL row object - * @throws DBUnexpectedError Thrown if the database returns an error - */ - function fetchRow( $res ) { + function getErrors() { + $strRet = ''; + $retErrors = sqlsrv_errors( SQLSRV_ERR_ALL ); + if ( $retErrors != null ) { + foreach ( $retErrors as $arrError ) { + $strRet .= "SQLState: " . $arrError[ 'SQLSTATE'] . "\n"; + $strRet .= "Error Code: " . $arrError[ 'code'] . "\n"; + $strRet .= "Message: " . $arrError[ 'message'] . "\n"; + } + } else { + $strRet = "No errors found"; + } + return $strRet; + } + + function fetchRow( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @/**/$row = mssql_fetch_array( $res ); - if ( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) ); - } + $row = $res->fetch( SQLSRV_FETCH_BOTH ); return $row; } - /** - * Get the number of rows in a result object - */ function numRows( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - @/**/$n = mssql_num_rows( $res ); - if ( $this->lastErrno() ) { - throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) ); - } - return $n; + return ( $res ) ? $res->numrows() : 0; } - /** - * Get the number of fields in a result object - * See documentation for mysql_num_fields() - * @param $res SQL result object as returned from Database::query(), etc. - */ function numFields( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - return mssql_num_fields( $res ); + return ( $res ) ? $res->numfields() : 0; } - /** - * Get a field name in a result object - * See documentation for mysql_field_name(): - * http://www.php.net/mysql_field_name - * @param $res SQL result object as returned from Database::query(), etc. - * @param $n Int - */ function fieldName( $res, $n ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - return mssql_field_name( $res, $n ); + return ( $res ) ? $res->fieldname( $n ) : 0; } /** - * Get the inserted value of an auto-increment row - * - * The value inserted should be fetched from nextSequenceValue() - * - * Example: - * $id = $dbw->nextSequenceValue('page_page_id_seq'); - * $dbw->insert('page',array('page_id' => $id)); - * $id = $dbw->insertId(); + * This must be called after nextSequenceVal */ function insertId() { - $row = mssql_fetch_row(mssql_query('select @@IDENTITY')); - return $row[0]; + return $this->mInsertId; } - /** - * Change the position of the cursor in a result object - * See mysql_data_seek() - * @param $res SQL result object as returned from Database::query(), etc. - * @param $row Database row - */ function dataSeek( $res, $row ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } - return mssql_data_seek( $res, $row ); + return ( $res ) ? $res->seek( $row ) : false; } - /** - * Get the last error number - */ - function lastErrno() { - return $this->mlastErrorNo; + function lastError() { + if ( $this->mConn ) { + return $this->getErrors(); + } + else { + return "No database connection"; + } } - /** - * Get a description of the last error - */ - function lastError() { - return $this->mlastError; + function lastErrno() { + $err = sqlsrv_errors( SQLSRV_ERR_ALL ); + if ( $err[0] ) return $err[0]['code']; + else return 0; } - /** - * Get the number of rows affected by the last write query - */ function affectedRows() { - return mssql_rows_affected( $this->mConn ); + return $this->mAffectedRows; } /** - * Simple UPDATE wrapper - * Usually aborts on failure - * If errors are explicitly ignored, returns success + * SELECT wrapper * - * This function exists for historical reasons, Database::update() has a more standard - * calling convention and feature set + * @param $table Mixed: array or string, table name(s) (prefix auto-added) + * @param $vars Mixed: array or string, field name(s) to be retrieved + * @param $conds Mixed: array or string, condition(s) for WHERE + * @param $fname String: calling function name (use __METHOD__) for logs/profiling + * @param $options Array: associative array of options (e.g. array('GROUP BY' => 'page_title')), + * see Database::makeSelectOptions code for list of supported stuff + * @param $join_conds Array: Associative array of table join conditions (optional) + * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) + * @return Mixed: database result resource (feed to Database::fetchObject or whatever), or false on failure */ - function set( $table, $var, $value, $cond, $fname = 'Database::set' ) + function select( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) { - if ($value == "NULL") $value = "''"; # see comments in makeListWithoutNulls() - $table = $this->tableName( $table ); - $sql = "UPDATE $table SET $var = '" . - $this->strencode( $value ) . "' WHERE ($cond)"; - return (bool)$this->query( $sql, $fname ); - } - - /** - * Simple SELECT wrapper, returns a single field, input must be encoded - * Usually aborts on failure - * If errors are explicitly ignored, returns FALSE on failure - */ - function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) { - if ( !is_array( $options ) ) { - $options = array( $options ); - } - $options['LIMIT'] = 1; - - $res = $this->select( $table, $var, $cond, $fname, $options ); - if ( $res === false || !$this->numRows( $res ) ) { - return false; - } - $row = $this->fetchRow( $res ); - if ( $row !== false ) { - $this->freeResult( $res ); - return $row[0]; - } else { - return false; - } - } - - /** - * Returns an optional USE INDEX clause to go after the table, and a - * string to go at the end of the query - * - * @private - * - * @param $options Array: an associative array of options to be turned into - * an SQL query, valid keys are listed in the function. - * @return array - */ - function makeSelectOptions( $options ) { - $preLimitTail = $postLimitTail = ''; - $startOpts = ''; - - $noKeyOptions = array(); - foreach ( $options as $key => $option ) { - if ( is_numeric( $key ) ) { - $noKeyOptions[$option] = true; - } - } - - if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}"; - if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}"; - if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}"; - - //if (isset($options['LIMIT'])) { - // $tailOpts .= $this->limitResult('', $options['LIMIT'], - // isset($options['OFFSET']) ? $options['OFFSET'] - // : false); - //} - - 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'; - - # Various MySQL extensions - if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */'; - if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY'; - if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT'; - if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT'; - if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT'; - if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS'; - if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE'; - if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE'; - - if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) { - $useIndex = $this->useIndexClause( $options['USE INDEX'] ); - } else { - $useIndex = ''; + $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); + if ( isset( $options['EXPLAIN'] ) ) { + sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL ON;" ); + $ret = $this->query( $sql, $fname ); + sqlsrv_query( $this->mConn, "SET SHOWPLAN_ALL OFF;" ); + return $ret; } - - return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); + return $this->query( $sql, $fname ); } /** * SELECT wrapper * - * @param $table Mixed: Array or string, table name(s) (prefix auto-added) - * @param $vars Mixed: Array or string, field name(s) to be retrieved - * @param $conds Mixed: Array or string, condition(s) for WHERE + * @param $table Mixed: Array or string, table name(s) (prefix auto-added) + * @param $vars Mixed: Array or string, field name(s) to be retrieved + * @param $conds Mixed: Array or string, condition(s) for WHERE * @param $fname String: Calling function name (use __METHOD__) for logs/profiling - * @param $options Array: Associative array of options (e.g. array('GROUP BY' => 'page_title')), - * see Database::makeSelectOptions code for list of supported stuff - * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure + * @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 Array: Associative array of table join conditions (optional) + * (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') ) + * @return string, the SQL text */ - function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() ) - { - if( is_array( $vars ) ) { - $vars = implode( ',', $vars ); - } - if( !is_array( $options ) ) { - $options = array( $options ); - } - if( is_array( $table ) ) { - if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) ) - $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] ); - else - $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) ); - } elseif ($table!='') { - if ($table{0}==' ') { - $from = ' FROM ' . $table; - } else { - $from = ' FROM ' . $this->tableName( $table ); - } - } else { - $from = ''; - } - - list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options ); - - if( !empty( $conds ) ) { - if ( is_array( $conds ) ) { - $conds = $this->makeList( $conds, LIST_AND ); - } - $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail"; - } else { - $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail"; - } - - if (isset($options['LIMIT'])) - $sql = $this->limitResult($sql, $options['LIMIT'], - isset($options['OFFSET']) ? $options['OFFSET'] : false); - $sql = "$sql $postLimitTail"; - - if (isset($options['EXPLAIN'])) { - $sql = 'EXPLAIN ' . $sql; + function selectSQLText( $table, $vars, $conds = '', $fname = 'DatabaseMssql::select', $options = array(), $join_conds = array() ) { + if ( isset( $options['EXPLAIN'] ) ) { + unset( $options['EXPLAIN'] ); } - return $this->query( $sql, $fname ); + return parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds ); } /** - * Determines whether a field exists in a table - * Usually aborts on failure - * If errors are explicitly ignored, returns NULL on failure + * Estimate rows in dataset + * Returns estimated count, based on SHOWPLAN_ALL output + * This is not necessarily an accurate estimate, so use sparingly + * Returns -1 if count cannot be found + * Takes same arguments as Database::select() */ - function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) { - $table = $this->tableName( $table ); - $sql = "SELECT TOP 1 * FROM $table"; - $res = $this->query( $sql, 'Database::fieldExists' ); - - $found = false; - while ( $row = $this->fetchArray( $res ) ) { - if ( isset($row[$field]) ) { - $found = true; - break; - } + function estimateRowCount( $table, $vars = '*', $conds = '', $fname = 'DatabaseMssql::estimateRowCount', $options = array() ) { + $options['EXPLAIN'] = true;// http://msdn2.microsoft.com/en-us/library/aa259203.aspx + $res = $this->select( $table, $vars, $conds, $fname, $options ); + + $rows = -1; + if ( $res ) { + $row = $this->fetchRow( $res ); + if ( isset( $row['EstimateRows'] ) ) $rows = $row['EstimateRows']; } - - $this->freeResult( $res ); - return $found; + return $rows; } + /** - * Get information about an index into an object - * Returns false if the index does not exist + * Returns information about an index + * If errors are explicitly ignored, returns NULL on failure */ - function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) { - - throw new DBUnexpectedError( $this, 'Database::indexInfo called which is not supported yet' ); - return null; - - $table = $this->tableName( $table ); - $sql = 'SHOW INDEX FROM '.$table; + 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 + # returned value except to check for the existance of indexes. + $sql = "sp_helpindex '" . $table . "'"; $res = $this->query( $sql, $fname ); if ( !$res ) { - return null; + return NULL; } $result = array(); - while ( $row = $this->fetchObject( $res ) ) { - if ( $row->Key_name == $index ) { - $result[] = $row; - } - } - $this->freeResult($res); - - return empty($result) ? false : $result; - } - - /** - * Query whether a given table exists - */ - function tableExists( $table ) { - $table = $this->tableName( $table ); - $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '$table'" ); - $exist = ($res->numRows() > 0); - $this->freeResult($res); - return $exist; - } - - /** - * mysql_fetch_field() wrapper - * Returns false if the field doesn't exist - * - * @param $table - * @param $field - */ - function fieldInfo( $table, $field ) { - $table = $this->tableName( $table ); - $res = $this->query( "SELECT TOP 1 * FROM $table" ); - $n = mssql_num_fields( $res->result ); - for( $i = 0; $i < $n; $i++ ) { - $meta = mssql_fetch_field( $res->result, $i ); - if( $field == $meta->name ) { - return new MSSQLField($meta); + foreach ( $res as $row ) { + if ( $row->index_name == $index ) { + $row->Non_unique = !stristr( $row->index_description, "unique" ); + $cols = explode( ", ", $row->index_keys ); + foreach ( $cols as $col ) { + $row->Column_name = trim( $col ); + $result[] = clone $row; + } + } else if ( $index == 'PRIMARY' && stristr( $row->index_description, 'PRIMARY' ) ) { + $row->Non_unique = 0; + $cols = explode( ", ", $row->index_keys ); + foreach ( $cols as $col ) { + $row->Column_name = trim( $col ); + $result[] = clone $row; + } } } - return false; - } - - /** - * mysql_field_type() wrapper - */ - function fieldType( $res, $index ) { - if ( $res instanceof ResultWrapper ) { - $res = $res->result; - } - return mssql_field_type( $res, $index ); + return empty( $result ) ? false : $result; } /** * INSERT wrapper, inserts an array into a table * - * $a may be a single associative array, or an array of these with numeric keys, for + * $arrToInsert may be a single associative array, or an array of these with numeric keys, for * multi-row insert. * * Usually aborts on failure * If errors are explicitly ignored, returns success - * - * Same as parent class implementation except that it removes primary key from column lists - * because MSSQL doesn't support writing nulls to IDENTITY (AUTO_INCREMENT) columns */ - function insert( $table, $a, $fname = 'Database::insert', $options = array() ) { + function insert( $table, $arrToInsert, $fname = 'DatabaseMssql::insert', $options = array() ) { # No rows to insert, easy just return now - if ( !count( $a ) ) { + if ( !count( $arrToInsert ) ) { return true; } - $table = $this->tableName( $table ); + if ( !is_array( $options ) ) { $options = array( $options ); } - - # todo: need to record primary keys at table create time, and remove NULL assignments to them - if ( isset( $a[0] ) && is_array( $a[0] ) ) { - $multi = true; - $keys = array_keys( $a[0] ); -# if (ereg('_id$',$keys[0])) { - foreach ($a as $i) { - if (is_null($i[$keys[0]])) unset($i[$keys[0]]); # remove primary-key column from multiple insert lists if empty value - } -# } - $keys = array_keys( $a[0] ); - } else { - $multi = false; - $keys = array_keys( $a ); -# if (ereg('_id$',$keys[0]) && empty($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value - if (is_null($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value - $keys = array_keys( $a ); + + $table = $this->tableName( $table ); + + if ( !( isset( $arrToInsert[0] ) && is_array( $arrToInsert[0] ) ) ) {// Not multi row + $arrToInsert = array( 0 => $arrToInsert );// make everything multi row compatible } - # handle IGNORE option - # example: - # MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop') - # MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop') - $ignore = in_array('IGNORE',$options); - - # remove IGNORE from options list - if ($ignore) { - $oldoptions = $options; - $options = array(); - foreach ($oldoptions as $o) if ($o != 'IGNORE') $options[] = $o; - } - - $keylist = implode(',', $keys); - $sql = 'INSERT '.implode(' ', $options)." INTO $table (".implode(',', $keys).') VALUES '; - if ($multi) { - if ($ignore) { - # If multiple and ignore, then do each row as a separate conditional insert - foreach ($a as $row) { - $prival = $row[$keys[0]]; - $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql"; - if (!$this->query("$sql (".$this->makeListWithoutNulls($row).')', $fname)) return false; + $allOk = true; + + + // We know the table we're inserting into, get its identity column + $identity = null; + $tableRaw = preg_replace( '#\[([^\]]*)\]#', '$1', $table ); // strip matching square brackets from table name + $res = $this->doQuery( "SELECT NAME AS idColumn FROM SYS.IDENTITY_COLUMNS WHERE OBJECT_NAME(OBJECT_ID)='{$tableRaw}'" ); + if( $res && $res->numrows() ){ + // There is an identity for this table. + $identity = array_pop( $res->fetch( SQLSRV_FETCH_ASSOC ) ); + } + unset( $res ); + + foreach ( $arrToInsert as $a ) { + // start out with empty identity column, this is so we can return it as a result of the insert logic + $sqlPre = ''; + $sqlPost = ''; + $identityClause = ''; + + // if we have an identity column + if( $identity ) { + // iterate through + foreach ($a as $k => $v ) { + if ( $k == $identity ) { + if( !is_null($v) ){ + // there is a value being passed to us, we need to turn on and off inserted identity + $sqlPre = "SET IDENTITY_INSERT $table ON;" ; + $sqlPost = ";SET IDENTITY_INSERT $table OFF;"; + + } else { + // we can't insert NULL into an identity column, so remove the column from the insert. + unset( $a[$k] ); + } + } } - return true; - } else { - $first = true; - foreach ($a as $row) { - if ($first) $first = false; else $sql .= ','; - $sql .= '('.$this->makeListWithoutNulls($row).')'; + $identityClause = "OUTPUT INSERTED.$identity "; // we want to output an identity column as result + } + + $keys = array_keys( $a ); + + + // INSERT IGNORE is not supported by SQL Server + // remove IGNORE from options list and set ignore flag to true + $ignoreClause = false; + foreach ( $options as $k => $v ) { + if ( strtoupper( $v ) == "IGNORE" ) { + unset( $options[$k] ); + $ignoreClause = true; } } - } else { - if ($ignore) { + + // translate MySQL INSERT IGNORE to something SQL Server can use + // example: + // MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop') + // MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop') + if ( $ignoreClause == true ) { $prival = $a[$keys[0]]; - $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql"; + $sqlPre .= "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival')"; + } + + // Build the actual query + $sql = $sqlPre . 'INSERT ' . implode( ' ', $options ) . + " INTO $table (" . implode( ',', $keys ) . ") $identityClause VALUES ("; + + $first = true; + foreach ( $a as $value ) { + if ( $first ) { + $first = false; + } else { + $sql .= ','; + } + if ( is_string( $value ) ) { + $sql .= $this->addIdentifierQuotes( $value ); + } elseif ( is_null( $value ) ) { + $sql .= 'null'; + } elseif ( is_array( $value ) || is_object( $value ) ) { + if ( is_object( $value ) && strtolower( get_class( $value ) ) == 'blob' ) { + $sql .= $this->addIdentifierQuotes( $value->fetch() ); + } else { + $sql .= $this->addIdentifierQuotes( serialize( $value ) ); + } + } else { + $sql .= $value; + } } - $sql .= '('.$this->makeListWithoutNulls($a).')'; + $sql .= ')' . $sqlPost; + + // Run the query + $ret = sqlsrv_query( $this->mConn, $sql ); + + if ( $ret === false ) { + throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname ); + } elseif ( $ret != NULL ) { + // remember number of rows affected + $this->mAffectedRows = sqlsrv_rows_affected( $ret ); + if ( !is_null($identity) ) { + // then we want to get the identity column value we were assigned and save it off + $row = sqlsrv_fetch_object( $ret ); + $this->mInsertId = $row->$identity; + } + sqlsrv_free_stmt( $ret ); + continue; + } + $allOk = false; } - return (bool)$this->query( $sql, $fname ); + return $allOk; } /** - * MSSQL doesn't allow implicit casting of NULL's into non-null values for NOT NULL columns - * for now I've just converted the NULL's in the lists for updates and inserts into empty strings - * which get implicitly casted to 0 for numeric columns - * NOTE: the set() method above converts NULL to empty string as well but not via this method + * INSERT SELECT wrapper + * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...) + * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() + * $conds may be "*" to copy the whole table + * srcTable may be an array of tables. */ - function makeListWithoutNulls($a, $mode = LIST_COMMA) { - return str_replace("NULL","''",$this->makeList($a,$mode)); + function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseMssql::insertSelect', + $insertOptions = array(), $selectOptions = array() ) + { + $ret = parent::insertSelect( $destTable, $srcTable, $varMap, $conds, $fname, $insertOptions, $selectOptions ); + + if ( $ret === false ) { + throw new DBQueryError( $this, $this->getErrors(), $this->lastErrno(), $sql, $fname ); + } elseif ( $ret != NULL ) { + // remember number of rows affected + $this->mAffectedRows = sqlsrv_rows_affected( $ret ); + return $ret; + } + return NULL; } /** - * UPDATE wrapper, takes a condition array and a SET array + * Format a table name ready for use in constructing an SQL query + * + * This does two important things: it brackets table names which as necessary, + * and it adds a table prefix if there is one. * - * @param $table String: The table to UPDATE - * @param $values Array: An array of values to SET - * @param $conds Array: An array of conditions (WHERE). Use '*' to update all rows. - * @param $fname String: The Class::Function calling this function - * (for the log) - * @param $options Array: An array of UPDATE options, can be one or - * more of IGNORE, LOW_PRIORITY - * @return bool + * All functions of this object which require a table name call this function + * themselves. Pass the canonical name to such functions. This is only needed + * when calling query() directly. + * + * @param $name String: database table name */ - function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) { - $table = $this->tableName( $table ); - $opts = $this->makeUpdateOptions( $options ); - $sql = "UPDATE $opts $table SET " . $this->makeListWithoutNulls( $values, LIST_SET ); - if ( $conds != '*' ) { - $sql .= " WHERE " . $this->makeList( $conds, LIST_AND ); + function tableName( $name ) { + global $wgSharedDB; + # Skip quoted literals + if ( $name != '' && $name { 0 } != '[' ) { + if ( $this->mTablePrefix !== '' && strpos( '.', $name ) === false ) { + $name = "{$this->mTablePrefix}$name"; + } + if ( isset( $wgSharedDB ) && "{$this->mTablePrefix}user" == $name ) { + $name = "[$wgSharedDB].[$name]"; + } else { + # Standard quoting + if ( $name != '' ) $name = "[$name]"; + } } - return $this->query( $sql, $fname ); + return $name; } /** - * Make UPDATE options for the Database::update function - * - * @private - * @param $options Array: The options passed to Database::update - * @return string + * Return the next in a sequence, save the value for retrieval via insertId() */ - function makeUpdateOptions( $options ) { - if( !is_array( $options ) ) { - $options = array( $options ); + function nextSequenceValue( $seqName ) { + if ( !$this->tableExists( 'sequence_' . $seqName ) ) { + sqlsrv_query( $this->mConn, "CREATE TABLE [sequence_$seqName] (id INT NOT NULL IDENTITY PRIMARY KEY, junk varchar(10) NULL)" ); } - $opts = array(); - if ( in_array( 'LOW_PRIORITY', $options ) ) - $opts[] = $this->lowPriorityOption(); - if ( in_array( 'IGNORE', $options ) ) - $opts[] = 'IGNORE'; - return implode(' ', $opts); - } + sqlsrv_query( $this->mConn, "INSERT INTO [sequence_$seqName] (junk) VALUES ('')" ); + $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" ); + $row = sqlsrv_fetch_array( $ret, SQLSRV_FETCH_ASSOC );// KEEP ASSOC THERE, weird weird bug dealing with the return value if you don't - /** - * Change the current database - */ - function selectDB( $db ) { - $this->mDBname = $db; - return mssql_select_db( $db, $this->mConn ); + sqlsrv_free_stmt( $ret ); + $this->mInsertId = $row['id']; + return $row['id']; } /** - * MSSQL has a problem with the backtick quoting, so all this does is ensure the prefix is added exactly once + * Return the current value of a sequence. Assumes it has ben nextval'ed in this session. */ - function tableName($name) { - return strpos($name, $this->mTablePrefix) === 0 ? $name : "{$this->mTablePrefix}$name"; + function currentSequenceValue( $seqName ) { + $ret = sqlsrv_query( $this->mConn, "SELECT TOP 1 id FROM [sequence_$seqName] ORDER BY id DESC" ); + if ( $ret !== false ) { + $row = sqlsrv_fetch_array( $ret ); + sqlsrv_free_stmt( $ret ); + return $row['id']; + } else { + return $this->nextSequenceValue( $seqName ); + } } - /** - * MSSQL doubles quotes instead of escaping them - * @param $s String to be slashed. - * @return string slashed string. - */ - function strencode($s) { - return str_replace("'","''",$s); - } - /** - * REPLACE query wrapper - * PostgreSQL simulates this with a DELETE followed by INSERT - * $row is the row to insert, an associative array - * $uniqueIndexes is an array of indexes. Each element may be either a - * field name or an array of field names - * - * It may be more efficient to leave off unique indexes which are unlikely to collide. - * However if you do this, you run the risk of encountering errors which wouldn't have - * occurred in MySQL - * - * @todo migrate comment to phodocumentor format - */ - function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) { + # REPLACE query wrapper + # MSSQL simulates this with a DELETE followed by INSERT + # $row is the row to insert, an associative array + # $uniqueIndexes is an array of indexes. Each element may be either a + # field name or an array of field names + # + # It may be more efficient to leave off unique indexes which are unlikely to collide. + # However if you do this, you run the risk of encountering errors which wouldn't have + # occurred in MySQL + function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseMssql::replace' ) { $table = $this->tableName( $table ); + if ( count( $rows ) == 0 ) { + return; + } + # Single row case if ( !is_array( reset( $rows ) ) ) { $rows = array( $rows ); } - $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES '; - $first = true; foreach ( $rows as $row ) { - if ( $first ) { - $first = false; - } else { - $sql .= ','; + # Delete rows which collide + if ( $uniqueIndexes ) { + $sql = "DELETE FROM $table WHERE "; + $first = true; + foreach ( $uniqueIndexes as $index ) { + if ( $first ) { + $first = false; + $sql .= "("; + } else { + $sql .= ') OR ('; + } + if ( is_array( $index ) ) { + $first2 = true; + foreach ( $index as $col ) { + if ( $first2 ) { + $first2 = false; + } else { + $sql .= ' AND '; + } + $sql .= $col . '=' . $this->addQuotes( $row[$col] ); + } + } else { + $sql .= $index . '=' . $this->addQuotes( $row[$index] ); + } + } + $sql .= ')'; + $this->query( $sql, $fname ); } - $sql .= '(' . $this->makeList( $row ) . ')'; + + # Now insert the row + $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) . ') VALUES (' . + $this->makeList( $row, LIST_COMMA ) . ')'; + $this->query( $sql, $fname ); } - return $this->query( $sql, $fname ); } - /** - * DELETE where the condition is a join - * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects - * - * For safety, an empty $conds will not delete everything. If you want to delete all rows where the - * join condition matches, set $conds='*' - * - * DO NOT put the join condition in $conds - * - * @param $delTable String: The table to delete from. - * @param $joinTable String: The other table. - * @param $delVar String: The variable to join on, in the first table. - * @param $joinVar String: The variable to join on, in the second table. - * @param $conds Array: Condition array of field names mapped to variables, ANDed together in the WHERE clause - * @param $fname String: Calling function name - */ - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) { + # DELETE where the condition is a join + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseMssql::deleteJoin" ) { if ( !$conds ) { - throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' ); + throw new DBUnexpectedError( $this, 'DatabaseMssql::deleteJoin() called with empty $conds' ); } $delTable = $this->tableName( $delTable ); $joinTable = $this->tableName( $joinTable ); - $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar "; + $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable "; if ( $conds != '*' ) { - $sql .= ' AND ' . $this->makeList( $conds, LIST_AND ); + $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND ); } + $sql .= ')'; - return $this->query( $sql, $fname ); + $this->query( $sql, $fname ); } - /** - * Returns the size of a text field, or -1 for "unlimited" - */ + # Returns the size of a text field, or -1 for "unlimited" function textFieldSize( $table, $field ) { $table = $this->tableName( $table ); - $sql = "SELECT TOP 1 * FROM $table;"; - $res = $this->query( $sql, 'Database::textFieldSize' ); - $row = $this->fetchObject( $res ); - $this->freeResult( $res ); - - $m = array(); - if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) { - $size = $m[1]; + $sql = "SELECT CHARACTER_MAXIMUM_LENGTH,DATA_TYPE FROM INFORMATION_SCHEMA.Columns + WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'"; + $res = $this->query( $sql ); + $row = $this->fetchRow( $res ); + $size = -1; + if ( strtolower( $row['DATA_TYPE'] ) != 'text' ) $size = $row['CHARACTER_MAXIMUM_LENGTH']; + return $size; + } + + /** + * Construct a LIMIT query with optional offset + * This is used for query pages + * $sql string SQL query we will append the limit too + * $limit integer the SQL limit + * $offset integer the SQL offset (default false) + */ + function limitResult( $sql, $limit, $offset = false ) { + if ( $offset === false || $offset == 0 ) { + if ( strpos( $sql, "SELECT" ) === false ) { + return "TOP {$limit} " . $sql; + } else { + return preg_replace( '/\bSELECT(\s*DISTINCT)?\b/Dsi', 'SELECT$1 TOP ' . $limit, $sql, 1 ); + } } else { - $size = -1; + $sql = ' + SELECT * FROM ( + SELECT sub2.*, ROW_NUMBER() OVER(ORDER BY sub2.line2) AS line3 FROM ( + SELECT 1 AS line2, sub1.* FROM (' . $sql . ') AS sub1 + ) as sub2 + ) AS sub3 + WHERE line3 BETWEEN ' . ( $offset + 1 ) . ' AND ' . ( $offset + $limit ); + return $sql; } - return $size; + } + + // If there is a limit clause, parse it, strip it, and pass the remaining sql through limitResult() + // with the appropriate parameters. Not the prettiest solution, but better than building a whole new parser. + // This exists becase there are still too many extensions that don't use dynamic sql generation. + function LimitToTopN( $sql ) { + // Matches: LIMIT {[offset,] row_count | row_count OFFSET offset} + $pattern = '/\bLIMIT\s+((([0-9]+)\s*,\s*)?([0-9]+)(\s+OFFSET\s+([0-9]+))?)/i'; + if ( preg_match( $pattern, $sql, $matches ) ) { + // row_count = $matches[4] + $row_count = $matches[4]; + // offset = $matches[3] OR $matches[6] + $offset = $matches[3] or + $offset = $matches[6] or + $offset = false; + + // strip the matching LIMIT clause out + $sql = str_replace( $matches[0], '', $sql ); + return $this->limitResult( $sql, $row_count, $offset ); + } + return $sql; + } + + // 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 ); } /** - * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise + * @return string wikitext of a link to the server software's web site */ - function lowPriorityOption() { - return 'LOW_PRIORITY'; + public static function getSoftwareLink() { + return "[http://www.microsoft.com/sql/ MS SQL Server]"; } /** - * INSERT SELECT wrapper - * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...) - * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes() - * $conds may be "*" to copy the whole table - * srcTable may be an array of tables. + * @return string Version information from the database */ - function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect', - $insertOptions = array(), $selectOptions = array() ) - { - $destTable = $this->tableName( $destTable ); - if ( is_array( $insertOptions ) ) { - $insertOptions = implode( ' ', $insertOptions ); - } - if( !is_array( $selectOptions ) ) { - $selectOptions = array( $selectOptions ); - } - list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions ); - if( is_array( $srcTable ) ) { - $srcTable = implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) ); - } else { - $srcTable = $this->tableName( $srcTable ); - } - $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' . - " SELECT $startOpts " . implode( ',', $varMap ) . - " FROM $srcTable $useIndex "; - if ( $conds != '*' ) { - $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); + function getServerVersion() { + $server_info = sqlsrv_server_info( $this->mConn ); + $version = 'Error'; + if ( isset( $server_info['SQLServerVersion'] ) ) $version = $server_info['SQLServerVersion']; + return $version; + } + + function tableExists ( $table, $schema = false ) { + $res = sqlsrv_query( $this->mConn, "SELECT * FROM information_schema.tables + WHERE table_type='BASE TABLE' AND table_name = '$table'" ); + if ( $res === false ) { + print( "Error in tableExists query: " . $this->getErrors() ); + return false; } - $sql .= " $tailOpts"; - return $this->query( $sql, $fname ); + if ( sqlsrv_fetch( $res ) ) + return true; + else + return false; } /** - * Construct a LIMIT query with optional offset - * This is used for query pages - * $sql string SQL query we will append the limit to - * $limit integer the SQL limit - * $offset integer the SQL offset (default false) + * Query whether a given column exists in the mediawiki schema */ - function limitResult($sql, $limit, $offset=false) { - if( !is_numeric($limit) ) { - throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" ); + function fieldExists( $table, $field, $fname = 'DatabaseMssql::fieldExists' ) { + $table = $this->tableName( $table ); + $res = sqlsrv_query( $this->mConn, "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.Columns + WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); + if ( $res === false ) { + print( "Error in fieldExists query: " . $this->getErrors() ); + return false; } - if ($offset) { - throw new DBUnexpectedError( $this, 'Database::limitResult called with non-zero offset which is not supported yet' ); - } else { - $sql = ereg_replace("^SELECT", "SELECT TOP $limit", $sql); + if ( sqlsrv_fetch( $res ) ) + return true; + else + return false; + } + + function fieldInfo( $table, $field ) { + $table = $this->tableName( $table ); + $res = sqlsrv_query( $this->mConn, "SELECT * FROM INFORMATION_SCHEMA.Columns + WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field'" ); + if ( $res === false ) { + print( "Error in fieldInfo query: " . $this->getErrors() ); + return false; } - return $sql; + $meta = $this->fetchRow( $res ); + if ( $meta ) { + return new MssqlField( $meta ); + } + return false; + } + + public function unixTimestamp( $field ) { + return "DATEDIFF(s,CONVERT(datetime,'1/1/1970'),$field)"; } /** - * Should determine if the last failure was due to a deadlock - * @return bool + * Begin a transaction, committing any previously open transaction */ - function wasDeadlock() { - return $this->lastErrno() == 1205; + function begin( $fname = 'DatabaseMssql::begin' ) { + sqlsrv_begin_transaction( $this->mConn ); + $this->mTrxLevel = 1; } /** - * Return MW-style timestamp used for MySQL schema + * End a transaction */ - function timestamp( $ts=0 ) { - return wfTimestamp(TS_MW,$ts); + function commit( $fname = 'DatabaseMssql::commit' ) { + sqlsrv_commit( $this->mConn ); + $this->mTrxLevel = 0; } /** - * Local database timestamp format or null + * Rollback a transaction. + * No-op on non-transactional databases. */ - function timestampOrNull( $ts = null ) { - if( is_null( $ts ) ) { - return null; - } else { - return $this->timestamp( $ts ); + function rollback( $fname = 'DatabaseMssql::rollback' ) { + sqlsrv_rollback( $this->mConn ); + $this->mTrxLevel = 0; + } + + function setup_database() { + global $wgDBuser; + + // Make sure that we can write to the correct schema + $ctest = "mediawiki_test_table"; + if ( $this->tableExists( $ctest ) ) { + $this->doQuery( "DROP TABLE $ctest" ); + } + $SQL = "CREATE TABLE $ctest (a int)"; + $res = $this->doQuery( $SQL ); + if ( !$res ) { + print "FAILED. Make sure that the user " . htmlspecialchars( $wgDBuser ) . " can write to the database\n"; + dieout( ); } + $this->doQuery( "DROP TABLE $ctest" ); + + $res = $this->sourceFile( "../maintenance/mssql/tables.sql" ); + if ( $res !== true ) { + echo " FAILED"; + dieout( htmlspecialchars( $res ) ); + } + + # Avoid the non-standard "REPLACE INTO" syntax + $f = fopen( "../maintenance/interwiki.sql", 'r' ); + if ( $f == false ) { + dieout( "
  • Could not find the interwiki.sql file" ); + } + # We simply assume it is already empty as we have just created it + $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES "; + while ( ! feof( $f ) ) { + $line = fgets( $f, 1024 ); + $matches = array(); + if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) { + continue; + } + $this->query( "$SQL $matches[1],$matches[2])" ); + } + print " (table interwiki successfully populated)...\n"; + + $this->commit(); } /** - * @return string wikitext of a link to the server software's web site + * 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 */ - function getSoftwareLink() { - return "[http://www.microsoft.com/sql/default.mspx Microsoft SQL Server 2005 Home]"; + private function escapeIdentifier( $identifier ) { + if ( strlen( $identifier ) == 0 ) { + throw new MWException( "An identifier must not be empty" ); + } + if ( strlen( $identifier ) > 128 ) { + throw new MWException( "The identifier '$identifier' is too long (max. 128)" ); + } + if ( ( strpos( $identifier, '[' ) !== false ) || ( strpos( $identifier, ']' ) !== false ) ) { + // It may be allowed if you quoted with double quotation marks, but that would break if QUOTED_IDENTIFIER is OFF + throw new MWException( "You can't use square brackers in the identifier '$identifier'" ); + } + return "[$identifier]"; } /** - * @return string Version information from the database + * Initial setup. + * Precondition: This object is connected as the superuser. + * Creates the database, schema, user and login. */ - function getServerVersion() { - $row = mssql_fetch_row(mssql_query('select @@VERSION')); - return ereg("^(.+[0-9]+\\.[0-9]+\\.[0-9]+) ",$row[0],$m) ? $m[1] : $row[0]; + function initial_setup( $dbName, $newUser, $loginPassword ) { + $dbName = $this->escapeIdentifier( $dbName ); + + // It is not clear what can be used as a login, + // From http://msdn.microsoft.com/en-us/library/ms173463.aspx + // a sysname may be the same as an identifier. + $newUser = $this->escapeIdentifier( $newUser ); + $loginPassword = $this->addQuotes( $loginPassword ); + + $this->doQuery("CREATE DATABASE $dbName;"); + $this->doQuery("USE $dbName;"); + $this->doQuery("CREATE SCHEMA $dbName;"); + $this->doQuery(" + CREATE + LOGIN $newUser + WITH + PASSWORD=$loginPassword + ; + "); + $this->doQuery(" + CREATE + USER $newUser + FOR + LOGIN $newUser + WITH + DEFAULT_SCHEMA=$dbName + ; + "); + $this->doQuery(" + GRANT + BACKUP DATABASE, + BACKUP LOG, + CREATE DEFAULT, + CREATE FUNCTION, + CREATE PROCEDURE, + CREATE RULE, + CREATE TABLE, + CREATE VIEW, + CREATE FULLTEXT CATALOG + ON + DATABASE::$dbName + TO $newUser + ; + "); + $this->doQuery(" + GRANT + CONTROL + ON + SCHEMA::$dbName + TO $newUser + ; + "); + + } - function limitResultForUpdate($sql, $num) { - return $sql; + function encodeBlob( $b ) { + // we can't have zero's and such, this is a simple encoding to make sure we don't barf + return base64_encode( $b ); + } + + function decodeBlob( $b ) { + // we can't have zero's and such, this is a simple encoding to make sure we don't barf + return base64_decode( $b ); } /** - * How lagged is this slave? + * @private */ - public function getLag() { - return 0; + function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) { + $ret = array(); + $retJOIN = array(); + $use_index_safe = is_array( $use_index ) ? $use_index : array(); + $join_conds_safe = is_array( $join_conds ) ? $join_conds : array(); + foreach ( $tables as $table ) { + // Is there a JOIN and INDEX clause for this table? + if ( isset( $join_conds_safe[$table] ) && isset( $use_index_safe[$table] ) ) { + $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table ); + $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) ); + $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')'; + $retJOIN[] = $tableClause; + // Is there an INDEX clause? + } else if ( isset( $use_index_safe[$table] ) ) { + $tableClause = $this->tableName( $table ); + $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) ); + $ret[] = $tableClause; + // Is there a JOIN clause? + } else if ( isset( $join_conds_safe[$table] ) ) { + $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table ); + $tableClause .= ' ON (' . $this->makeList( (array)$join_conds_safe[$table][1], LIST_AND ) . ')'; + $retJOIN[] = $tableClause; + } else { + $tableClause = $this->tableName( $table ); + $ret[] = $tableClause; + } + } + // We can't separate explicit JOIN clauses with ',', use ' ' for those + $straightJoins = !empty( $ret ) ? implode( ',', $ret ) : ""; + $otherJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : ""; + // Compile our final table clause + return implode( ' ', array( $straightJoins, $otherJoins ) ); + } + + function strencode( $s ) { # Should not be called by us + return str_replace( "'", "''", $s ); + } + + function addQuotes( $s ) { + if ( $s instanceof Blob ) { + return "'" . $s->fetch( $s ) . "'"; + } else { + return parent::addQuotes( $s ); + } + } + + function selectDB( $db ) { + return ( $this->query( "SET DATABASE $db" ) !== false ); } /** - * Called by the installer script - * - this is the same way as DatabasePostgresql.php, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile) + * @private + * + * @param $options Array: an associative array of options to be turned into + * an SQL query, valid keys are listed in the function. + * @return Array */ - public function setup_database() { - global $IP,$wgDBTableOptions; - $wgDBTableOptions = ''; - $mysql_tmpl = "$IP/maintenance/tables.sql"; - $mysql_iw = "$IP/maintenance/interwiki.sql"; - $mssql_tmpl = "$IP/maintenance/mssql/tables.sql"; - - # Make an MSSQL template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db) - if (!file_exists($mssql_tmpl)) { # todo: make this conditional again - $sql = file_get_contents($mysql_tmpl); - $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments - $sql = preg_replace('/^\s*(UNIQUE )?(INDEX|KEY|FULLTEXT).+?$/m', '', $sql); # These indexes should be created with a CREATE INDEX query - $sql = preg_replace('/(\sKEY) [^\(]+\(/is', '$1 (', $sql); # "KEY foo (foo)" should just be "KEY (foo)" - $sql = preg_replace('/(varchar\([0-9]+\))\s+binary/i', '$1', $sql); # "varchar(n) binary" cannot be followed by "binary" - $sql = preg_replace('/(var)?binary\(([0-9]+)\)/ie', '"varchar(".strlen(pow(2,$2)).")"', $sql); # use varchar(chars) not binary(bits) - $sql = preg_replace('/ (var)?binary/i', ' varchar', $sql); # use varchar not binary - $sql = preg_replace('/(varchar\([0-9]+\)(?! N))/', '$1 NULL', $sql); # MSSQL complains if NULL is put into a varchar - #$sql = preg_replace('/ binary/i',' varchar',$sql); # MSSQL binary's can't be assigned with strings, so use varchar's instead - #$sql = preg_replace('/(binary\([0-9]+\) (NOT NULL )?default) [\'"].*?[\'"]/i','$1 0',$sql); # binary default cannot be string - $sql = preg_replace('/[a-z]*(blob|text)([ ,])/i', 'text$2', $sql); # no BLOB types in MSSQL - $sql = preg_replace('/\).+?;/',');', $sql); # remove all table options - $sql = preg_replace('/ (un)?signed/i', '', $sql); - $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's - $sql = str_replace(' bool ', ' bit ', $sql); - $sql = str_replace('auto_increment', 'IDENTITY(1,1)', $sql); - #$sql = preg_replace('/NOT NULL(?! IDENTITY)/', 'NULL', $sql); # Allow NULL's for non IDENTITY columns - - # Tidy up and write file - $sql = preg_replace('/,\s*\)/s', "\n)", $sql); # Remove spurious commas left after INDEX removals - $sql = preg_replace('/^\s*^/m', '', $sql); # Remove empty lines - $sql = preg_replace('/;$/m', ";\n", $sql); # Separate each statement with an empty line - file_put_contents($mssql_tmpl, $sql); - } - - # Parse the MSSQL template replacing inline variables such as /*$wgDBprefix*/ - $err = $this->sourceFile($mssql_tmpl); - if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__); - - # Use DatabasePostgres's code to populate interwiki from MySQL template - $f = fopen($mysql_iw,'r'); - if ($f == false) dieout("
  • Could not find the interwiki.sql file"); - $sql = "INSERT INTO {$this->mTablePrefix}interwiki(iw_prefix,iw_url,iw_local) VALUES "; - while (!feof($f)) { - $line = fgets($f,1024); - $matches = array(); - if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue; - $this->query("$sql $matches[1],$matches[2])"); + function makeSelectOptions( $options ) { + $tailOpts = ''; + $startOpts = ''; + + $noKeyOptions = array(); + foreach ( $options as $key => $option ) { + if ( is_numeric( $key ) ) { + $noKeyOptions[$option] = true; + } } + + if ( isset( $options['GROUP BY'] ) ) $tailOpts .= " GROUP BY {$options['GROUP BY']}"; + if ( isset( $options['HAVING'] ) ) $tailOpts .= " HAVING {$options['GROUP BY']}"; + if ( isset( $options['ORDER BY'] ) ) $tailOpts .= " ORDER BY {$options['ORDER BY']}"; + + if ( isset( $noKeyOptions['DISTINCT'] ) && isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT'; + + // we want this to be compatible with the output of parent::makeSelectOptions() + return array( $startOpts, '' , $tailOpts, '' ); + } + + /** + * Get the type of the DBMS, as it appears in $wgDBtype. + */ + function getType(){ + return 'mssql'; } - + + function buildConcat( $stringList ) { + return implode( ' + ', $stringList ); + } + public function getSearchEngine() { - return "SearchEngineDummy"; + return "SearchMssql"; + } + +} // end DatabaseMssql class + +/** + * Utility class. + * + * @ingroup Database + */ +class MssqlField implements Field { + private $name, $tablename, $default, $max_length, $nullable, $type; + function __construct ( $info ) { + $this->name = $info['COLUMN_NAME']; + $this->tablename = $info['TABLE_NAME']; + $this->default = $info['COLUMN_DEFAULT']; + $this->max_length = $info['CHARACTER_MAXIMUM_LENGTH']; + $this->nullable = ( strtolower( $info['IS_NULLABLE'] ) == 'no' ) ? false:true; + $this->type = $info['DATA_TYPE']; + } + function name() { + return $this->name; + } + + function tableName() { + return $this->tableName; + } + + function defaultValue() { + return $this->default; + } + + function maxLength() { + return $this->max_length; + } + + function isNullable() { + return $this->nullable; + } + + function type() { + return $this->type; } } /** + * The MSSQL PHP driver doesn't support sqlsrv_num_rows, so we recall all rows into an array and maintain our + * own cursor index into that array...This is similar to the way the Oracle driver handles this same issue + * * @ingroup Database */ -class MSSQLField extends MySQLField { +class MssqlResult { + + public function __construct( $queryresult = false ) { + $this->mCursor = 0; + $this->mRows = array(); + $this->mNumFields = sqlsrv_num_fields( $queryresult ); + $this->mFieldMeta = sqlsrv_field_metadata( $queryresult ); + while ( $row = sqlsrv_fetch_array( $queryresult, SQLSRV_FETCH_ASSOC ) ) { + if ( $row !== null ) { + foreach ( $row as $k => $v ) { + if ( is_object( $v ) && method_exists( $v, 'format' ) ) {// DateTime Object + $row[$k] = $v->format( "Y-m-d\TH:i:s\Z" ); + } + } + $this->mRows[] = $row;// read results into memory, cursors are not supported + } + } + $this->mRowCount = count( $this->mRows ); + sqlsrv_free_stmt( $queryresult ); + } + + private function array_to_obj( $array, &$obj ) { + foreach ( $array as $key => $value ) { + if ( is_array( $value ) ) { + $obj->$key = new stdClass(); + $this->array_to_obj( $value, $obj->$key ); + } else { + if ( !empty( $key ) ) { + $obj->$key = $value; + } + } + } + return $obj; + } - function __construct() { + public function fetch( $mode = SQLSRV_FETCH_BOTH, $object_class = 'stdClass' ) { + if ( $this->mCursor >= $this->mRowCount || $this->mRowCount == 0 ) { + return false; + } + $arrNum = array(); + if ( $mode == SQLSRV_FETCH_NUMERIC || $mode == SQLSRV_FETCH_BOTH ) { + foreach ( $this->mRows[$this->mCursor] as $value ) { + $arrNum[] = $value; + } + } + switch( $mode ) { + case SQLSRV_FETCH_ASSOC: + $ret = $this->mRows[$this->mCursor]; + break; + case SQLSRV_FETCH_NUMERIC: + $ret = $arrNum; + break; + case 'OBJECT': + $o = new $object_class; + $ret = $this->array_to_obj( $this->mRows[$this->mCursor], $o ); + break; + case SQLSRV_FETCH_BOTH: + default: + $ret = $this->mRows[$this->mCursor] + $arrNum; + break; } - static function fromText($db, $table, $field) { - $n = new MSSQLField; - $n->name = $field; - $n->tablename = $table; - return $n; + $this->mCursor++; + return $ret; + } + + public function get( $pos, $fld ) { + return $this->mRows[$pos][$fld]; + } + + public function numrows() { + return $this->mRowCount; + } + + public function seek( $iRow ) { + $this->mCursor = min( $iRow, $this->mRowCount ); + } + + public function numfields() { + return $this->mNumFields; + } + + public function fieldname( $nr ) { + $arrKeys = array_keys( $this->mRows[0] ); + return $arrKeys[$nr]; + } + + public function fieldtype( $nr ) { + $i = 0; + $intType = -1; + foreach ( $this->mFieldMeta as $meta ) { + if ( $nr == $i ) { + $intType = $meta['Type']; + break; + } + $i++; } + // http://msdn.microsoft.com/en-us/library/cc296183.aspx contains type table + switch( $intType ) { + case SQLSRV_SQLTYPE_BIGINT: $strType = 'bigint'; break; + case SQLSRV_SQLTYPE_BINARY: $strType = 'binary'; break; + case SQLSRV_SQLTYPE_BIT: $strType = 'bit'; break; + case SQLSRV_SQLTYPE_CHAR: $strType = 'char'; break; + case SQLSRV_SQLTYPE_DATETIME: $strType = 'datetime'; break; + case SQLSRV_SQLTYPE_DECIMAL/*($precision, $scale)*/: $strType = 'decimal'; break; + case SQLSRV_SQLTYPE_FLOAT: $strType = 'float'; break; + case SQLSRV_SQLTYPE_IMAGE: $strType = 'image'; break; + case SQLSRV_SQLTYPE_INT: $strType = 'int'; break; + case SQLSRV_SQLTYPE_MONEY: $strType = 'money'; break; + case SQLSRV_SQLTYPE_NCHAR/*($charCount)*/: $strType = 'nchar'; break; + case SQLSRV_SQLTYPE_NUMERIC/*($precision, $scale)*/: $strType = 'numeric'; break; + case SQLSRV_SQLTYPE_NVARCHAR/*($charCount)*/: $strType = 'nvarchar'; break; + // case SQLSRV_SQLTYPE_NVARCHAR('max'): $strType = 'nvarchar(MAX)'; break; + case SQLSRV_SQLTYPE_NTEXT: $strType = 'ntext'; break; + case SQLSRV_SQLTYPE_REAL: $strType = 'real'; break; + case SQLSRV_SQLTYPE_SMALLDATETIME: $strType = 'smalldatetime'; break; + case SQLSRV_SQLTYPE_SMALLINT: $strType = 'smallint'; break; + case SQLSRV_SQLTYPE_SMALLMONEY: $strType = 'smallmoney'; break; + case SQLSRV_SQLTYPE_TEXT: $strType = 'text'; break; + case SQLSRV_SQLTYPE_TIMESTAMP: $strType = 'timestamp'; break; + case SQLSRV_SQLTYPE_TINYINT: $strType = 'tinyint'; break; + case SQLSRV_SQLTYPE_UNIQUEIDENTIFIER: $strType = 'uniqueidentifier'; break; + case SQLSRV_SQLTYPE_UDT: $strType = 'UDT'; break; + case SQLSRV_SQLTYPE_VARBINARY/*($byteCount)*/: $strType = 'varbinary'; break; + // case SQLSRV_SQLTYPE_VARBINARY('max'): $strType = 'varbinary(MAX)'; break; + case SQLSRV_SQLTYPE_VARCHAR/*($charCount)*/: $strType = 'varchar'; break; + // case SQLSRV_SQLTYPE_VARCHAR('max'): $strType = 'varchar(MAX)'; break; + case SQLSRV_SQLTYPE_XML: $strType = 'xml'; break; + default: $strType = $intType; + } + return $strType; + } -} // end DatabaseMssql class + public function free() { + unset( $this->mRows ); + return; + } +} diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php index ea7ef5b9..ed276ec5 100644 --- a/includes/db/DatabaseMysql.php +++ b/includes/db/DatabaseMysql.php @@ -1,4 +1,11 @@ mPassword = $password; $this->mDBname = $dbName; - $success = false; - wfProfileIn("dbconnect-$server"); # The kernel's default SYN retransmission period is far too slow for us, @@ -72,10 +74,10 @@ class DatabaseMysql extends DatabaseBase { # Create a new connection... $this->mConn = mysql_connect( $realServer, $user, $password, true ); } - if ($this->mConn === false) { + #if ( $this->mConn === false ) { #$iplus = $i + 1; - #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); - } + #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); + #} } $phpError = $this->restoreErrorHandler(); # Always log connection errors @@ -88,9 +90,8 @@ class DatabaseMysql extends DatabaseBase { wfDebug( "DB connection error\n" ); wfDebug( "Server: $server, User: $user, Password: " . substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" ); - $success = false; } - + wfProfileOut("dbconnect-$server"); if ( $dbName != '' && $this->mConn !== false ) { @@ -114,9 +115,15 @@ class DatabaseMysql extends DatabaseBase { 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__ ); } - // Turn off strict mode - $this->query( "SET sql_mode = ''", __METHOD__ ); } // Turn off strict mode if it is on @@ -233,34 +240,35 @@ class DatabaseMysql extends DatabaseBase { } function affectedRows() { return mysql_affected_rows( $this->mConn ); } - + /** * Estimate rows in dataset * Returns estimated count, based on EXPLAIN output * Takes same arguments as Database::select() */ - public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { + public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'DatabaseMysql::estimateRowCount', $options = array() ) { $options['EXPLAIN'] = true; $res = $this->select( $table, $vars, $conds, $fname, $options ); - if ( $res === false ) + if ( $res === false ) { return false; + } if ( !$this->numRows( $res ) ) { - $this->freeResult($res); return 0; } $rows = 1; - while( $plan = $this->fetchObject( $res ) ) { + foreach ( $res as $plan ) { $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero } - - $this->freeResult($res); - return $rows; + return $rows; } function fieldInfo( $table, $field ) { $table = $this->tableName( $table ); - $res = $this->query( "SELECT * FROM $table LIMIT 1" ); + $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true ); + if ( !$res ) { + return false; + } $n = mysql_num_fields( $res->result ); for( $i = 0; $i < $n; $i++ ) { $meta = mysql_fetch_field( $res->result, $i ); @@ -271,32 +279,108 @@ class DatabaseMysql extends DatabaseBase { return false; } + /** + * Get information about an index into an object + * Returns false if the index does not exist + */ + function indexInfo( $table, $index, $fname = 'DatabaseMysql::indexInfo' ) { + # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not. + # SHOW INDEX should work for 3.x and up: + # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html + $table = $this->tableName( $table ); + $index = $this->indexName( $index ); + $sql = 'SHOW INDEX FROM ' . $table; + $res = $this->query( $sql, $fname ); + + if ( !$res ) { + return null; + } + + $result = array(); + + foreach ( $res as $row ) { + if ( $row->Key_name == $index ) { + $result[] = $row; + } + } + + return empty( $result ) ? false : $result; + } + function selectDB( $db ) { $this->mDBname = $db; return mysql_select_db( $db, $this->mConn ); } function strencode( $s ) { - return mysql_real_escape_string( $s, $this->mConn ); + $sQuoted = mysql_real_escape_string( $s, $this->mConn ); + + if($sQuoted === false) { + $this->ping(); + $sQuoted = mysql_real_escape_string( $s, $this->mConn ); + } + return $sQuoted; + } + + /** + * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes". + */ + public function addIdentifierQuotes( $s ) { + return "`" . $this->strencode( $s ) . "`"; } function ping() { - if( !function_exists( 'mysql_ping' ) ) { - wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" ); - return true; - } $ping = mysql_ping( $this->mConn ); if ( $ping ) { return true; } - // Need to reconnect manually in MySQL client 5.0.13+ - if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) { - mysql_close( $this->mConn ); - $this->mOpened = false; - $this->mConn = false; - $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname ); - return true; + mysql_close( $this->mConn ); + $this->mOpened = false; + $this->mConn = false; + $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname ); + return true; + } + + /** + * Returns slave lag. + * At the moment, this will only work if the DB user has the PROCESS privilege + * @result int + */ + function getLag() { + if ( !is_null( $this->mFakeSlaveLag ) ) { + wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" ); + return $this->mFakeSlaveLag; + } + $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ ); + if( !$res ) { + return false; + } + # Find slave SQL thread + foreach( $res as $row ) { + /* This should work for most situations - when default db + * for thread is not specified, it had no events executed, + * and therefore it doesn't know yet how lagged it is. + * + * Relay log I/O thread does not select databases. + */ + if ( $row->User == 'system user' && + $row->State != 'Waiting for master to send event' && + $row->State != 'Connecting to master' && + $row->State != 'Queueing master event to the relay log' && + $row->State != 'Waiting for master update' && + $row->State != 'Requesting binlog dump' && + $row->State != 'Waiting to reconnect after a failed master event read' && + $row->State != 'Reconnecting after a failed master event read' && + $row->State != 'Registering slave on master' + ) { + # This is it, return the time (except -ve) + if ( $row->Time > 0x7fffffff ) { + return false; + } else { + return $row->Time; + } + } } return false; } @@ -313,7 +397,7 @@ class DatabaseMysql extends DatabaseBase { return 'LOW_PRIORITY'; } - function getSoftwareLink() { + public static function getSoftwareLink() { return '[http://www.mysql.com/ MySQL]'; } @@ -330,7 +414,6 @@ class DatabaseMysql extends DatabaseBase { $lockName = $this->addQuotes( $lockName ); $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method ); $row = $this->fetchObject( $result ); - $this->freeResult( $result ); if( $row->lockstatus == 1 ) { return true; @@ -340,6 +423,9 @@ class DatabaseMysql extends DatabaseBase { } } + /** + * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock + */ public function unlock( $lockName, $method ) { $lockName = $this->addQuotes( $lockName ); $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method ); @@ -351,8 +437,8 @@ class DatabaseMysql extends DatabaseBase { $items = array(); foreach( $write as $table ) { - $tbl = $this->tableName( $table ) . - ( $lowPriority ? ' LOW_PRIORITY' : '' ) . + $tbl = $this->tableName( $table ) . + ( $lowPriority ? ' LOW_PRIORITY' : '' ) . ' WRITE'; $items[] = $tbl; } @@ -367,6 +453,16 @@ class DatabaseMysql extends DatabaseBase { $this->query( "UNLOCK TABLES", $method ); } + /** + * Get search engine class. All subclasses of this + * need to implement this if they wish to use searching. + * + * @return String + */ + public function getSearchEngine() { + return 'SearchMySQL'; + } + public function setBigSelects( $value = true ) { if ( $value === 'default' ) { if ( $this->mDefaultBigSelects === null ) { @@ -382,7 +478,10 @@ class DatabaseMysql extends DatabaseBase { $this->query( "SET sql_big_selects=$encValue", __METHOD__ ); } - + public function unixTimestamp( $field ) { + return "UNIX_TIMESTAMP($field)"; + } + /** * Determines if the last failure was due to a deadlock */ @@ -391,7 +490,7 @@ class DatabaseMysql extends DatabaseBase { } /** - * Determines if the last query error was something that should be dealt + * Determines if the last query error was something that should be dealt * with by pinging the connection and reissuing the query */ function wasErrorReissuable() { @@ -402,7 +501,7 @@ class DatabaseMysql extends DatabaseBase { * Determines if the last failure was due to the database being read-only. */ function wasReadOnlyError() { - return $this->lastErrno() == 1223 || + return $this->lastErrno() == 1223 || ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false ); } @@ -420,7 +519,7 @@ class DatabaseMysql extends DatabaseBase { $res = $this->query( "SHOW CREATE TABLE $oldName" ); $row = $this->fetchRow( $res ); $oldQuery = $row[1]; - $query = preg_replace( '/CREATE TABLE `(.*?)`/', + $query = preg_replace( '/CREATE TABLE `(.*?)`/', "CREATE $tmp TABLE `$newName`", $oldQuery ); if ($oldQuery === $query) { # Couldn't do replacement @@ -432,6 +531,11 @@ class DatabaseMysql extends DatabaseBase { $this->query( $query, $fname ); } + protected function getDefaultSchemaVars() { + $vars = parent::getDefaultSchemaVars(); + $vars['wgDBTableOptions'] = $GLOBALS['wgDBTableOptions']; + return $vars; + } } /** @@ -439,6 +543,56 @@ class DatabaseMysql extends DatabaseBase { */ class Database extends DatabaseMysql {} +/** + * Utility class. + * @ingroup Database + */ +class MySQLField implements Field { + private $name, $tablename, $default, $max_length, $nullable, + $is_pk, $is_unique, $is_multiple, $is_key, $type; + + function __construct ( $info ) { + $this->name = $info->name; + $this->tablename = $info->table; + $this->default = $info->def; + $this->max_length = $info->max_length; + $this->nullable = !$info->not_null; + $this->is_pk = $info->primary_key; + $this->is_unique = $info->unique_key; + $this->is_multiple = $info->multiple_key; + $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple ); + $this->type = $info->type; + } + + function name() { + return $this->name; + } + + function tableName() { + return $this->tableName; + } + + function type() { + return $this->type; + } + + function isNullable() { + return $this->nullable; + } + + function defaultValue() { + return $this->default; + } + + function isKey() { + return $this->is_key; + } + + function isMultipleKey() { + return $this->is_multiple; + } +} + class MySQLMasterPos { var $file, $pos; diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php index bd60bbf8..4fe3e980 100644 --- a/includes/db/DatabaseOracle.php +++ b/includes/db/DatabaseOracle.php @@ -1,24 +1,10 @@ mData = $data; - } - - function getData() { - return $this->mData; - } -} /** * The oci8 extension is fairly weak and doesn't support oci_num_rows, among @@ -29,15 +15,15 @@ class ORABlob { class ORAResult { private $rows; private $cursor; - private $stmt; private $nrows; + + private $columns = array(); - private $unique; private function array_unique_md( $array_in ) { $array_out = array(); $array_hashes = array(); - foreach ( $array_in as $key => $item ) { + foreach ( $array_in as $item ) { $hash = md5( serialize( $item ) ); if ( !isset( $array_hashes[$hash] ) ) { $array_hashes[$hash] = $hash; @@ -53,7 +39,8 @@ class ORAResult { if ( ( $this->nrows = oci_fetch_all( $stmt, $this->rows, 0, - 1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM ) ) === false ) { $e = oci_error( $stmt ); - $db->reportQueryError( $e['message'], $e['code'], '', __FUNCTION__ ); + $db->reportQueryError( $e['message'], $e['code'], '', __METHOD__ ); + $this->free(); return; } @@ -62,12 +49,18 @@ class ORAResult { $this->nrows = count( $this->rows ); } + if ($this->nrows > 0) { + foreach ( $this->rows[0] as $k => $v ) { + $this->columns[$k] = strtolower( oci_field_name( $stmt, $k + 1 ) ); + } + } + $this->cursor = 0; - $this->stmt = $stmt; + oci_free_statement( $stmt ); } public function free() { - oci_free_statement( $this->stmt ); + unset($this->db); } public function seek( $row ) { @@ -79,7 +72,7 @@ class ORAResult { } public function numFields() { - return oci_num_fields( $this->stmt ); + return count($this->columns); } public function fetchObject() { @@ -89,7 +82,7 @@ class ORAResult { $row = $this->rows[$this->cursor++]; $ret = new stdClass(); foreach ( $row as $k => $v ) { - $lc = strtolower( oci_field_name( $this->stmt, $k + 1 ) ); + $lc = $this->columns[$k]; $ret->$lc = $v; } @@ -104,7 +97,7 @@ class ORAResult { $row = $this->rows[$this->cursor++]; $ret = array(); foreach ( $row as $k => $v ) { - $lc = strtolower( oci_field_name( $this->stmt, $k + 1 ) ); + $lc = $this->columns[$k]; $ret[$lc] = $v; $ret[$k] = $v; } @@ -116,7 +109,7 @@ class ORAResult { * Utility class. * @ingroup Database */ -class ORAField { +class ORAField implements Field { private $name, $tablename, $default, $max_length, $nullable, $is_pk, $is_unique, $is_multiple, $is_key, $type; @@ -149,7 +142,7 @@ class ORAField { return $this->max_length; } - function nullable() { + function isNullable() { return $this->nullable; } @@ -185,11 +178,19 @@ class DatabaseOracle extends DatabaseBase { var $mFieldInfoCache = array(); function __construct( $server = false, $user = false, $password = false, $dbName = false, - $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) + $flags = 0, $tablePrefix = 'get from global' ) { $tablePrefix = $tablePrefix == 'get from global' ? $tablePrefix : strtoupper( $tablePrefix ); - parent::__construct( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix ); - wfRunHooks( 'DatabaseOraclePostInit', array( &$this ) ); + parent::__construct( $server, $user, $password, $dbName, $flags, $tablePrefix ); + wfRunHooks( 'DatabaseOraclePostInit', array( $this ) ); + } + + function __destruct() { + if ($this->mOpened) { + wfSuppressWarnings(); + $this->close(); + wfRestoreWarnings(); + } } function getType() { @@ -218,25 +219,36 @@ class DatabaseOracle extends DatabaseBase { return true; } - static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) + static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { - return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags ); + return new DatabaseOracle( $server, $user, $password, $dbName, $flags ); } /** * Usually aborts on failure - * If the failFunction is set to a non-zero integer, returns success */ function open( $server, $user, $password, $dbName ) { if ( !function_exists( 'oci_connect' ) ) { throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" ); } - $this->close(); - $this->mServer = $server; $this->mUser = $user; $this->mPassword = $password; - $this->mDBname = $dbName; + // changed internal variables functions + // mServer now holds the TNS endpoint + // mDBname is schema name if different from username + if ( !$server ) { + // backward compatibillity (server used to be null and TNS was supplied in dbname) + $this->mServer = $dbName; + $this->mDBname = $user; + } else { + $this->mServer = $server; + if ( !$dbName ) { + $this->mDBname = $user; + } else { + $this->mDBname = $dbName; + } + } if ( !strlen( $user ) ) { # e.g. the class is being loaded return; @@ -244,16 +256,18 @@ class DatabaseOracle extends DatabaseBase { $session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT; if ( $this->mFlags & DBO_DEFAULT ) { - $this->mConn = oci_new_connect( $user, $password, $dbName, $this->defaultCharset, $session_mode ); + $this->mConn = oci_new_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode ); } else { - $this->mConn = oci_connect( $user, $password, $dbName, $this->defaultCharset, $session_mode ); + $this->mConn = oci_connect( $this->mUser, $this->mPassword, $this->mServer, $this->defaultCharset, $session_mode ); } - if ( $this->mConn == false ) { - wfDebug( "DB connection error\n" ); - wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); - wfDebug( $this->lastError() . "\n" ); - return false; + if ( $this->mUser != $this->mDBname ) { + //change current schema in session + $this->selectDB( $this->mDBname ); + } + + if ( !$this->mConn ) { + throw new DBConnectionError( $this, $this->lastError() ); } $this->mOpened = true; @@ -271,6 +285,9 @@ class DatabaseOracle extends DatabaseBase { function close() { $this->mOpened = false; if ( $this->mConn ) { + if ( $this->mTrxLevel ) { + $this->commit(); + } return oci_close( $this->mConn ); } else { return true; @@ -289,9 +306,10 @@ class DatabaseOracle extends DatabaseBase { // handle some oracle specifics // remove AS column/table/subquery namings - if ( !defined( 'MEDIAWIKI_INSTALL' ) ) { + if( !$this->getFlag( DBO_DDLMODE ) ) { $sql = preg_replace( '/ as /i', ' ', $sql ); } + // Oracle has issues with UNION clause if the statement includes LOB fields // So we do a UNION ALL and then filter the results array with array_unique $union_unique = ( preg_match( '/\/\* UNION_UNIQUE \*\/ /', $sql ) != 0 ); @@ -301,23 +319,22 @@ class DatabaseOracle extends DatabaseBase { $sql = preg_replace( '/^EXPLAIN /', 'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR', $sql, 1, $explain_count ); - wfSuppressWarnings(); if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) { $e = oci_error( $this->mConn ); - $this->reportQueryError( $e['message'], $e['code'], $sql, __FUNCTION__ ); + $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); return false; } - if ( oci_execute( $stmt, $this->execFlags() ) == false ) { + if ( !oci_execute( $stmt, $this->execFlags() ) ) { $e = oci_error( $stmt ); if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) { - $this->reportQueryError( $e['message'], $e['code'], $sql, __FUNCTION__ ); + $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); return false; } } - + wfRestoreWarnings(); if ( $explain_count > 0 ) { @@ -335,43 +352,43 @@ class DatabaseOracle extends DatabaseBase { } function freeResult( $res ) { - if ( $res instanceof ORAResult ) { - $res->free(); - } else { - $res->result->free(); + if ( $res instanceof ResultWrapper ) { + $res = $res->result; } + + $res->free(); } function fetchObject( $res ) { - if ( $res instanceof ORAResult ) { - return $res->numRows(); - } else { - return $res->result->fetchObject(); + if ( $res instanceof ResultWrapper ) { + $res = $res->result; } + + return $res->fetchObject(); } function fetchRow( $res ) { - if ( $res instanceof ORAResult ) { - return $res->fetchRow(); - } else { - return $res->result->fetchRow(); + if ( $res instanceof ResultWrapper ) { + $res = $res->result; } + + return $res->fetchRow(); } function numRows( $res ) { - if ( $res instanceof ORAResult ) { - return $res->numRows(); - } else { - return $res->result->numRows(); + if ( $res instanceof ResultWrapper ) { + $res = $res->result; } + + return $res->numRows(); } function numFields( $res ) { - if ( $res instanceof ORAResult ) { - return $res->numFields(); - } else { - return $res->result->numFields(); + if ( $res instanceof ResultWrapper ) { + $res = $res->result; } + + return $res->numFields(); } function fieldName( $stmt, $n ) { @@ -456,8 +473,38 @@ class DatabaseOracle extends DatabaseBase { return $retVal; } + private function fieldBindStatement ( $table, $col, &$val, $includeCol = false ) { + $col_info = $this->fieldInfoMulti( $table, $col ); + $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; + + $bind = ''; + if ( is_numeric( $col ) ) { + $bind = $val; + $val = null; + return $bind; + } else if ( $includeCol ) { + $bind = "$col = "; + } + + if ( $val == '' && $val !== 0 && $col_type != 'BLOB' && $col_type != 'CLOB' ) { + $val = null; + } + + if ( $val === null ) { + if ( $col_info != false && $col_info->isNullable() == 0 && $col_info->defaultValue() != null ) { + $bind .= 'DEFAULT'; + } else { + $bind .= 'NULL'; + } + } else { + $bind .= ':' . $col; + } + + return $bind; + } + private function insertOneRow( $table, $row, $fname ) { - global $wgLang; + global $wgContLang; $table = $this->tableName( $table ); // "INSERT INTO tables (a, b, c)" @@ -466,18 +513,22 @@ class DatabaseOracle extends DatabaseBase { // for each value, append ":key" $first = true; - foreach ( $row as $col => $val ) { - if ( $first ) { - $sql .= $val !== null ? ':' . $col : 'NULL'; + foreach ( $row as $col => &$val ) { + if ( !$first ) { + $sql .= ', '; } else { - $sql .= $val !== null ? ', :' . $col : ', NULL'; + $first = false; } - - $first = false; + + $sql .= $this->fieldBindStatement( $table, $col, $val ); } $sql .= ')'; - $stmt = oci_parse( $this->mConn, $sql ); + if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) { + $e = oci_error( $this->mConn ); + $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; + } foreach ( $row as $col => &$val ) { $col_info = $this->fieldInfoMulti( $table, $col ); $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; @@ -486,16 +537,17 @@ class DatabaseOracle extends DatabaseBase { // do nothing ... null was inserted in statement creation } elseif ( $col_type != 'BLOB' && $col_type != 'CLOB' ) { if ( is_object( $val ) ) { - $val = $val->getData(); + $val = $val->fetch(); } if ( preg_match( '/^timestamp.*/i', $col_type ) == 1 && strtolower( $val ) == 'infinity' ) { $val = '31-12-2030 12:00:00.000000'; } - $val = ( $wgLang != null ) ? $wgLang->checkTitleEncoding( $val ) : $val; + $val = ( $wgContLang != null ) ? $wgContLang->checkTitleEncoding( $val ) : $val; if ( oci_bind_by_name( $stmt, ":$col", $val ) === false ) { - $this->reportQueryError( $this->lastErrno(), $this->lastError(), $sql, __METHOD__ ); + $e = oci_error( $stmt ); + $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); return false; } } else { @@ -504,11 +556,15 @@ class DatabaseOracle extends DatabaseBase { throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] ); } - if ( $col_type == 'BLOB' ) { // is_object($val)) { - $lob[$col]->writeTemporary( $val ); // ->getData()); - oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, SQLT_BLOB ); + if ( is_object( $val ) ) { + $val = $val->fetch(); + } + + if ( $col_type == 'BLOB' ) { + $lob[$col]->writeTemporary( $val, OCI_TEMP_BLOB ); + oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, OCI_B_BLOB ); } else { - $lob[$col]->writeTemporary( $val ); + $lob[$col]->writeTemporary( $val, OCI_TEMP_CLOB ); oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, OCI_B_CLOB ); } } @@ -516,9 +572,8 @@ class DatabaseOracle extends DatabaseBase { wfSuppressWarnings(); - if ( oci_execute( $stmt, OCI_DEFAULT ) === false ) { + if ( oci_execute( $stmt, $this->execFlags() ) === false ) { $e = oci_error( $stmt ); - if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) { $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); return false; @@ -532,7 +587,7 @@ class DatabaseOracle extends DatabaseBase { wfRestoreWarnings(); if ( isset( $lob ) ) { - foreach ( $lob as $lob_i => $lob_v ) { + foreach ( $lob as $lob_v ) { $lob_v->free(); } } @@ -560,11 +615,13 @@ class DatabaseOracle extends DatabaseBase { if ( ( $sequenceData = $this->getSequenceData( $destTable ) ) !== false && !isset( $varMap[$sequenceData['column']] ) ) + { $varMap[$sequenceData['column']] = 'GET_SEQUENCE_VALUE(\'' . $sequenceData['sequence'] . '\')'; + } // count-alias subselect fields to avoid abigious definition errors $i = 0; - foreach ( $varMap as $key => &$val ) { + foreach ( $varMap as &$val ) { $val = $val . ' field' . ( $i++ ); } @@ -640,13 +697,18 @@ class DatabaseOracle extends DatabaseBase { if ( isset( $database ) ) { $database = ( $database[0] == '"' ? $database : "\"{$database}\"" ); } - $table = ( $table[0] == '"' ? $table : "\"{$prefix}{$table}\"" ); + $table = ( $table[0] == '"') ? $table : "\"{$prefix}{$table}\"" ; $tableName = ( isset( $database ) ? "{$database}.{$table}" : "{$table}" ); return strtoupper( $tableName ); } + function tableNameInternal( $name ) { + $name = $this->tableName( $name ); + return preg_replace( '/.*\."(.*)"/', '$1', $name); + } + /** * Return the next in a sequence, save the value for retrieval via insertId() */ @@ -654,7 +716,6 @@ class DatabaseOracle extends DatabaseBase { $res = $this->query( "SELECT $seqName.nextval FROM dual" ); $row = $this->fetchRow( $res ); $this->mInsertId = $row[0]; - $this->freeResult( $res ); return $this->mInsertId; } @@ -663,7 +724,7 @@ class DatabaseOracle extends DatabaseBase { */ private function getSequenceData( $table ) { if ( $this->sequenceData == null ) { - $result = $this->query( "SELECT lower(us.sequence_name), lower(utc.table_name), lower(utc.column_name) from user_sequences us, user_tab_columns utc where us.sequence_name = utc.table_name||'_'||utc.column_name||'_SEQ'" ); + $result = $this->doQuery( 'SELECT lower(us.sequence_name), lower(utc.table_name), lower(utc.column_name) from user_sequences us, user_tab_columns utc where us.sequence_name = utc.table_name||\'_\'||utc.column_name||\'_SEQ\'' ); while ( ( $row = $result->fetchRow() ) !== false ) { $this->sequenceData[$this->tableName( $row[1] )] = array( @@ -676,15 +737,23 @@ class DatabaseOracle extends DatabaseBase { return ( isset( $this->sequenceData[$table] ) ) ? $this->sequenceData[$table] : false; } - # REPLACE query wrapper - # Oracle simulates this with a DELETE followed by INSERT - # $row is the row to insert, an associative array - # $uniqueIndexes is an array of indexes. Each element may be either a - # field name or an array of field names - # - # It may be more efficient to leave off unique indexes which are unlikely to collide. - # However if you do this, you run the risk of encountering errors which wouldn't have - # occurred in MySQL + /** + * REPLACE query wrapper + * Oracle simulates this with a DELETE followed by INSERT + * $row is the row to insert, an associative array + * $uniqueIndexes is an array of indexes. Each element may be either a + * field name or an array of field names + * + * It may be more efficient to leave off unique indexes which are unlikely to collide. + * However if you do this, you run the risk of encountering errors which wouldn't have + * occurred in MySQL. + * + * @param $table String: table name + * @param $uniqueIndexes Array: array of indexes. Each element may be + * either a field name or an array of field names + * @param $rows Array: rows to insert to $table + * @param $fname String: function name, you can use __METHOD__ here + */ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseOracle::replace' ) { $table = $this->tableName( $table ); @@ -703,9 +772,10 @@ class DatabaseOracle extends DatabaseBase { # Delete rows which collide if ( $uniqueIndexes ) { $condsDelete = array(); - foreach ( $uniqueIndexes as $index ) + foreach ( $uniqueIndexes as $index ) { $condsDelete[$index] = $row[$index]; - if (count($condsDelete) > 0) { + } + if ( count( $condsDelete ) > 0 ) { $this->delete( $table, $condsDelete, $fname ); } } @@ -720,9 +790,9 @@ class DatabaseOracle extends DatabaseBase { } # DELETE where the condition is a join - function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseOracle::deleteJoin" ) { + function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabaseOracle::deleteJoin' ) { if ( !$conds ) { - throw new DBUnexpectedError( $this, 'DatabaseOracle::deleteJoin() called with empty $conds' ); + throw new DBUnexpectedError( $this, 'DatabaseOracle::deleteJoin() called with empty $conds' ); } $delTable = $this->tableName( $delTable ); @@ -738,13 +808,8 @@ class DatabaseOracle extends DatabaseBase { # Returns the size of a text field, or -1 for "unlimited" function textFieldSize( $table, $field ) { - $fieldInfoData = $this->fieldInfo( $table, $field); - if ( $fieldInfoData->type == "varchar" ) { - $size = $row->size - 4; - } else { - $size = $row->size; - } - return $size; + $fieldInfoData = $this->fieldInfo( $table, $field ); + return $fieldInfoData->maxLength(); } function limitResult( $sql, $limit, $offset = false ) { @@ -754,40 +819,73 @@ class DatabaseOracle extends DatabaseBase { return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < (1 + $limit + $offset)"; } + function encodeBlob( $b ) { + return new Blob( $b ); + } + + function decodeBlob( $b ) { + if ( $b instanceof Blob ) { + $b = $b->fetch(); + } + return $b; + } function unionQueries( $sqls, $all ) { $glue = ' UNION ALL '; return 'SELECT * ' . ( $all ? '':'/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')' ; } + public function unixTimestamp( $field ) { + return "((trunc($field) - to_date('19700101','YYYYMMDD')) * 86400)"; + } + function wasDeadlock() { return $this->lastErrno() == 'OCI-00060'; } - function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseOracle::duplicateTableStructure' ) { - $temporary = $temporary ? 'TRUE' : 'FALSE'; - $oldName = trim(strtoupper($oldName), '"'); - $oldParts = explode('_', $oldName); + global $wgDBprefix; - $newName = trim(strtoupper($newName), '"'); - $newParts = explode('_', $newName); - - $oldPrefix = ''; - $newPrefix = ''; - for ($i = count($oldParts)-1; $i >= 0; $i--) { - if ($oldParts[$i] != $newParts[$i]) { - $oldPrefix = implode('_', $oldParts).'_'; - $newPrefix = implode('_', $newParts).'_'; - break; - } - unset($oldParts[$i]); - unset($newParts[$i]); + $temporary = $temporary ? 'TRUE' : 'FALSE'; + + $newName = trim( strtoupper( $newName ), '"'); + $oldName = trim( strtoupper( $oldName ), '"'); + + $tabName = substr( $newName, strlen( $wgDBprefix ) ); + $oldPrefix = substr( $oldName, 0, strlen( $oldName ) - strlen( $tabName ) ); + + return $this->doQuery( 'BEGIN DUPLICATE_TABLE(\'' . $tabName . '\', \'' . $oldPrefix . '\', \'' . strtoupper( $wgDBprefix ) . '\', ' . $temporary . '); END;' ); + } + + function listTables( $prefix = null, $fname = 'DatabaseOracle::listTables' ) { + $listWhere = ''; + if (!empty($prefix)) { + $listWhere = ' AND table_name LIKE \''.strtoupper($prefix).'%\''; } - $tabName = substr($oldName, strlen($oldPrefix)); + $result = $this->doQuery( "SELECT table_name FROM user_tables WHERE table_name NOT LIKE '%!_IDX$_' ESCAPE '!' $listWhere" ); + + // dirty code ... i know + $endArray = array(); + $endArray[] = $prefix.'MWUSER'; + $endArray[] = $prefix.'PAGE'; + $endArray[] = $prefix.'IMAGE'; + $fixedOrderTabs = $endArray; + while (($row = $result->fetchRow()) !== false) { + if (!in_array($row['table_name'], $fixedOrderTabs)) + $endArray[] = $row['table_name']; + } + + return $endArray; + } + + public function dropTable( $tableName, $fName = 'DatabaseOracle::dropTable' ) { + $tableName = $this->tableName($tableName); + if( !$this->tableExists( $tableName ) ) { + return false; + } - return $this->query( 'BEGIN DUPLICATE_TABLE(\'' . $tabName . '\', \'' . $oldPrefix . '\', \''.$newPrefix.'\', ' . $temporary . '); END;', $fname ); + return $this->doQuery( "DROP TABLE $tableName CASCADE CONSTRAINTS PURGE" ); } function timestamp( $ts = 0 ) { @@ -818,7 +916,7 @@ class DatabaseOracle extends DatabaseBase { /** * @return string wikitext of a link to the server software's web site */ - function getSoftwareLink() { + public static function getSoftwareLink() { return '[http://www.oracle.com/ Oracle]'; } @@ -826,14 +924,21 @@ class DatabaseOracle extends DatabaseBase { * @return string Version information from the database */ function getServerVersion() { - return oci_server_version( $this->mConn ); + //better version number, fallback on driver + $rset = $this->doQuery( 'SELECT version FROM product_component_version WHERE UPPER(product) LIKE \'ORACLE DATABASE%\'' ); + if ( !( $row = $rset->fetchRow() ) ) { + return oci_server_version( $this->mConn ); + } + return $row['version']; } /** * Query whether a given table exists (in the given schema, or the default mw one if not given) */ function tableExists( $table ) { - $SQL = "SELECT 1 FROM user_tables WHERE table_name='$table'"; + $table = $this->addQuotes( trim( $this->tableName($table), '"' ) ); + $owner = $this->addQuotes( strtoupper( $this->mDBname ) ); + $SQL = "SELECT 1 FROM all_tables WHERE owner=$owner AND table_name=$table"; $res = $this->doQuery( $SQL ); if ( $res ) { $count = $res->numRows(); @@ -841,7 +946,7 @@ class DatabaseOracle extends DatabaseBase { } else { $count = 0; } - return $count; + return $count!=0; } /** @@ -850,76 +955,78 @@ class DatabaseOracle extends DatabaseBase { * For internal calls. Use fieldInfo for normal usage. * Returns false if the field doesn't exist * - * @param Array $table - * @param String $field + * @param $table Array + * @param $field String */ private function fieldInfoMulti( $table, $field ) { - $tableWhere = ''; - $field = strtoupper($field); - if (is_array($table)) { - $table = array_map( array( &$this, 'tableName' ), $table ); + $field = strtoupper( $field ); + if ( is_array( $table ) ) { + $table = array_map( array( &$this, 'tableNameInternal' ), $table ); $tableWhere = 'IN ('; - foreach($table as &$singleTable) { - $singleTable = strtoupper(trim( $singleTable, '"' )); - if (isset($this->mFieldInfoCache["$singleTable.$field"])) { + foreach( $table as &$singleTable ) { + $singleTable = strtoupper( trim( $singleTable, '"' ) ); + if ( isset( $this->mFieldInfoCache["$singleTable.$field"] ) ) { return $this->mFieldInfoCache["$singleTable.$field"]; } - $tableWhere .= '\''.$singleTable.'\','; + $tableWhere .= '\'' . $singleTable . '\','; } - $tableWhere = rtrim($tableWhere, ',').')'; + $tableWhere = rtrim( $tableWhere, ',' ) . ')'; } else { - $table = strtoupper(trim( $this->tableName($table), '"' )); - if (isset($this->mFieldInfoCache["$table.$field"])) { + $table = strtoupper( trim( $this->tableNameInternal( $table ), '"' ) ); + if ( isset( $this->mFieldInfoCache["$table.$field"] ) ) { return $this->mFieldInfoCache["$table.$field"]; } $tableWhere = '= \''.$table.'\''; } $fieldInfoStmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name '.$tableWhere.' and column_name = \''.$field.'\'' ); - if ( oci_execute( $fieldInfoStmt, OCI_DEFAULT ) === false ) { + if ( oci_execute( $fieldInfoStmt, $this->execFlags() ) === false ) { $e = oci_error( $fieldInfoStmt ); $this->reportQueryError( $e['message'], $e['code'], 'fieldInfo QUERY', __METHOD__ ); return false; } $res = new ORAResult( $this, $fieldInfoStmt ); - if ($res->numRows() == 0 ) { - if (is_array($table)) { - foreach($table as &$singleTable) { + if ( $res->numRows() == 0 ) { + if ( is_array( $table ) ) { + foreach( $table as &$singleTable ) { $this->mFieldInfoCache["$singleTable.$field"] = false; } } else { $this->mFieldInfoCache["$table.$field"] = false; } + $fieldInfoTemp = null; } else { $fieldInfoTemp = new ORAField( $res->fetchRow() ); $table = $fieldInfoTemp->tableName(); $this->mFieldInfoCache["$table.$field"] = $fieldInfoTemp; - return $fieldInfoTemp; } + $res->free(); + return $fieldInfoTemp; } function fieldInfo( $table, $field ) { if ( is_array( $table ) ) { - throw new DBUnexpectedError( $this, 'Database::fieldInfo called with table array!' ); + throw new DBUnexpectedError( $this, 'DatabaseOracle::fieldInfo called with table array!' ); } return $this->fieldInfoMulti ($table, $field); } - function fieldExists( $table, $field, $fname = 'DatabaseOracle::fieldExists' ) { - return (bool)$this->fieldInfo( $table, $field, $fname ); - } - - function begin( $fname = '' ) { + function begin( $fname = 'DatabaseOracle::begin' ) { $this->mTrxLevel = 1; } - function immediateCommit( $fname = '' ) { - return true; + function commit( $fname = 'DatabaseOracle::commit' ) { + if ( $this->mTrxLevel ) { + oci_commit( $this->mConn ); + $this->mTrxLevel = 0; + } } - function commit( $fname = '' ) { - oci_commit( $this->mConn ); - $this->mTrxLevel = 0; + function rollback( $fname = 'DatabaseOracle::rollback' ) { + if ( $this->mTrxLevel ) { + oci_rollback( $this->mConn ); + $this->mTrxLevel = 0; + } } /* Not even sure why this is used in the main codebase... */ @@ -928,7 +1035,7 @@ class DatabaseOracle extends DatabaseBase { } /* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */ - function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) { + function sourceStream( $fp, $lineCallback = false, $resultCallback = false, $fname = 'DatabaseOracle::sourceStream' ) { $cmd = ''; $done = false; $dollarquote = false; @@ -953,6 +1060,7 @@ class DatabaseOracle extends DatabaseBase { if ( substr( $line, 0, 8 ) == '/*$mw$*/' ) { if ( $dollarquote ) { $dollarquote = false; + $line = str_replace( '/*$mw$*/', '', $line ); // remove dollarquotes $done = true; } else { $dollarquote = true; @@ -977,11 +1085,11 @@ class DatabaseOracle extends DatabaseBase { } } else { foreach ( $replacements as $mwVar => $scVar ) { - $cmd = str_replace( '&' . $scVar . '.', '{$' . $mwVar . '}', $cmd ); + $cmd = str_replace( '&' . $scVar . '.', '`{$' . $mwVar . '}`', $cmd ); } $cmd = $this->replaceVars( $cmd ); - $res = $this->query( $cmd, __METHOD__ ); + $res = $this->doQuery( $cmd ); if ( $resultCallback ) { call_user_func( $resultCallback, $res, $this ); } @@ -999,36 +1107,24 @@ class DatabaseOracle extends DatabaseBase { return true; } - function setup_database() { - global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser; - - $res = $this->sourceFile( "../maintenance/ora/tables.sql" ); - if ($res === true) { - print " done.
  • \n"; - } else { - print " FAILED\n"; - dieout( htmlspecialchars( $res ) ); - } - - // Avoid the non-standard "REPLACE INTO" syntax - echo "
  • Populating interwiki table
  • \n"; - $f = fopen( "../maintenance/interwiki.sql", 'r' ); - if ( $f == false ) { - dieout( "Could not find the interwiki.sql file" ); + function selectDB( $db ) { + $this->mDBname = $db; + if ( $db == null || $db == $this->mUser ) { + return true; } - - // do it like the postgres :D - $SQL = "INSERT INTO ".$this->tableName('interwiki')." (iw_prefix,iw_url,iw_local) VALUES "; - while ( !feof( $f ) ) { - $line = fgets( $f, 1024 ); - $matches = array(); - if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) { - continue; + $sql = 'ALTER SESSION SET CURRENT_SCHEMA=' . strtoupper($db); + $stmt = oci_parse( $this->mConn, $sql ); + wfSuppressWarnings(); + $success = oci_execute( $stmt ); + wfRestoreWarnings(); + if ( !$success ) { + $e = oci_error( $stmt ); + if ( $e['code'] != '1435' ) { + $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); } - $this->query( "$SQL $matches[1],$matches[2])" ); + return false; } - - echo "
  • Table interwiki successfully populated
  • \n"; + return true; } function strencode( $s ) { @@ -1036,35 +1132,42 @@ class DatabaseOracle extends DatabaseBase { } function addQuotes( $s ) { - global $wgLang; - if ( isset( $wgLang->mLoaded ) && $wgLang->mLoaded ) { - $s = $wgLang->checkTitleEncoding( $s ); + global $wgContLang; + if ( isset( $wgContLang->mLoaded ) && $wgContLang->mLoaded ) { + $s = $wgContLang->checkTitleEncoding( $s ); } return "'" . $this->strencode( $s ) . "'"; } - function quote_ident( $s ) { + public function addIdentifierQuotes( $s ) { + if ( !$this->mFlags & DBO_DDLMODE ) { + $s = '"' . str_replace( '"', '""', $s ) . '"'; + } return $s; } function selectRow( $table, $vars, $conds, $fname = 'DatabaseOracle::selectRow', $options = array(), $join_conds = array() ) { - global $wgLang; + global $wgContLang; - $conds2 = array(); - $conds = ($conds != null && !is_array($conds)) ? array($conds) : $conds; - foreach ( $conds as $col => $val ) { - $col_info = $this->fieldInfoMulti( $table, $col ); - $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; - if ( $col_type == 'CLOB' ) { - $conds2['TO_CHAR(' . $col . ')'] = $wgLang->checkTitleEncoding( $val ); - } elseif ( $col_type == 'VARCHAR2' && !mb_check_encoding( $val ) ) { - $conds2[$col] = $wgLang->checkTitleEncoding( $val ); - } else { - $conds2[$col] = $val; + if ($conds != null) { + $conds2 = array(); + $conds = ( !is_array( $conds ) ) ? array( $conds ) : $conds; + foreach ( $conds as $col => $val ) { + $col_info = $this->fieldInfoMulti( $table, $col ); + $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; + if ( $col_type == 'CLOB' ) { + $conds2['TO_CHAR(' . $col . ')'] = $wgContLang->checkTitleEncoding( $val ); + } elseif ( $col_type == 'VARCHAR2' && !mb_check_encoding( $val ) ) { + $conds2[$col] = $wgContLang->checkTitleEncoding( $val ); + } else { + $conds2[$col] = $val; + } } - } - return parent::selectRow( $table, $vars, $conds2, $fname, $options, $join_conds ); + return parent::selectRow( $table, $vars, $conds2, $fname, $options, $join_conds ); + } else { + return parent::selectRow( $table, $vars, $conds, $fname, $options, $join_conds ); + } } /** @@ -1111,24 +1214,24 @@ class DatabaseOracle extends DatabaseBase { } public function delete( $table, $conds, $fname = 'DatabaseOracle::delete' ) { - global $wgLang; + global $wgContLang; - if ( $wgLang != null ) { + if ( $wgContLang != null && $conds != null && $conds != '*' ) { $conds2 = array(); - $conds = ($conds != null && !is_array($conds)) ? array($conds) : $conds; + $conds = ( !is_array( $conds ) ) ? array( $conds ) : $conds; foreach ( $conds as $col => $val ) { $col_info = $this->fieldInfoMulti( $table, $col ); $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; if ( $col_type == 'CLOB' ) { - $conds2['TO_CHAR(' . $col . ')'] = $wgLang->checkTitleEncoding( $val ); + $conds2['TO_CHAR(' . $col . ')'] = $wgContLang->checkTitleEncoding( $val ); } else { if ( is_array( $val ) ) { $conds2[$col] = $val; foreach ( $conds2[$col] as &$val2 ) { - $val2 = $wgLang->checkTitleEncoding( $val2 ); + $val2 = $wgContLang->checkTitleEncoding( $val2 ); } } else { - $conds2[$col] = $wgLang->checkTitleEncoding( $val ); + $conds2[$col] = $wgContLang->checkTitleEncoding( $val ); } } } @@ -1139,9 +1242,103 @@ class DatabaseOracle extends DatabaseBase { } } + function update( $table, $values, $conds, $fname = 'DatabaseOracle::update', $options = array() ) { + global $wgContLang; + + $table = $this->tableName( $table ); + $opts = $this->makeUpdateOptions( $options ); + $sql = "UPDATE $opts $table SET "; + + $first = true; + foreach ( $values as $col => &$val ) { + $sqlSet = $this->fieldBindStatement( $table, $col, $val, true ); + + if ( !$first ) { + $sqlSet = ', ' . $sqlSet; + } else { + $first = false; + } + $sql .= $sqlSet; + } + + if ( $conds != '*' ) { + $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); + } + + if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) { + $e = oci_error( $this->mConn ); + $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; + } + foreach ( $values as $col => &$val ) { + $col_info = $this->fieldInfoMulti( $table, $col ); + $col_type = $col_info != false ? $col_info->type() : 'CONSTANT'; + + if ( $val === null ) { + // do nothing ... null was inserted in statement creation + } elseif ( $col_type != 'BLOB' && $col_type != 'CLOB' ) { + if ( is_object( $val ) ) { + $val = $val->getData(); + } + + if ( preg_match( '/^timestamp.*/i', $col_type ) == 1 && strtolower( $val ) == 'infinity' ) { + $val = '31-12-2030 12:00:00.000000'; + } + + $val = ( $wgContLang != null ) ? $wgContLang->checkTitleEncoding( $val ) : $val; + if ( oci_bind_by_name( $stmt, ":$col", $val ) === false ) { + $e = oci_error( $stmt ); + $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; + } + } else { + if ( ( $lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB ) ) === false ) { + $e = oci_error( $stmt ); + throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] ); + } + + if ( $col_type == 'BLOB' ) { + $lob[$col]->writeTemporary( $val ); + oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, SQLT_BLOB ); + } else { + $lob[$col]->writeTemporary( $val ); + oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, OCI_B_CLOB ); + } + } + } + + wfSuppressWarnings(); + + if ( oci_execute( $stmt, $this->execFlags() ) === false ) { + $e = oci_error( $stmt ); + if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) { + $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ ); + return false; + } else { + $this->mAffectedRows = oci_num_rows( $stmt ); + } + } else { + $this->mAffectedRows = oci_num_rows( $stmt ); + } + + wfRestoreWarnings(); + + if ( isset( $lob ) ) { + foreach ( $lob as $lob_v ) { + $lob_v->free(); + } + } + + if ( !$this->mTrxLevel ) { + oci_commit( $this->mConn ); + } + + oci_free_statement( $stmt ); + } + function bitNot( $field ) { // expecting bit-fields smaller than 4bytes - return 'BITNOT(' . $bitField . ')'; + return 'BITNOT(' . $field . ')'; } function bitAnd( $fieldLeft, $fieldRight ) { @@ -1152,17 +1349,6 @@ class DatabaseOracle extends DatabaseBase { return 'BITOR(' . $fieldLeft . ', ' . $fieldRight . ')'; } - /** - * How lagged is this slave? - * - * @return int - */ - public function getLag() { - # Not implemented for Oracle - return 0; - } - - function setFakeSlaveLag( $lag ) { } function setFakeMaster( $enabled = true ) { } function getDBname() { @@ -1173,26 +1359,6 @@ class DatabaseOracle extends DatabaseBase { return $this->mServer; } - public function replaceVars( $ins ) { - $varnames = array( 'wgDBprefix' ); - if ( $this->mFlags & DBO_SYSDBA ) { - $varnames[] = 'wgDBOracleDefTS'; - $varnames[] = 'wgDBOracleTempTS'; - } - - // Ordinary variables - foreach ( $varnames as $var ) { - if ( isset( $GLOBALS[$var] ) ) { - $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check? - $ins = str_replace( '{$' . $var . '}', $val, $ins ); - $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins ); - $ins = str_replace( '/*$' . $var . '*/', $val, $ins ); - } - } - - return parent::replaceVars( $ins ); - } - public function getSearchEngine() { return 'SearchOracle'; } diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php index 9072a5b2..bc71a9a5 100644 --- a/includes/db/DatabasePostgres.php +++ b/includes/db/DatabasePostgres.php @@ -1,46 +1,59 @@ query(sprintf($q, - $db->addQuotes($wgDBmwschema), - $db->addQuotes($table), - $db->addQuotes($field))); - $row = $db->fetchObject($res); - if (!$row) + + $table = $db->tableName( $table ); + $res = $db->query( + sprintf( $q, + $db->addQuotes( $wgDBmwschema ), + $db->addQuotes( $table ), + $db->addQuotes( $field ) + ) + ); + $row = $db->fetchObject( $res ); + if ( !$row ) { return null; + } $n = new PostgresField; $n->type = $row->typname; - $n->nullable = ($row->attnotnull == 'f'); + $n->nullable = ( $row->attnotnull == 'f' ); $n->name = $field; $n->tablename = $table; $n->max_length = $row->attlen; + $n->deferrable = ( $row->deferrable == 't' ); + $n->deferred = ( $row->deferred == 't' ); + $n->conname = $row->conname; return $n; } @@ -56,13 +69,26 @@ SQL; return $this->type; } - function nullable() { + function isNullable() { return $this->nullable; } function maxLength() { return $this->max_length; } + + function is_deferrable() { + return $this->deferrable; + } + + function is_deferred() { + return $this->deferred; + } + + function conname() { + return $this->conname; + } + } /** @@ -74,16 +100,6 @@ class DatabasePostgres extends DatabaseBase { var $numeric_version = null; var $mAffectedRows = null; - function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false, - $failFunction = false, $flags = 0 ) - { - - $this->mFailFunction = $failFunction; - $this->mFlags = $flags; - $this->open( $server, $user, $password, $dbName); - - } - function getType() { return 'postgres'; } @@ -115,18 +131,18 @@ 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( $name ) . "' AND n.nspname = '" . pg_escape_string($wgDBmwschema) ."'"; - return $this->numRows($res = $this->doQuery($SQL)); + $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . + pg_escape_string( $this->mConn, $name ) . "' AND n.nspname = '" . pg_escape_string( $this->mConn, $wgDBmwschema ) ."'"; + $res = $this->doQuery( $SQL ); + return $this->numRows( $res ); } - static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0) - { - return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags ); + static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { + return new DatabasePostgres( $server, $user, $password, $dbName, $flags ); } /** * Usually aborts on failure - * If the failFunction is set to a non-zero integer, returns success */ function open( $server, $user, $password, $dbName ) { # Test for Postgres support, to avoid suppressed fatal error @@ -136,7 +152,7 @@ class DatabasePostgres extends DatabaseBase { global $wgDBport; - if (!strlen($user)) { ## e.g. the class is being loaded + if ( !strlen( $user ) ) { # e.g. the class is being loaded return; } $this->close(); @@ -149,11 +165,12 @@ class DatabasePostgres extends DatabaseBase { $connectVars = array( 'dbname' => $dbName, 'user' => $user, - 'password' => $password ); - if ($server!=false && $server!="") { + 'password' => $password + ); + if ( $server != false && $server != '' ) { $connectVars['host'] = $server; } - if ($port!=false && $port!="") { + if ( $port != false && $port != '' ) { $connectVars['port'] = $port; } $connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW ); @@ -162,35 +179,30 @@ class DatabasePostgres extends DatabaseBase { $this->mConn = pg_connect( $connectString ); $phpError = $this->restoreErrorHandler(); - if ( $this->mConn == false ) { + if ( !$this->mConn ) { wfDebug( "DB connection error\n" ); wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" ); - wfDebug( $this->lastError()."\n" ); - if ( !$this->mFailFunction ) { - throw new DBConnectionError( $this, $phpError ); - } else { - return false; - } + wfDebug( $this->lastError() . "\n" ); + throw new DBConnectionError( $this, str_replace( "\n", ' ', $phpError ) ); } $this->mOpened = true; global $wgCommandLineMode; - ## If called from the command-line (e.g. importDump), only show errors - if ($wgCommandLineMode) { + # If called from the command-line (e.g. importDump), only show errors + if ( $wgCommandLineMode ) { $this->doQuery( "SET client_min_messages = 'ERROR'" ); } $this->doQuery( "SET client_encoding='UTF8'" ); global $wgDBmwschema, $wgDBts2schema; - if (isset( $wgDBmwschema ) && isset( $wgDBts2schema ) + if ( isset( $wgDBmwschema ) && isset( $wgDBts2schema ) && $wgDBmwschema !== 'mediawiki' && preg_match( '/^\w+$/', $wgDBmwschema ) && preg_match( '/^\w+$/', $wgDBts2schema ) ) { - $safeschema = $this->quote_ident($wgDBmwschema); - $safeschema2 = $this->quote_ident($wgDBts2schema); + $safeschema = $this->addIdentifierQuotes( $wgDBmwschema ); $this->doQuery( "SET search_path = $safeschema, $wgDBts2schema, public" ); } @@ -205,365 +217,6 @@ class DatabasePostgres extends DatabaseBase { return $s; } - - function initial_setup($password, $dbName) { - // If this is the initial connection, setup the schema stuff and possibly create the user - global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema; - - print "
  • Checking the version of Postgres..."; - $version = $this->getServerVersion(); - $PGMINVER = '8.1'; - if ($version < $PGMINVER) { - print "FAILED. Required version is $PGMINVER. You have " . htmlspecialchars( $version ) . "
  • \n"; - dieout(""); - } - print "version " . htmlspecialchars( $this->numeric_version ) . " is OK.\n"; - - $safeuser = $this->quote_ident($wgDBuser); - // Are we connecting as a superuser for the first time? - if ($wgDBsuperuser) { - // Are we really a superuser? Check out our rights - $SQL = "SELECT - CASE WHEN usesuper IS TRUE THEN - CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END - ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END - END AS rights - FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser); - $rows = $this->numRows($res = $this->doQuery($SQL)); - if (!$rows) { - print "
  • ERROR: Could not read permissions for user \"" . htmlspecialchars( $wgDBsuperuser ) . "\"
  • \n"; - dieout(''); - } - $perms = pg_fetch_result($res, 0, 0); - - $SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser); - $rows = $this->numRows($this->doQuery($SQL)); - if ($rows) { - print "
  • User \"" . htmlspecialchars( $wgDBuser ) . "\" already exists, skipping account creation.
  • "; - } - else { - if ($perms != 1 and $perms != 3) { - print "
  • ERROR: the user \"" . htmlspecialchars( $wgDBsuperuser ) . "\" cannot create other users. "; - print 'Please use a different Postgres user.
  • '; - dieout(''); - } - print "
  • Creating user " . htmlspecialchars( $wgDBuser ) . "..."; - $safepass = $this->addQuotes($wgDBpassword); - $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass"; - $this->doQuery($SQL); - print "OK
  • \n"; - } - // User now exists, check out the database - if ($dbName != $wgDBname) { - $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $this->addQuotes($wgDBname); - $rows = $this->numRows($this->doQuery($SQL)); - if ($rows) { - print "
  • Database \"" . htmlspecialchars( $wgDBname ) . "\" already exists, skipping database creation.
  • "; - } - else { - if ($perms < 1) { - print "
  • ERROR: the user \"" . htmlspecialchars( $wgDBsuperuser ) . "\" cannot create databases. "; - print 'Please use a different Postgres user.
  • '; - dieout(''); - } - print "
  • Creating database " . htmlspecialchars( $wgDBname ) . "..."; - $safename = $this->quote_ident($wgDBname); - $SQL = "CREATE DATABASE $safename OWNER $safeuser "; - $this->doQuery($SQL); - print "OK
  • \n"; - // Hopefully tsearch2 and plpgsql are in template1... - } - - // Reconnect to check out tsearch2 rights for this user - print "
  • Connecting to \"" . htmlspecialchars( $wgDBname ) . "\" as superuser \"" . - htmlspecialchars( $wgDBsuperuser ) . "\" to check rights..."; - - $connectVars = array(); - if ($this->mServer!=false && $this->mServer!="") { - $connectVars['host'] = $this->mServer; - } - if ($this->mPort!=false && $this->mPort!="") { - $connectVars['port'] = $this->mPort; - } - $connectVars['dbname'] = $wgDBname; - $connectVars['user'] = $wgDBsuperuser; - $connectVars['password'] = $password; - - @$this->mConn = pg_connect( $this->makeConnectionString( $connectVars ) ); - if ( $this->mConn == false ) { - print "FAILED TO CONNECT!
  • "; - dieout(""); - } - print "OK\n"; - } - - if ($this->numeric_version < 8.3) { - // Tsearch2 checks - print "
  • Checking that tsearch2 is installed in the database \"" . - htmlspecialchars( $wgDBname ) . "\"..."; - if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) { - print "FAILED. tsearch2 must be installed in the database \"" . - htmlspecialchars( $wgDBname ) . "\"."; - print "Please see this article"; - print " for instructions or ask on #postgresql on irc.freenode.net
  • \n"; - dieout(""); - } - print "OK\n"; - print "
  • Ensuring that user \"" . htmlspecialchars( $wgDBuser ) . - "\" has select rights on the tsearch2 tables..."; - foreach (array('cfg','cfgmap','dict','parser') as $table) { - $SQL = "GRANT SELECT ON pg_ts_$table TO $safeuser"; - $this->doQuery($SQL); - } - print "OK
  • \n"; - } - - // Setup the schema for this user if needed - $result = $this->schemaExists($wgDBmwschema); - $safeschema = $this->quote_ident($wgDBmwschema); - if (!$result) { - print "
  • Creating schema " . htmlspecialchars( $wgDBmwschema ) . " ..."; - $result = $this->doQuery("CREATE SCHEMA $safeschema AUTHORIZATION $safeuser"); - if (!$result) { - print "FAILED.
  • \n"; - dieout(""); - } - print "OK\n"; - } - else { - print "
  • Schema already exists, explicitly granting rights...\n"; - $safeschema2 = $this->addQuotes($wgDBmwschema); - $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n". - "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n". - "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n". - "AND p.relkind IN ('r','S','v')\n"; - $SQL .= "UNION\n"; - $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n". - "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n". - "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n". - "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2"; - $res = $this->doQuery($SQL); - if (!$res) { - print "FAILED. Could not set rights for the user.
  • \n"; - dieout(""); - } - $this->doQuery("SET search_path = $safeschema"); - $rows = $this->numRows($res); - while ($rows) { - $rows--; - $this->doQuery(pg_fetch_result($res, $rows, 0)); - } - print "OK"; - } - - // Install plpgsql if needed - $this->setup_plpgsql(); - - $wgDBsuperuser = ''; - return true; // Reconnect as regular user - - } // end superuser - - if (!defined('POSTGRES_SEARCHPATH')) { - - if ($this->numeric_version < 8.3) { - // Do we have the basic tsearch2 table? - print "
  • Checking for tsearch2 in the schema \"" . htmlspecialchars( $wgDBts2schema ) . "\"..."; - if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) { - print "FAILED. Make sure tsearch2 is installed. See this article"; - print " for instructions.
  • \n"; - dieout(""); - } - print "OK\n"; - - // Does this user have the rights to the tsearch2 tables? - $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0); - print "
  • Checking tsearch2 permissions..."; - // Let's check all four, just to be safe - error_reporting( 0 ); - $ts2tables = array('cfg','cfgmap','dict','parser'); - $safetsschema = $this->quote_ident($wgDBts2schema); - foreach ( $ts2tables AS $tname ) { - $SQL = "SELECT count(*) FROM $safetsschema.pg_ts_$tname"; - $res = $this->doQuery($SQL); - if (!$res) { - print "FAILED to access " . htmlspecialchars( "pg_ts_$tname" ) . - ". Make sure that the user \"". htmlspecialchars( $wgDBuser ) . - "\" has SELECT access to all four tsearch2 tables
  • \n"; - dieout(""); - } - } - $SQL = "SELECT ts_name FROM $safetsschema.pg_ts_cfg WHERE locale = " . $this->addQuotes( $ctype ) ; - $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END"; - $res = $this->doQuery($SQL); - error_reporting( E_ALL ); - if (!$res) { - print "FAILED. Could not determine the tsearch2 locale information\n"; - dieout(""); - } - print "OK"; - - // Will the current locale work? Can we force it to? - print "
  • Verifying tsearch2 locale with " . htmlspecialchars( $ctype ) . "..."; - $rows = $this->numRows($res); - $resetlocale = 0; - if (!$rows) { - print "not found
  • \n"; - print "
  • Attempting to set default tsearch2 locale to \"" . htmlspecialchars( $ctype ) . "\"..."; - $resetlocale = 1; - } - else { - $tsname = pg_fetch_result($res, 0, 0); - if ($tsname != 'default') { - print "not set to default (" . htmlspecialchars( $tsname ) . ")"; - print "
  • Attempting to change tsearch2 default locale to \"" . - htmlspecialchars( $ctype ) . "\"..."; - $resetlocale = 1; - } - } - if ($resetlocale) { - $SQL = "UPDATE $safetsschema.pg_ts_cfg SET locale = " . $this->addQuotes( $ctype ) . " WHERE ts_name = 'default'"; - $res = $this->doQuery($SQL); - if (!$res) { - print "FAILED. "; - print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"" . - htmlspecialchars( $ctype ) . "\"
  • \n"; - dieout(""); - } - print "OK"; - } - - // Final test: try out a simple tsearch2 query - $SQL = "SELECT $safetsschema.to_tsvector('default','MediaWiki tsearch2 testing')"; - $res = $this->doQuery($SQL); - if (!$res) { - print "FAILED. Specifically, \"" . htmlspecialchars( $SQL ) . "\" did not work."; - dieout(""); - } - print "OK"; - } - - // Install plpgsql if needed - $this->setup_plpgsql(); - - // Does the schema already exist? Who owns it? - $result = $this->schemaExists($wgDBmwschema); - if (!$result) { - print "
  • Creating schema " . htmlspecialchars( $wgDBmwschema ) . " ..."; - error_reporting( 0 ); - $safeschema = $this->quote_ident($wgDBmwschema); - $result = $this->doQuery("CREATE SCHEMA $safeschema"); - error_reporting( E_ALL ); - if (!$result) { - print "FAILED. The user \"" . htmlspecialchars( $wgDBuser ) . - "\" must be able to access the schema. ". - "You can try making them the owner of the database, or try creating the schema with a ". - "different user, and then grant access to the \"" . - htmlspecialchars( $wgDBuser ) . "\" user.
  • \n"; - dieout(""); - } - print "OK\n"; - } - else if ($result != $wgDBuser) { - print "
  • Schema \"" . htmlspecialchars( $wgDBmwschema ) . "\" exists but is not owned by \"" . - htmlspecialchars( $wgDBuser ) . "\". Not ideal.
  • \n"; - } - else { - print "
  • Schema \"" . htmlspecialchars( $wgDBmwschema ) . "\" exists and is owned by \"" . - htmlspecialchars( $wgDBuser ) . "\". Excellent.
  • \n"; - } - - // Always return GMT time to accomodate the existing integer-based timestamp assumption - print "
  • Setting the timezone to GMT for user \"" . htmlspecialchars( $wgDBuser ) . "\" ..."; - $SQL = "ALTER USER $safeuser SET timezone = 'GMT'"; - $result = pg_query($this->mConn, $SQL); - if (!$result) { - print "FAILED.
  • \n"; - dieout(""); - } - print "OK\n"; - // Set for the rest of this session - $SQL = "SET timezone = 'GMT'"; - $result = pg_query($this->mConn, $SQL); - if (!$result) { - print "
  • Failed to set timezone
  • \n"; - dieout(""); - } - - print "
  • Setting the datestyle to ISO, YMD for user \"" . htmlspecialchars( $wgDBuser ) . "\" ..."; - $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'"; - $result = pg_query($this->mConn, $SQL); - if (!$result) { - print "FAILED.
  • \n"; - dieout(""); - } - print "OK\n"; - // Set for the rest of this session - $SQL = "SET datestyle = 'ISO, YMD'"; - $result = pg_query($this->mConn, $SQL); - if (!$result) { - print "
  • Failed to set datestyle
  • \n"; - dieout(""); - } - - // Fix up the search paths if needed - print "
  • Setting the search path for user \"" . htmlspecialchars( $wgDBuser ) . "\" ..."; - $path = $this->quote_ident($wgDBmwschema); - if ($wgDBts2schema !== $wgDBmwschema) - $path .= ", ". $this->quote_ident($wgDBts2schema); - if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public') - $path .= ", public"; - $SQL = "ALTER USER $safeuser SET search_path = $path"; - $result = pg_query($this->mConn, $SQL); - if (!$result) { - print "FAILED.
  • \n"; - dieout(""); - } - print "OK\n"; - // Set for the rest of this session - $SQL = "SET search_path = $path"; - $result = pg_query($this->mConn, $SQL); - if (!$result) { - print "
  • Failed to set search_path
  • \n"; - dieout(""); - } - define( "POSTGRES_SEARCHPATH", $path ); - } - } - - - function setup_plpgsql() { - print "
  • Checking for Pl/Pgsql ..."; - $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'"; - $rows = $this->numRows($this->doQuery($SQL)); - if ($rows < 1) { - // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it - print "not installed. Attempting to install Pl/Pgsql ..."; - $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ". - "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'"; - $rows = $this->numRows($this->doQuery($SQL)); - if ($rows >= 1) { - $olde = error_reporting(0); - error_reporting($olde - E_WARNING); - $result = $this->doQuery("CREATE LANGUAGE plpgsql"); - error_reporting($olde); - if (!$result) { - print "FAILED. You need to install the language plpgsql in the database " . - htmlspecialchars( $wgDBname ) . "
  • "; - dieout(""); - } - } - else { - print "FAILED. You need to install the language plpgsql in the database " . - htmlspecialchars( $wgDBname ) . ""; - dieout(""); - } - } - print "OK\n"; - } - - /** * Closes a database connection, if it is open * Returns success, true if already closed @@ -578,15 +231,15 @@ class DatabasePostgres extends DatabaseBase { } function doQuery( $sql ) { - if (function_exists('mb_convert_encoding')) { - $sql = mb_convert_encoding($sql,'UTF-8'); + if ( function_exists( 'mb_convert_encoding' ) ) { + $sql = mb_convert_encoding( $sql, 'UTF-8' ); } - $this->mLastResult = pg_query( $this->mConn, $sql); + $this->mLastResult = pg_query( $this->mConn, $sql ); $this->mAffectedRows = null; // use pg_affected_rows(mLastResult) return $this->mLastResult; } - function queryIgnore( $sql, $fname = '' ) { + function queryIgnore( $sql, $fname = 'DatabasePostgres::queryIgnore' ) { return $this->query( $sql, $fname, true ); } @@ -595,7 +248,7 @@ class DatabasePostgres extends DatabaseBase { $res = $res->result; } if ( !@pg_free_result( $res ) ) { - throw new DBUnexpectedError($this, "Unable to free Postgres result\n" ); + throw new DBUnexpectedError( $this, "Unable to free Postgres result\n" ); } } @@ -609,8 +262,8 @@ class DatabasePostgres extends DatabaseBase { # TODO: # hashar : not sure if the following test really trigger if the object # fetching failed. - if( pg_last_error($this->mConn) ) { - throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) ); + if( pg_last_error( $this->mConn ) ) { + throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); } return $row; } @@ -620,8 +273,8 @@ class DatabasePostgres extends DatabaseBase { $res = $res->result; } @$row = pg_fetch_array( $res ); - if( pg_last_error($this->mConn) ) { - throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) ); + if( pg_last_error( $this->mConn ) ) { + throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); } return $row; } @@ -631,17 +284,19 @@ class DatabasePostgres extends DatabaseBase { $res = $res->result; } @$n = pg_num_rows( $res ); - if( pg_last_error($this->mConn) ) { - throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) ); + if( pg_last_error( $this->mConn ) ) { + throw new DBUnexpectedError( $this, 'SQL error: ' . htmlspecialchars( pg_last_error( $this->mConn ) ) ); } return $n; } + function numFields( $res ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; } return pg_num_fields( $res ); } + function fieldName( $res, $n ) { if ( $res instanceof ResultWrapper ) { $res = $res->result; @@ -666,9 +321,8 @@ class DatabasePostgres extends DatabaseBase { function lastError() { if ( $this->mConn ) { return pg_last_error(); - } - else { - return "No database connection"; + } else { + return 'No database connection'; } } function lastErrno() { @@ -680,8 +334,9 @@ class DatabasePostgres extends DatabaseBase { // Forced result for simulated queries return $this->mAffectedRows; } - if( empty( $this->mLastResult ) ) + if( empty( $this->mLastResult ) ) { return 0; + } return pg_affected_rows( $this->mLastResult ); } @@ -692,8 +347,7 @@ class DatabasePostgres extends DatabaseBase { * Returns -1 if count cannot be found * Takes same arguments as Database::select() */ - - function estimateRowCount( $table, $vars='*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) { + function estimateRowCount( $table, $vars = '*', $conds='', $fname = 'DatabasePostgres::estimateRowCount', $options = array() ) { $options['EXPLAIN'] = true; $res = $this->select( $table, $vars, $conds, $fname, $options ); $rows = -1; @@ -703,12 +357,10 @@ class DatabasePostgres extends DatabaseBase { if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) { $rows = $count[1]; } - $this->freeResult($res); } return $rows; } - /** * Returns information about an index * If errors are explicitly ignored, returns NULL on failure @@ -719,7 +371,7 @@ class DatabasePostgres extends DatabaseBase { if ( !$res ) { return null; } - while ( $row = $this->fetchObject( $res ) ) { + foreach ( $res as $row ) { if ( $row->indexname == $this->indexName( $index ) ) { return $row; } @@ -727,18 +379,19 @@ class DatabasePostgres extends DatabaseBase { return false; } - function indexUnique ($table, $index, $fname = 'DatabasePostgres::indexUnique' ) { + function indexUnique( $table, $index, $fname = 'DatabasePostgres::indexUnique' ) { $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'". - " AND indexdef LIKE 'CREATE UNIQUE%(" . + " AND indexdef LIKE 'CREATE UNIQUE%(" . $this->strencode( $this->indexName( $index ) ) . ")'"; $res = $this->query( $sql, $fname ); - if ( !$res ) + if ( !$res ) { return null; - while ($row = $this->fetchObject( $res )) + } + foreach ( $res as $row ) { return true; + } return false; - } /** @@ -755,25 +408,23 @@ class DatabasePostgres extends DatabaseBase { * @return bool Success of insert operation. IGNORE always returns true. */ function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) { - global $wgDBversion; - if ( !count( $args ) ) { return true; } $table = $this->tableName( $table ); - if (! isset( $wgDBversion ) ) { - $wgDBversion = $this->getServerVersion(); + if (! isset( $this->numeric_version ) ) { + $this->getServerVersion(); } - if ( !is_array( $options ) ) + if ( !is_array( $options ) ) { $options = array( $options ); + } if ( isset( $args[0] ) && is_array( $args[0] ) ) { $multi = true; $keys = array_keys( $args[0] ); - } - else { + } else { $multi = false; $keys = array_keys( $args ); } @@ -784,7 +435,7 @@ class DatabasePostgres extends DatabaseBase { // If we are not in a transaction, we need to be for savepoint trickery $didbegin = 0; if ( $ignore ) { - if (! $this->mTrxLevel) { + if ( !$this->mTrxLevel ) { $this->begin(); $didbegin = 1; } @@ -797,7 +448,7 @@ class DatabasePostgres extends DatabaseBase { $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES '; if ( $multi ) { - if ( $wgDBversion >= 8.2 && !$ignore ) { + if ( $this->numeric_version >= 8.2 && !$ignore ) { $first = true; foreach ( $args as $row ) { if ( $first ) { @@ -808,8 +459,7 @@ class DatabasePostgres extends DatabaseBase { $sql .= '(' . $this->makeList( $row ) . ')'; } $res = (bool)$this->query( $sql, $fname, $ignore ); - } - else { + } else { $res = true; $origsql = $sql; foreach ( $args as $row ) { @@ -817,17 +467,16 @@ class DatabasePostgres extends DatabaseBase { $tempsql .= '(' . $this->makeList( $row ) . ')'; if ( $ignore ) { - pg_query($this->mConn, "SAVEPOINT $ignore"); + pg_query( $this->mConn, "SAVEPOINT $ignore" ); } $tempres = (bool)$this->query( $tempsql, $fname, $ignore ); if ( $ignore ) { $bar = pg_last_error(); - if ($bar != false) { + if ( $bar != false ) { pg_query( $this->mConn, "ROLLBACK TO $ignore" ); - } - else { + } else { pg_query( $this->mConn, "RELEASE $ignore" ); $numrowsinserted++; } @@ -835,12 +484,12 @@ class DatabasePostgres extends DatabaseBase { // If any of them fail, we fail overall for this function call // Note that this will be ignored if IGNORE is set - if (! $tempres) + if ( !$tempres ) { $res = false; + } } } - } - else { + } else { // Not multi, just a lone insert if ( $ignore ) { pg_query($this->mConn, "SAVEPOINT $ignore"); @@ -850,10 +499,9 @@ class DatabasePostgres extends DatabaseBase { $res = (bool)$this->query( $sql, $fname, $ignore ); if ( $ignore ) { $bar = pg_last_error(); - if ($bar != false) { + if ( $bar != false ) { pg_query( $this->mConn, "ROLLBACK TO $ignore" ); - } - else { + } else { pg_query( $this->mConn, "RELEASE $ignore" ); $numrowsinserted++; } @@ -861,7 +509,7 @@ class DatabasePostgres extends DatabaseBase { } if ( $ignore ) { $olde = error_reporting( $olde ); - if ($didbegin) { + if ( $didbegin ) { $this->commit(); } @@ -872,9 +520,7 @@ class DatabasePostgres extends DatabaseBase { return true; } - return $res; - } /** @@ -884,7 +530,7 @@ 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)? - */ + */ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect', $insertOptions = array(), $selectOptions = array() ) { @@ -922,7 +568,7 @@ class DatabasePostgres extends DatabaseBase { " SELECT $startOpts " . implode( ',', $varMap ) . " FROM $srcTable $useIndex"; - if ( $conds != '*') { + if ( $conds != '*' ) { $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND ); } @@ -951,7 +597,7 @@ class DatabasePostgres extends DatabaseBase { return $res; } - + function tableName( $name ) { # Replace reserved words with better ones switch( $name ) { @@ -968,11 +614,10 @@ class DatabasePostgres extends DatabaseBase { * Return the next in a sequence, save the value for retrieval via insertId() */ function nextSequenceValue( $seqName ) { - $safeseq = preg_replace( "/'/", "''", $seqName ); + $safeseq = str_replace( "'", "''", $seqName ); $res = $this->query( "SELECT nextval('$safeseq')" ); $row = $this->fetchRow( $res ); $this->mInsertId = $row[0]; - $this->freeResult( $res ); return $this->mInsertId; } @@ -980,27 +625,28 @@ class DatabasePostgres extends DatabaseBase { * Return the current value of a sequence. Assumes it has been nextval'ed in this session. */ function currentSequenceValue( $seqName ) { - $safeseq = preg_replace( "/'/", "''", $seqName ); + $safeseq = str_replace( "'", "''", $seqName ); $res = $this->query( "SELECT currval('$safeseq')" ); $row = $this->fetchRow( $res ); $currval = $row[0]; - $this->freeResult( $res ); return $currval; } - # REPLACE query wrapper - # Postgres simulates this with a DELETE followed by INSERT - # $row is the row to insert, an associative array - # $uniqueIndexes is an array of indexes. Each element may be either a - # field name or an array of field names - # - # It may be more efficient to leave off unique indexes which are unlikely to collide. - # However if you do this, you run the risk of encountering errors which wouldn't have - # occurred in MySQL + /** + * REPLACE query wrapper + * Postgres simulates this with a DELETE followed by INSERT + * $row is the row to insert, an associative array + * $uniqueIndexes is an array of indexes. Each element may be either a + * field name or an array of field names + * + * It may be more efficient to leave off unique indexes which are unlikely to collide. + * However if you do this, you run the risk of encountering errors which wouldn't have + * occurred in MySQL + */ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabasePostgres::replace' ) { $table = $this->tableName( $table ); - if (count($rows)==0) { + if ( count( $rows ) == 0 ) { return; } @@ -1017,7 +663,7 @@ class DatabasePostgres extends DatabaseBase { foreach ( $uniqueIndexes as $index ) { if ( $first ) { $first = false; - $sql .= "("; + $sql .= '('; } else { $sql .= ') OR ('; } @@ -1049,7 +695,7 @@ class DatabasePostgres extends DatabaseBase { # DELETE where the condition is a join function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'DatabasePostgres::deleteJoin' ) { if ( !$conds ) { - throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' ); + throw new DBUnexpectedError( $this, 'DatabasePostgres::deleteJoin() called with empty $conds' ); } $delTable = $this->tableName( $delTable ); @@ -1070,19 +716,18 @@ class DatabasePostgres extends DatabaseBase { FROM pg_class c, pg_attribute a, pg_type t WHERE relname='$table' AND a.attrelid=c.oid AND a.atttypid=t.oid and a.attname='$field'"; - $res =$this->query($sql); - $row=$this->fetchObject($res); - if ($row->ftype=="varchar") { - $size=$row->size-4; + $res =$this->query( $sql ); + $row = $this->fetchObject( $res ); + if ( $row->ftype == 'varchar' ) { + $size = $row->size - 4; } else { - $size=$row->size; + $size = $row->size; } - $this->freeResult( $res ); return $size; } - function limitResult($sql, $limit, $offset=false) { - return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":""); + function limitResult( $sql, $limit, $offset = false ) { + return "$sql LIMIT $limit " . ( is_numeric( $offset ) ? " OFFSET {$offset} " : '' ); } function wasDeadlock() { @@ -1093,57 +738,40 @@ class DatabasePostgres extends DatabaseBase { return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname ); } - function timestamp( $ts=0 ) { - return wfTimestamp(TS_POSTGRES,$ts); + function timestamp( $ts = 0 ) { + return wfTimestamp( TS_POSTGRES, $ts ); } /** * Return aggregated value function call */ - function aggregateValue ($valuedata,$valuename='value') { + function aggregateValue( $valuedata, $valuename = 'value' ) { return $valuedata; } - - function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) { - // Ignore errors during error handling to avoid infinite recursion - $ignore = $this->ignoreErrors( true ); - $this->mErrorCount++; - - if ($ignore || $tempIgnore) { - wfDebug("SQL ERROR (ignored): $error\n"); - $this->ignoreErrors( $ignore ); - } - else { - $message = "A database error has occurred\n" . - "Query: $sql\n" . - "Function: $fname\n" . - "Error: $errno $error\n"; - throw new DBUnexpectedError($this, $message); - } - } - /** * @return string wikitext of a link to the server software's web site */ - function getSoftwareLink() { - return "[http://www.postgresql.org/ PostgreSQL]"; + public static function getSoftwareLink() { + return '[http://www.postgresql.org/ PostgreSQL]'; } /** * @return string Version information from the database */ function getServerVersion() { - $versionInfo = pg_version( $this->mConn ); - if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) { - // Old client, abort install - $this->numeric_version = '7.3 or earlier'; - } elseif ( isset( $versionInfo['server'] ) ) { - // Normal client - $this->numeric_version = $versionInfo['server']; - } else { - // Bug 16937: broken pgsql extension from PHP<5.3 - $this->numeric_version = pg_parameter_status( $this->mConn, 'server_version' ); + if ( !isset( $this->numeric_version ) ) { + $versionInfo = pg_version( $this->mConn ); + if ( version_compare( $versionInfo['client'], '7.4.0', 'lt' ) ) { + // Old client, abort install + $this->numeric_version = '7.3 or earlier'; + } elseif ( isset( $versionInfo['server'] ) ) { + // Normal client + $this->numeric_version = $versionInfo['server']; + } else { + // Bug 16937: broken pgsql extension from PHP<5.3 + $this->numeric_version = pg_parameter_status( $this->mConn, 'server_version' ); + } } return $this->numeric_version; } @@ -1154,23 +782,23 @@ class DatabasePostgres extends DatabaseBase { */ function relationExists( $table, $types, $schema = false ) { global $wgDBmwschema; - if ( !is_array( $types ) ) + if ( !is_array( $types ) ) { $types = array( $types ); - if ( !$schema ) + } + if ( !$schema ) { $schema = $wgDBmwschema; + } $etable = $this->addQuotes( $table ); $eschema = $this->addQuotes( $schema ); $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n " . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema " - . "AND c.relkind IN ('" . implode("','", $types) . "')"; + . "AND c.relkind IN ('" . implode( "','", $types ) . "')"; $res = $this->query( $SQL ); $count = $res ? $res->numRows() : 0; - if ($res) - $this->freeResult( $res ); - return $count ? true : false; + return (bool)$count; } - /* + /** * For backward compatibility, this function checks both tables and * views. */ @@ -1191,82 +819,71 @@ class DatabasePostgres extends DatabaseBase { AND tgrelid=pg_class.oid AND nspname=%s AND relname=%s AND tgname=%s SQL; - $res = $this->query(sprintf($q, - $this->addQuotes($wgDBmwschema), - $this->addQuotes($table), - $this->addQuotes($trigger))); - if (!$res) + $res = $this->query( + sprintf( + $q, + $this->addQuotes( $wgDBmwschema ), + $this->addQuotes( $table ), + $this->addQuotes( $trigger ) + ) + ); + if ( !$res ) { return null; + } $rows = $res->numRows(); - $this->freeResult( $res ); return $rows; } function ruleExists( $table, $rule ) { global $wgDBmwschema; - $exists = $this->selectField("pg_rules", "rulename", - array( "rulename" => $rule, - "tablename" => $table, - "schemaname" => $wgDBmwschema ) ); + $exists = $this->selectField( 'pg_rules', 'rulename', + array( + 'rulename' => $rule, + 'tablename' => $table, + 'schemaname' => $wgDBmwschema + ) + ); return $exists === $rule; } function constraintExists( $table, $constraint ) { global $wgDBmwschema; - $SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ". + $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($table), - $this->addQuotes($constraint)); - $res = $this->query($SQL); - if (!$res) + $this->addQuotes( $wgDBmwschema ), + $this->addQuotes( $table ), + $this->addQuotes( $constraint ) + ); + $res = $this->query( $SQL ); + if ( !$res ) { return null; + } $rows = $res->numRows(); - $this->freeResult($res); return $rows; } /** - * Query whether a given schema exists. Returns the name of the owner + * Query whether a given schema exists. Returns true if it does, false if it doesn't. */ function schemaExists( $schema ) { - $eschema = preg_replace("/'/", "''", $schema); - $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r " - ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'"; - $res = $this->query( $SQL ); - if ( $res && $res->numRows() ) { - $row = $res->fetchObject(); - $owner = $row->rolname; - } else { - $owner = false; - } - if ($res) - $this->freeResult($res); - return $owner; + $exists = $this->selectField( '"pg_catalog"."pg_namespace"', 1, + array( 'nspname' => $schema ), __METHOD__ ); + return (bool)$exists; } /** - * Query whether a given column exists in the mediawiki schema + * Returns true if a given role (i.e. user) exists, false otherwise. */ - function fieldExists( $table, $field, $fname = 'DatabasePostgres::fieldExists' ) { - global $wgDBmwschema; - $etable = preg_replace("/'/", "''", $table); - $eschema = preg_replace("/'/", "''", $wgDBmwschema); - $ecol = preg_replace("/'/", "''", $field); - $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a " - . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' " - . "AND a.attrelid = c.oid AND a.attname = '$ecol'"; - $res = $this->query( $SQL, $fname ); - $count = $res ? $res->numRows() : 0; - if ($res) - $this->freeResult( $res ); - return $count; + function roleExists( $roleName ) { + $exists = $this->selectField( '"pg_catalog"."pg_roles"', 1, + array( 'rolname' => $roleName ), __METHOD__ ); + return (bool)$exists; } function fieldInfo( $table, $field ) { - return PostgresField::fromText($this, $table, $field); + return PostgresField::fromText( $this, $table, $field ); } - + /** * pg_field_type() wrapper */ @@ -1277,119 +894,35 @@ SQL; return pg_field_type( $res, $index ); } - function begin( $fname = 'DatabasePostgres::begin' ) { - $this->query( 'BEGIN', $fname ); - $this->mTrxLevel = 1; - } - function immediateCommit( $fname = 'DatabasePostgres::immediateCommit' ) { - return true; - } - function commit( $fname = 'DatabasePostgres::commit' ) { - $this->query( 'COMMIT', $fname ); - $this->mTrxLevel = 0; - } - /* Not even sure why this is used in the main codebase... */ function limitResultForUpdate( $sql, $num ) { return $sql; } - function setup_database() { - global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser; - - // Make sure that we can write to the correct schema - // If not, Postgres will happily and silently go to the next search_path item - $ctest = "mediawiki_test_table"; - $safeschema = $this->quote_ident($wgDBmwschema); - if ($this->tableExists($ctest, $wgDBmwschema)) { - $this->doQuery("DROP TABLE $safeschema.$ctest"); - } - $SQL = "CREATE TABLE $safeschema.$ctest(a int)"; - $olde = error_reporting( 0 ); - $res = $this->doQuery($SQL); - error_reporting( $olde ); - if (!$res) { - print "FAILED. Make sure that the user \"" . htmlspecialchars( $wgDBuser ) . - "\" can write to the schema \"" . htmlspecialchars( $wgDBmwschema ) . "\"\n"; - dieout(""); # Will close the main list
      and finish the page. - } - $this->doQuery("DROP TABLE $safeschema.$ctest"); - - $res = $this->sourceFile( "../maintenance/postgres/tables.sql" ); - if ($res === true) { - print " done.\n"; - } else { - print " FAILED\n"; - dieout( htmlspecialchars( $res ) ); - } - - ## Update version information - $mwv = $this->addQuotes($wgVersion); - $pgv = $this->addQuotes($this->getServerVersion()); - $pgu = $this->addQuotes($this->mUser); - $mws = $this->addQuotes($wgDBmwschema); - $tss = $this->addQuotes($wgDBts2schema); - $pgp = $this->addQuotes($wgDBport); - $dbn = $this->addQuotes($this->mDBname); - $ctype = $this->addQuotes( pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0) ); - - $SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ". - "mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ". - "ctype = $ctype ". - "WHERE type = 'Creation'"; - $this->query($SQL); - - echo "
    • Populating interwiki table... "; - - ## Avoid the non-standard "REPLACE INTO" syntax - $f = fopen( "../maintenance/interwiki.sql", 'r' ); - if ($f == false ) { - print "FAILED
    • "; - dieout( "Could not find the interwiki.sql file" ); - } - ## We simply assume it is already empty as we have just created it - $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES "; - while ( ! feof( $f ) ) { - $line = fgets($f,1024); - $matches = array(); - if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) { - continue; - } - $this->query("$SQL $matches[1],$matches[2])"); - } - print " successfully populated.\n"; - - $this->doQuery("COMMIT"); - } - function encodeBlob( $b ) { - return new Blob ( pg_escape_bytea( $b ) ) ; + return new Blob( pg_escape_bytea( $this->mConn, $b ) ); } function decodeBlob( $b ) { - if ($b instanceof Blob) { + if ( $b instanceof Blob ) { $b = $b->fetch(); } return pg_unescape_bytea( $b ); } - function strencode( $s ) { ## Should not be called by us - return pg_escape_string( $s ); + function strencode( $s ) { # Should not be called by us + return pg_escape_string( $this->mConn, $s ); } function addQuotes( $s ) { if ( is_null( $s ) ) { return 'NULL'; - } else if ( is_bool( $s ) ) { + } elseif ( is_bool( $s ) ) { return intval( $s ); - } else if ($s instanceof Blob) { - return "'".$s->fetch($s)."'"; + } elseif ( $s instanceof Blob ) { + return "'" . $s->fetch( $s ) . "'"; } - return "'" . pg_escape_string($s) . "'"; - } - - function quote_ident( $s ) { - return '"' . preg_replace( '/"/', '""', $s) . '"'; + return "'" . pg_escape_string( $this->mConn, $s ) . "'"; } /** @@ -1403,15 +936,14 @@ SQL; * @return string SQL string */ protected function replaceVars( $ins ) { - $ins = parent::replaceVars( $ins ); - if ($this->numeric_version >= 8.3) { + if ( $this->numeric_version >= 8.3 ) { // Thanks for not providing backwards-compatibility, 8.3 $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins ); } - if ($this->numeric_version <= 8.1) { // Our minimum version + if ( $this->numeric_version <= 8.1 ) { // Our minimum version $ins = str_replace( 'USING gin', 'USING gist', $ins ); } @@ -1438,33 +970,35 @@ SQL; } } - if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY']; - if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}"; - if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY']; + if ( isset( $options['GROUP BY'] ) ) { + $preLimitTail .= ' GROUP BY ' . $options['GROUP BY']; + } + if ( isset( $options['HAVING'] ) ) { + $preLimitTail .= " HAVING {$options['HAVING']}"; + } + if ( isset( $options['ORDER BY'] ) ) { + $preLimitTail .= ' ORDER BY ' . $options['ORDER BY']; + } - //if (isset($options['LIMIT'])) { - // $tailOpts .= $this->limitResult('', $options['LIMIT'], - // isset($options['OFFSET']) ? $options['OFFSET'] - // : false); + //if ( isset( $options['LIMIT'] ) ) { + // $tailOpts .= $this->limitResult( '', $options['LIMIT'], + // isset( $options['OFFSET'] ) ? $options['OFFSET'] + // : false ); //} - 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'; + 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'; + } return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail ); } - /** - * How lagged is this slave? - * - */ - public function getLag() { - # Not implemented for PostgreSQL - return false; - } - - function setFakeSlaveLag( $lag ) {} function setFakeMaster( $enabled = true ) {} function getDBname() { @@ -1480,6 +1014,6 @@ SQL; } public function getSearchEngine() { - return "SearchPostgres"; + return 'SearchPostgres'; } } // end DatabasePostgres class diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php index c149cf04..503ebdf6 100644 --- a/includes/db/DatabaseSqlite.php +++ b/includes/db/DatabaseSqlite.php @@ -1,10 +1,10 @@ mFailFunction = $failFunction; - $this->mFlags = $flags; + function __construct( $server = false, $user = false, $password = false, $dbName = false, $flags = 0 ) { $this->mName = $dbName; - $this->open( $server, $user, $password, $dbName ); + parent::__construct( $server, $user, $password, $dbName, $flags ); + // parent doesn't open when $user is false, but we can work with $dbName + if( !$user && $dbName ) { + global $wgSharedDB; + if( $this->open( $server, $user, $password, $dbName ) && $wgSharedDB ) { + $this->attachDatabase( $wgSharedDB ); + } + } } function getType() { @@ -37,8 +44,8 @@ class DatabaseSqlite extends DatabaseBase { */ function implicitGroupby() { return false; } - static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 ) { - return new DatabaseSqlite( $server, $user, $password, $dbName, $failFunction, $flags ); + static function newFromParams( $server, $user, $password, $dbName, $flags = 0 ) { + return new DatabaseSqlite( $server, $user, $password, $dbName, $flags ); } /** Open an SQLite database and return a resource handle to it @@ -49,7 +56,8 @@ class DatabaseSqlite extends DatabaseBase { $fileName = self::generateFileName( $wgSQLiteDataDir, $dbName ); if ( !is_readable( $fileName ) ) { - throw new DBConnectionError( $this, "SQLite database not accessible" ); $this->mConn = false; + $this->mConn = false; + throw new DBConnectionError( $this, "SQLite database not accessible" ); } $this->openFile( $fileName ); return $this->mConn; @@ -71,14 +79,9 @@ class DatabaseSqlite extends DatabaseBase { } catch ( PDOException $e ) { $err = $e->getMessage(); } - if ( $this->mConn === false ) { + if ( !$this->mConn ) { wfDebug( "DB connection error: $err\n" ); - if ( !$this->mFailFunction ) { - throw new DBConnectionError( $this, $err ); - } else { - return false; - } - + throw new DBConnectionError( $this, $err ); } $this->mOpened = !!$this->mConn; # set error codes only, don't raise exceptions @@ -110,18 +113,65 @@ class DatabaseSqlite extends DatabaseBase { return "$dir/$dbName.sqlite"; } + /** + * Check if the searchindext table is FTS enabled. + * @returns false if not enabled. + */ + function checkForEnabledSearch() { + if ( self::$fulltextEnabled === null ) { + self::$fulltextEnabled = false; + $table = $this->tableName( 'searchindex' ); + $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ ); + if ( $res ) { + $row = $res->fetchRow(); + self::$fulltextEnabled = stristr($row['sql'], 'fts' ) !== false; + } + } + return self::$fulltextEnabled; + } + /** * Returns version of currently supported SQLite fulltext search module or false if none present. * @return String */ - function getFulltextSearchModule() { + static function getFulltextSearchModule() { + static $cachedResult = null; + if ( $cachedResult !== null ) { + return $cachedResult; + } + $cachedResult = false; $table = 'dummy_search_test'; - $this->query( "DROP TABLE IF EXISTS $table", __METHOD__ ); - if ( $this->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) { - $this->query( "DROP TABLE IF EXISTS $table", __METHOD__ ); - return 'FTS3'; + + $db = new DatabaseSqliteStandalone( ':memory:' ); + + if ( $db->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) { + $cachedResult = 'FTS3'; } - return false; + $db->close(); + return $cachedResult; + } + + /** + * Attaches external database to our connection, see http://sqlite.org/lang_attach.html + * for details. + * @param $name String: database name to be used in queries like SELECT foo FROM dbname.table + * @param $file String: database file name. If omitted, will be generated using $name and $wgSQLiteDataDir + * @param $fname String: calling function name + */ + function attachDatabase( $name, $file = false, $fname = 'DatabaseSqlite::attachDatabase' ) { + global $wgSQLiteDataDir; + if ( !$file ) { + $file = self::generateFileName( $wgSQLiteDataDir, $name ); + } + $file = $this->addQuotes( $file ); + return $this->query( "ATTACH DATABASE $file AS $name", $fname ); + } + + /** + * @see DatabaseBase::isWriteQuery() + */ + function isWriteQuery( $sql ) { + return parent::isWriteQuery( $sql ) && !preg_match( '/^ATTACH\b/i', $sql ); } /** @@ -140,25 +190,29 @@ class DatabaseSqlite extends DatabaseBase { } function freeResult( $res ) { - if ( $res instanceof ResultWrapper ) + if ( $res instanceof ResultWrapper ) { $res->result = null; - else + } else { $res = null; + } } function fetchObject( $res ) { - if ( $res instanceof ResultWrapper ) + if ( $res instanceof ResultWrapper ) { $r =& $res->result; - else + } else { $r =& $res; + } $cur = current( $r ); if ( is_array( $cur ) ) { next( $r ); $obj = new stdClass; - foreach ( $cur as $k => $v ) - if ( !is_numeric( $k ) ) + foreach ( $cur as $k => $v ) { + if ( !is_numeric( $k ) ) { $obj->$k = $v; + } + } return $obj; } @@ -166,11 +220,11 @@ class DatabaseSqlite extends DatabaseBase { } function fetchRow( $res ) { - if ( $res instanceof ResultWrapper ) + if ( $res instanceof ResultWrapper ) { $r =& $res->result; - else + } else { $r =& $res; - + } $cur = current( $r ); if ( is_array( $cur ) ) { next( $r ); @@ -205,6 +259,8 @@ class DatabaseSqlite extends DatabaseBase { * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks */ function tableName( $name ) { + // table names starting with sqlite_ are reserved + if ( strpos( $name, 'sqlite_' ) === 0 ) return $name; return str_replace( '`', '', parent::tableName( $name ) ); } @@ -223,19 +279,23 @@ class DatabaseSqlite extends DatabaseBase { } function dataSeek( $res, $row ) { - if ( $res instanceof ResultWrapper ) + if ( $res instanceof ResultWrapper ) { $r =& $res->result; - else + } else { $r =& $res; + } reset( $r ); - if ( $row > 0 ) - for ( $i = 0; $i < $row; $i++ ) + if ( $row > 0 ) { + for ( $i = 0; $i < $row; $i++ ) { next( $r ); + } + } } function lastError() { - if ( !is_object( $this->mConn ) ) + if ( !is_object( $this->mConn ) ) { return "Cannot return last error, no db connection"; + } $e = $this->mConn->errorInfo(); return isset( $e[2] ) ? $e[2] : ''; } @@ -298,9 +358,11 @@ class DatabaseSqlite extends DatabaseBase { * Filter the options used in SELECT statements */ function makeSelectOptions( $options ) { - foreach ( $options as $k => $v ) - if ( is_numeric( $k ) && $v == 'FOR UPDATE' ) + foreach ( $options as $k => $v ) { + if ( is_numeric( $k ) && $v == 'FOR UPDATE' ) { $options[$k] = ''; + } + } return parent::makeSelectOptions( $options ); } @@ -308,20 +370,28 @@ class DatabaseSqlite extends DatabaseBase { * Based on generic method (parent) with some prior SQLite-sepcific adjustments */ function insert( $table, $a, $fname = 'DatabaseSqlite::insert', $options = array() ) { - if ( !count( $a ) ) return true; - if ( !is_array( $options ) ) $options = array( $options ); + if ( !count( $a ) ) { + return true; + } + if ( !is_array( $options ) ) { + $options = array( $options ); + } # SQLite uses OR IGNORE not just IGNORE - foreach ( $options as $k => $v ) - if ( $v == 'IGNORE' ) + foreach ( $options as $k => $v ) { + if ( $v == 'IGNORE' ) { $options[$k] = 'OR IGNORE'; + } + } # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts if ( isset( $a[0] ) && is_array( $a[0] ) ) { $ret = true; - foreach ( $a as $k => $v ) - if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) + foreach ( $a as $v ) { + if ( !parent::insert( $table, $v, "$fname/multi-row", $options ) ) { $ret = false; + } + } } else { $ret = parent::insert( $table, $a, "$fname/single-row", $options ); } @@ -331,13 +401,15 @@ class DatabaseSqlite extends DatabaseBase { function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) { if ( !count( $rows ) ) return true; - + # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries if ( isset( $rows[0] ) && is_array( $rows[0] ) ) { $ret = true; - foreach ( $rows as $k => $v ) - if ( !parent::replace( $table, $uniqueIndexes, $v, "$fname/multi-row" ) ) + foreach ( $rows as $v ) { + if ( !parent::replace( $table, $uniqueIndexes, $v, "$fname/multi-row" ) ) { $ret = false; + } + } } else { $ret = parent::replace( $table, $uniqueIndexes, $rows, "$fname/single-row" ); } @@ -362,6 +434,10 @@ class DatabaseSqlite extends DatabaseBase { return implode( $glue, $sqls ); } + public function unixTimestamp( $field ) { + return $field; + } + function wasDeadlock() { return $this->lastErrno() == 5; // SQLITE_BUSY } @@ -377,7 +453,7 @@ class DatabaseSqlite extends DatabaseBase { /** * @return string wikitext of a link to the server software's web site */ - function getSoftwareLink() { + public static function getSoftwareLink() { return "[http://sqlite.org/ SQLite]"; } @@ -390,11 +466,10 @@ class DatabaseSqlite extends DatabaseBase { } /** - * Query whether a given column exists in the mediawiki schema + * @return string User-friendly database information */ - function fieldExists( $table, $field, $fname = '' ) { - $info = $this->fieldInfo( $table, $field ); - return (bool)$info; + public function getServerInfo() { + return wfMsg( self::getFulltextSearchModule() ? 'sqlite-has-fts' : 'sqlite-no-fts', $this->getServerVersion() ); } /** @@ -458,10 +533,6 @@ class DatabaseSqlite extends DatabaseBase { } } - function quote_ident( $s ) { - return $s; - } - function buildLike() { $params = func_get_args(); if ( count( $params ) > 0 && is_array( $params[0] ) ) { @@ -470,43 +541,6 @@ class DatabaseSqlite extends DatabaseBase { return parent::buildLike( $params ) . "ESCAPE '\' "; } - /** - * How lagged is this slave? - */ - public function getLag() { - return 0; - } - - /** - * Called by the installer script (when modified according to the MediaWikiLite installation instructions) - * - this is the same way PostgreSQL works, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile) - */ - public function setup_database() { - global $IP; - - # Process common MySQL/SQLite table definitions - $err = $this->sourceFile( "$IP/maintenance/tables.sql" ); - if ( $err !== true ) { - echo " FAILED"; - dieout( htmlspecialchars( $err ) ); - } - echo " done."; - - # Use DatabasePostgres's code to populate interwiki from MySQL template - $f = fopen( "$IP/maintenance/interwiki.sql", 'r' ); - if ( $f == false ) { - dieout( "Could not find the interwiki.sql file." ); - } - - $sql = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES "; - while ( !feof( $f ) ) { - $line = fgets( $f, 1024 ); - $matches = array(); - if ( !preg_match( '/^\s*(\(.+?),(\d)\)/', $line, $matches ) ) continue; - $this->query( "$sql $matches[1],$matches[2])" ); - } - } - public function getSearchEngine() { return "SearchSqlite"; } @@ -530,9 +564,11 @@ class DatabaseSqlite extends DatabaseBase { // no such thing as unsigned $s = preg_replace( '/\b(un)?signed\b/i', '', $s ); // INT -> INTEGER - $s = preg_replace( '/\b(tiny|small|medium|big|)int(\([\s\d]*\)|\b)/i', 'INTEGER', $s ); + $s = preg_replace( '/\b(tiny|small|medium|big|)int(\s*\(\s*\d+\s*\)|\b)/i', 'INTEGER', $s ); + // floating point types -> REAL + $s = preg_replace( '/\b(float|double(\s+precision)?)(\s*\(\s*\d+\s*(,\s*\d+\s*)?\)|\b)/i', 'REAL', $s ); // varchar -> TEXT - $s = preg_replace( '/\bvarchar\(\d+\)/i', 'TEXT', $s ); + $s = preg_replace( '/\b(var)?char\s*\(.*?\)/i', 'TEXT', $s ); // TEXT normalization $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s ); // BLOB normalization @@ -542,13 +578,15 @@ class DatabaseSqlite extends DatabaseBase { // DATETIME -> TEXT $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s ); // No ENUM type - $s = preg_replace( '/enum\([^)]*\)/i', 'BLOB', $s ); + $s = preg_replace( '/\benum\s*\([^)]*\)/i', 'TEXT', $s ); // binary collation type -> nothing $s = preg_replace( '/\bbinary\b/i', '', $s ); // auto_increment -> autoincrement $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s ); // No explicit options $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s ); + // AUTOINCREMENT should immedidately follow PRIMARY KEY + $s = preg_replace( '/primary key (.*?) autoincrement/i', 'PRIMARY KEY AUTOINCREMENT $1', $s ); } elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) { // No truncated indexes $s = preg_replace( '/\(\d+\)/', '', $s ); @@ -585,6 +623,7 @@ class DatabaseSqlite extends DatabaseBase { class DatabaseSqliteStandalone extends DatabaseSqlite { public function __construct( $fileName, $flags = 0 ) { $this->mFlags = $flags; + $this->tablePrefix( null ); $this->openFile( $fileName ); } } @@ -592,7 +631,7 @@ class DatabaseSqliteStandalone extends DatabaseSqlite { /** * @ingroup Database */ -class SQLiteField { +class SQLiteField implements Field { private $info, $tableName; function __construct( $info, $tableName ) { $this->info = $info; @@ -617,18 +656,10 @@ class SQLiteField { return $this->info->dflt_value; } - function maxLength() { - return -1; + function isNullable() { + return !$this->info->notnull; } - function nullable() { - // SQLite dynamic types are always nullable - return true; - } - - # isKey(), isMultipleKey() not implemented, MySQL-specific concept. - # Suggest removal from base class [TS] - function type() { return $this->info->type; } diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php index 10c87133..f84a70e5 100644 --- a/includes/db/LBFactory.php +++ b/includes/db/LBFactory.php @@ -1,5 +1,7 @@ startupPos === null ) { @@ -233,7 +293,7 @@ class ChronologyProtector { * Notify the ChronologyProtector that the LoadBalancer is about to shut * down. Saves replication positions. * - * @param LoadBalancer $lb + * @param $lb LoadBalancer */ function shutdownLB( $lb ) { // Don't start a session, don't bother with non-replicated setups diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php index 820aa2ea..0d411ec6 100644 --- a/includes/db/LBFactory_Multi.php +++ b/includes/db/LBFactory_Multi.php @@ -1,5 +1,7 @@ lastWiki === $wiki ) { return $this->lastSection; } - list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki ); + list( $dbName, ) = $this->getDBNameAndPrefix( $wiki ); if ( isset( $this->sectionsByDB[$dbName] ) ) { $section = $this->sectionsByDB[$dbName]; } else { @@ -97,7 +99,7 @@ class LBFactory_Multi extends LBFactory { } function newMainLB( $wiki = false ) { - list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki ); + list( $dbName, ) = $this->getDBNameAndPrefix( $wiki ); $section = $this->getSectionForWiki( $wiki ); $groupLoads = array(); if ( isset( $this->groupLoadsByDB[$dbName] ) ) { diff --git a/includes/db/LBFactory_Single.php b/includes/db/LBFactory_Single.php new file mode 100644 index 00000000..25acdc5b --- /dev/null +++ b/includes/db/LBFactory_Single.php @@ -0,0 +1,57 @@ +lb = new LoadBalancer_Single( $conf ); + } + + function newMainLB( $wiki = false ) { + return $this->lb; + } + + function getMainLB( $wiki = false ) { + return $this->lb; + } + + function newExternalLB( $cluster, $wiki = false ) { + return $this->lb; + } + + function &getExternalLB( $cluster, $wiki = false ) { + return $this->lb; + } + + function forEachLB( $callback, $params = array() ) { + call_user_func_array( $callback, array_merge( array( $this->lb ), $params ) ); + } +} + +/** + * Helper class for LBFactory_Single. + */ +class LoadBalancer_Single extends LoadBalancer { + var $db; + + function __construct( $params ) { + $this->db = $params['connection']; + parent::__construct( array( 'servers' => array( array( + 'type' => $this->db->getType(), + 'host' => $this->db->getServer(), + 'dbname' => $this->db->getDBname(), + 'load' => 1, + ) ) ) ); + } + + function reallyOpenConnection( $server, $dbNameOverride = false ) { + return $this->db; + } +} diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php index 083b70b3..d899ce07 100644 --- a/includes/db/LoadBalancer.php +++ b/includes/db/LoadBalancer.php @@ -1,5 +1,7 @@ mServers = $params['servers']; - if ( isset( $params['failFunction'] ) ) { - $this->mFailFunction = $params['failFunction']; - } else { - $this->mFailFunction = false; - } if ( isset( $params['waitTimeout'] ) ) { $this->mWaitTimeout = $params['waitTimeout']; } else { @@ -54,7 +50,7 @@ class LoadBalancer { $this->mWaitForPos = false; $this->mLaggedSlaveMode = false; $this->mErrorConnection = false; - $this->mAllowLag = false; + $this->mAllowLagged = false; $this->mLoadMonitorClass = isset( $params['loadMonitor'] ) ? $params['loadMonitor'] : 'LoadMonitor_MySQL'; @@ -71,11 +67,6 @@ class LoadBalancer { } } - static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 ) - { - return new LoadBalancer( $servers, $failFunction, $waitTimeout ); - } - /** * Get a LoadMonitor instance */ @@ -129,11 +120,11 @@ class LoadBalancer { # Unset excessively lagged servers $lags = $this->getLagTimes( $wiki ); foreach ( $lags as $i => $lag ) { - if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) ) { + if ( $i != 0 ) { if ( $lag === false ) { wfDebug( "Server #$i is not replicating\n" ); unset( $loads[$i] ); - } elseif ( $lag > $this->mServers[$i]['max lag'] ) { + } elseif ( isset( $this->mServers[$i]['max lag'] ) && $lag > $this->mServers[$i]['max lag'] ) { wfDebug( "Server #$i is excessively lagged ($lag seconds)\n" ); unset( $loads[$i] ); } @@ -214,8 +205,6 @@ class LoadBalancer { # Scale the configured load ratios according to the dynamic load (if the load monitor supports it) $this->getLoadMonitor()->scaleLoads( $nonErrorLoads, $group, $wiki ); - $i = false; - $found = false; $laggedSlaveMode = false; # First try quickly looking through the available servers for a server that @@ -231,7 +220,8 @@ class LoadBalancer { $i = $this->getRandomNonLagged( $currentLoads, $wiki ); if ( $i === false && count( $currentLoads ) != 0 ) { # All slaves lagged. Switch to read-only mode - $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' ); + $wgReadOnly = 'The database has been automatically locked ' . + 'while the slave database servers catch up to the master'; $i = $this->pickRandom( $currentLoads ); $laggedSlaveMode = true; } @@ -331,14 +321,6 @@ class LoadBalancer { return $t; } - /** - * Get a random server to use in a query group - * @deprecated use getReaderIndex - */ - function getGroupIndex( $group ) { - return $this->getReaderIndex( $group ); - } - /** * Set the master wait position * If a DB_SLAVE connection has been opened already, waits @@ -357,13 +339,25 @@ class LoadBalancer { } wfProfileOut( __METHOD__ ); } + + /** + * Set the master wait position and wait for ALL slaves to catch up to it + */ + public function waitForAll( $pos ) { + wfProfileIn( __METHOD__ ); + $this->mWaitForPos = $pos; + for ( $i = 1; $i < count( $this->mServers ); $i++ ) { + $this->doWait( $i ); + } + wfProfileOut( __METHOD__ ); + } /** * Get any open connection to a given server index, local or foreign * Returns false if there is no connection open */ function getAnyOpenConnection( $i ) { - foreach ( $this->mConns as $type => $conns ) { + foreach ( $this->mConns as $conns ) { if ( !empty( $conns[$i] ) ) { return reset( $conns[$i] ); } @@ -398,12 +392,14 @@ class LoadBalancer { /** * Get a connection by index * This is the main entry point for this class. - * @param int $i Database - * @param array $groups Query groups - * @param string $wiki Wiki ID + * + * @param $i Integer: server index + * @param $groups Array: query groups + * @param $wiki String: wiki ID + * + * @return DatabaseBase */ public function &getConnection( $i, $groups = array(), $wiki = false ) { - global $wgDBtype; wfProfileIn( __METHOD__ ); if ( $i == DB_LAST ) { @@ -445,6 +441,7 @@ class LoadBalancer { if ( $i === false ) { $this->mLastError = 'No working slave server: ' . $this->mLastError; $this->reportConnectionError( $this->mErrorConnection ); + wfProfileOut( __METHOD__ ); return false; } } @@ -509,9 +506,9 @@ class LoadBalancer { * On error, returns false, and the connection which caused the * error will be available via $this->mErrorConnection. * - * @param integer $i Server index - * @param string $wiki Wiki ID to open - * @return Database + * @param $i Integer: server index + * @param $wiki String: wiki ID to open + * @return DatabaseBase * * @access private */ @@ -554,9 +551,9 @@ class LoadBalancer { * On error, returns false, and the connection which caused the * error will be available via $this->mErrorConnection. * - * @param integer $i Server index - * @param string $wiki Wiki ID to open - * @return Database + * @param $i Integer: server index + * @param $wiki String: wiki ID to open + * @return DatabaseBase */ function openForeignConnection( $i, $wiki ) { wfProfileIn(__METHOD__); @@ -615,6 +612,8 @@ class LoadBalancer { /** * Test if the specified index represents an open connection + * + * @param $index Integer: server index * @access private */ function isOpen( $index ) { @@ -634,21 +633,26 @@ class LoadBalancer { throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' ); } - extract( $server ); + $host = $server['host']; + $dbname = $server['dbname']; + if ( $dbNameOverride !== false ) { - $dbname = $dbNameOverride; + $server['dbname'] = $dbname = $dbNameOverride; } - # Get class for this database type - $class = 'Database' . ucfirst( $type ); - # Create object wfDebug( "Connecting to $host $dbname...\n" ); - $db = new $class( $host, $user, $password, $dbname, 1, $flags ); + try { + $db = DatabaseBase::newFromType( $server['type'], $server ); + } catch ( DBConnectionError $e ) { + // FIXME: This is probably the ugliest thing I have ever done to + // PHP. I'm half-expecting it to segfault, just out of disgust. -- TS + $db = $e->db; + } if ( $db->isOpen() ) { - wfDebug( "Connected\n" ); + wfDebug( "Connected to $host $dbname.\n" ); } else { - wfDebug( "Failed\n" ); + wfDebug( "Connection failed to $host $dbname.\n" ); } $db->setLBInfo( $server ); if ( isset( $server['fakeSlaveLag'] ) ) { @@ -667,19 +671,9 @@ class LoadBalancer { // No last connection, probably due to all servers being too busy wfLogDBError( "LB failure with no last connection\n" ); $conn = new Database; - if ( $this->mFailFunction ) { - $conn->failFunction( $this->mFailFunction ); - $conn->reportConnectionError( $this->mLastError ); - } else { - // If all servers were busy, mLastError will contain something sensible - throw new DBConnectionError( $conn, $this->mLastError ); - } + // If all servers were busy, mLastError will contain something sensible + throw new DBConnectionError( $conn, $this->mLastError ); } else { - if ( $this->mFailFunction ) { - $conn->failFunction( $this->mFailFunction ); - } else { - $conn->failFunction( false ); - } $server = $conn->getProperty( 'mServer' ); wfLogDBError( "Connection error: {$this->mLastError} ({$server})\n" ); $conn->reportConnectionError( "{$this->mLastError} ({$server})" ); @@ -778,12 +772,21 @@ class LoadBalancer { ); } + /** + * Deprecated function, typo in function name + */ + function closeConnecton( $conn ) { + $this->closeConnection( $conn ); + } + /** * Close a connection * Using this function makes sure the LoadBalancer knows the connection is closed. * If you use $conn->close() directly, the load balancer won't update its state. + * @param $conn + * @return void */ - function closeConnecton( $conn ) { + function closeConnection( $conn ) { $done = false; foreach ( $this->mConns as $i1 => $conns2 ) { foreach ( $conns2 as $i2 => $conns3 ) { @@ -819,7 +822,7 @@ class LoadBalancer { function commitMasterChanges() { // Always 0, but who knows.. :) $masterIndex = $this->getWriterIndex(); - foreach ( $this->mConns as $type => $conns2 ) { + foreach ( $this->mConns as $conns2 ) { if ( empty( $conns2[$masterIndex] ) ) { continue; } diff --git a/includes/db/LoadMonitor.php b/includes/db/LoadMonitor.php index 929ab2b9..9b959728 100644 --- a/includes/db/LoadMonitor.php +++ b/includes/db/LoadMonitor.php @@ -1,9 +1,16 @@